Compare commits
1 Commits
43683-inco
...
nixos-test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dad6f2480d |
12
.github/actions/build_docs/action.yml
vendored
12
.github/actions/build_docs/action.yml
vendored
@@ -19,18 +19,6 @@ 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:
|
||||
|
||||
2
.github/workflows/autofix_pr.yml
vendored
2
.github/workflows/autofix_pr.yml
vendored
@@ -90,7 +90,7 @@ jobs:
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
name: autofix_pr::commit_changes::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
|
||||
2
.github/workflows/cherry_pick.yml
vendored
2
.github/workflows/cherry_pick.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
name: cherry_pick::run_cherry_pick::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
|
||||
@@ -1,40 +1,29 @@
|
||||
name: "Close Stale Issues"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 2 * * 5"
|
||||
- cron: "0 8 31 DEC *"
|
||||
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@997185467fa4f803885201cee163a9f38240193d # v10
|
||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: >
|
||||
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.
|
||||
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.
|
||||
|
||||
Thanks for your help!
|
||||
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."
|
||||
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."
|
||||
days-before-stale: 60
|
||||
days-before-close: 14
|
||||
only-issue-types: "Bug,Crash"
|
||||
operations-per-run: ${{ inputs.operations-per-run || 1000 }}
|
||||
operations-per-run: 1000
|
||||
ascending: true
|
||||
enable-statistics: true
|
||||
debug-only: ${{ inputs.debug-only }}
|
||||
stale-issue-label: "stale"
|
||||
exempt-issue-labels: "never stale"
|
||||
|
||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -74,6 +74,12 @@ jobs:
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::trigger_autofix
|
||||
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -472,17 +478,11 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
|
||||
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: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
|
||||
run: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
notify_on_failure:
|
||||
needs:
|
||||
- upload_release_assets
|
||||
|
||||
17
.github/workflows/run_tests.yml
vendored
17
.github/workflows/run_tests.yml
vendored
@@ -74,12 +74,18 @@ jobs:
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||
with:
|
||||
version: '9'
|
||||
- name: steps::prettier
|
||||
- name: ./script/prettier
|
||||
run: ./script/prettier
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::trigger_autofix
|
||||
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=false
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: ./script/check-todos
|
||||
run: ./script/check-todos
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -160,6 +166,12 @@ jobs:
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::trigger_autofix
|
||||
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -353,9 +365,6 @@ 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:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -36,7 +36,6 @@
|
||||
DerivedData/
|
||||
Packages
|
||||
xcuserdata/
|
||||
crates/docs_preprocessor/actions.json
|
||||
|
||||
# Don't commit any secrets to the repo.
|
||||
.env
|
||||
|
||||
50
Cargo.lock
generated
50
Cargo.lock
generated
@@ -226,9 +226,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.9.2"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3e527d7dfe0f334313d42d1d9318f0a79665f6f21c440d0798f230a77a7ed6c"
|
||||
checksum = "c2ffe7d502c1e451aafc5aff655000f84d09c9af681354ac0012527009b1af13"
|
||||
dependencies = [
|
||||
"agent-client-protocol-schema",
|
||||
"anyhow",
|
||||
@@ -243,9 +243,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol-schema"
|
||||
version = "0.10.5"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6903a00e8ac822f9bacac59a1932754d7387c72ebb7c9c7439ad021505591da4"
|
||||
checksum = "8af81cc2d5c3f9c04f73db452efd058333735ba9d51c2cf7ef33c9fee038e7e6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"derive_more 2.0.1",
|
||||
@@ -793,7 +793,7 @@ dependencies = [
|
||||
"url",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols 0.32.9",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
@@ -5021,6 +5021,8 @@ name = "docs_preprocessor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command_palette",
|
||||
"gpui",
|
||||
"mdbook",
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -5029,6 +5031,7 @@ dependencies = [
|
||||
"task",
|
||||
"theme",
|
||||
"util",
|
||||
"zed",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
@@ -7367,7 +7370,7 @@ dependencies = [
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-cursor",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols 0.31.2",
|
||||
"wayland-protocols-plasma",
|
||||
"wayland-protocols-wlr",
|
||||
"windows 0.61.3",
|
||||
@@ -8929,8 +8932,6 @@ dependencies = [
|
||||
"credentials_provider",
|
||||
"deepseek",
|
||||
"editor",
|
||||
"extension",
|
||||
"extension_host",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"google_ai",
|
||||
@@ -12570,7 +12571,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"notifications",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rayon",
|
||||
@@ -12648,8 +12648,6 @@ dependencies = [
|
||||
"paths",
|
||||
"rope",
|
||||
"serde",
|
||||
"strum 0.27.2",
|
||||
"tempfile",
|
||||
"text",
|
||||
"util",
|
||||
"uuid",
|
||||
@@ -18929,6 +18927,18 @@ dependencies = [
|
||||
"xcursor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.32.9"
|
||||
@@ -18943,14 +18953,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-plasma"
|
||||
version = "0.3.9"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032"
|
||||
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols 0.31.2",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
@@ -18963,7 +18973,7 @@ dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols 0.32.9",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
@@ -20265,16 +20275,6 @@ dependencies = [
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "worktree_benchmarks"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"fs",
|
||||
"gpui",
|
||||
"settings",
|
||||
"worktree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.1"
|
||||
|
||||
@@ -198,7 +198,6 @@ members = [
|
||||
"crates/web_search_providers",
|
||||
"crates/workspace",
|
||||
"crates/worktree",
|
||||
"crates/worktree_benchmarks",
|
||||
"crates/x_ai",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
@@ -439,7 +438,7 @@ ztracing_macro = { path = "crates/ztracing_macro" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = { version = "=0.9.2", features = ["unstable"] }
|
||||
agent-client-protocol = { version = "=0.9.0", features = ["unstable"] }
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = "0.25.1-rc1"
|
||||
any_vec = "0.14"
|
||||
|
||||
@@ -20,6 +20,7 @@ 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
|
||||
|
||||
|
||||
@@ -227,7 +227,6 @@
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -294,7 +293,6 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -306,7 +304,6 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -267,7 +267,6 @@
|
||||
"cmd-shift-g": "search::SelectPreviousMatch",
|
||||
"cmd-k l": "agent::OpenRulesLibrary",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -336,7 +335,6 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -349,7 +347,6 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -227,7 +227,6 @@
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -297,7 +296,6 @@
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -310,7 +308,6 @@
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1178,10 +1178,6 @@
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
// Whether to start a new line with a comment when a previous line is a comment as well.
|
||||
"extend_comment_on_newline": true,
|
||||
// Whether to continue markdown lists when pressing enter.
|
||||
"extend_list_on_newline": true,
|
||||
// Whether to indent list items when pressing tab after a list marker.
|
||||
"indent_list_on_tab": true,
|
||||
// Removes any lines containing only whitespace at the end of the file and
|
||||
// ensures just one newline at the end.
|
||||
"ensure_final_newline_on_save": true,
|
||||
@@ -1325,14 +1321,6 @@
|
||||
"hidden_files": ["**/.*"],
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Global switch to enable or disable all git integration features.
|
||||
// If set to true, disables all git integration features.
|
||||
// If set to false, individual git integration features below will be independently enabled or disabled.
|
||||
"disable_git": false,
|
||||
// Whether to enable git status tracking.
|
||||
"enable_status": true,
|
||||
// Whether to enable git diff display.
|
||||
"enable_diff": true,
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
// 1. Show the gutter
|
||||
// "git_gutter": "tracked_files"
|
||||
@@ -1717,12 +1705,7 @@
|
||||
// }
|
||||
//
|
||||
"file_types": {
|
||||
"JSONC": [
|
||||
"**/.zed/*.json",
|
||||
"**/.vscode/**/*.json",
|
||||
"**/{zed,Zed}/{settings,keymap,tasks,debug}.json",
|
||||
"tsconfig*.json",
|
||||
],
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"],
|
||||
"Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"],
|
||||
"Shell Script": [".env.*"],
|
||||
},
|
||||
|
||||
@@ -192,7 +192,6 @@ pub struct ToolCall {
|
||||
pub locations: Vec<acp::ToolCallLocation>,
|
||||
pub resolved_locations: Vec<Option<AgentLocation>>,
|
||||
pub raw_input: Option<serde_json::Value>,
|
||||
pub raw_input_markdown: Option<Entity<Markdown>>,
|
||||
pub raw_output: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
@@ -223,11 +222,6 @@ impl ToolCall {
|
||||
}
|
||||
}
|
||||
|
||||
let raw_input_markdown = tool_call
|
||||
.raw_input
|
||||
.as_ref()
|
||||
.and_then(|input| markdown_for_raw_output(input, &language_registry, cx));
|
||||
|
||||
let result = Self {
|
||||
id: tool_call.tool_call_id,
|
||||
label: cx
|
||||
@@ -238,7 +232,6 @@ impl ToolCall {
|
||||
resolved_locations: Vec::default(),
|
||||
status,
|
||||
raw_input: tool_call.raw_input,
|
||||
raw_input_markdown,
|
||||
raw_output: tool_call.raw_output,
|
||||
};
|
||||
Ok(result)
|
||||
@@ -314,7 +307,6 @@ impl ToolCall {
|
||||
}
|
||||
|
||||
if let Some(raw_input) = raw_input {
|
||||
self.raw_input_markdown = markdown_for_raw_output(&raw_input, &language_registry, cx);
|
||||
self.raw_input = Some(raw_input);
|
||||
}
|
||||
|
||||
@@ -1363,7 +1355,6 @@ impl AcpThread {
|
||||
locations: Vec::new(),
|
||||
resolved_locations: Vec::new(),
|
||||
raw_input: None,
|
||||
raw_input_markdown: None,
|
||||
raw_output: None,
|
||||
};
|
||||
self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);
|
||||
|
||||
@@ -210,21 +210,12 @@ 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<AgentModelIcon>,
|
||||
pub icon: Option<IconName>,
|
||||
}
|
||||
|
||||
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::{IconOrSvg, LanguageModel, LanguageModelProvider, LanguageModelRegistry};
|
||||
use language_model::{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)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.into_iter()
|
||||
.filter(|provider| provider.is_authenticated(cx))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -153,10 +153,7 @@ impl LanguageModels {
|
||||
id: Self::model_id(model),
|
||||
name: model.name().0,
|
||||
description: None,
|
||||
icon: Some(match provider.icon() {
|
||||
IconOrSvg::Svg(path) => acp_thread::AgentModelIcon::Path(path),
|
||||
IconOrSvg::Icon(name) => acp_thread::AgentModelIcon::Named(name),
|
||||
}),
|
||||
icon: Some(provider.icon()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +164,7 @@ impl LanguageModels {
|
||||
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
|
||||
let authenticate_all_providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.iter()
|
||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -429,7 +426,7 @@ impl NativeAgent {
|
||||
.into_iter()
|
||||
.flat_map(|(contents, prompt_metadata)| match contents {
|
||||
Ok(contents) => Some(UserRulesContext {
|
||||
uuid: prompt_metadata.id.as_user()?,
|
||||
uuid: prompt_metadata.id.user_id()?,
|
||||
title: prompt_metadata.title.map(|title| title.to_string()),
|
||||
contents,
|
||||
}),
|
||||
@@ -1633,9 +1630,7 @@ mod internal_tests {
|
||||
id: acp::ModelId::new("fake/fake"),
|
||||
name: "Fake".into(),
|
||||
description: None,
|
||||
icon: Some(acp_thread::AgentModelIcon::Named(
|
||||
ui::IconName::ZedAssistant
|
||||
)),
|
||||
icon: Some(ui::IconName::ZedAssistant),
|
||||
}]
|
||||
)])
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use util::{ResultExt, debug_panic};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::{Chat, PasteRaw};
|
||||
use zed_actions::agent::Chat;
|
||||
|
||||
pub struct MessageEditor {
|
||||
mention_set: Entity<MentionSet>,
|
||||
@@ -543,9 +543,6 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let editor_clipboard_selections = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.entries().first().cloned())
|
||||
@@ -556,127 +553,133 @@ impl MessageEditor {
|
||||
_ => None,
|
||||
});
|
||||
|
||||
// Insert creases for pasted clipboard selections that:
|
||||
// 1. Contain exactly one selection
|
||||
// 2. Have an associated file path
|
||||
// 3. Span multiple lines (not single-line selections)
|
||||
// 4. Belong to a file that exists in the current project
|
||||
let should_insert_creases = util::maybe!({
|
||||
let selections = editor_clipboard_selections.as_ref()?;
|
||||
if selections.len() > 1 {
|
||||
return Some(false);
|
||||
}
|
||||
let selection = selections.first()?;
|
||||
let file_path = selection.file_path.as_ref()?;
|
||||
let line_range = selection.line_range.as_ref()?;
|
||||
let has_file_context = editor_clipboard_selections
|
||||
.as_ref()
|
||||
.is_some_and(|selections| {
|
||||
selections
|
||||
.iter()
|
||||
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
|
||||
});
|
||||
|
||||
if line_range.start() == line_range.end() {
|
||||
return Some(false);
|
||||
}
|
||||
|
||||
Some(
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some(),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if should_insert_creases && let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
let insertion_target = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.start
|
||||
.text_anchor;
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let crease_text =
|
||||
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
|
||||
|
||||
let mention_uri = MentionUri::Selection {
|
||||
abs_path: Some(file_path.clone()),
|
||||
line_range: line_range.clone(),
|
||||
};
|
||||
|
||||
let mention_text = mention_uri.as_link().to_string();
|
||||
let (excerpt_id, text_anchor, content_len) =
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
|
||||
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
|
||||
|
||||
editor.insert(&mention_text, window, cx);
|
||||
editor.insert(" ", window, cx);
|
||||
|
||||
(*excerpt_id, text_anchor, mention_text.len())
|
||||
});
|
||||
|
||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
crease_text.into(),
|
||||
mention_uri.icon_path(cx),
|
||||
None,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
drop(tx);
|
||||
|
||||
let mention_task = cx
|
||||
.spawn({
|
||||
let project = project.clone();
|
||||
async move |_, cx| {
|
||||
let project_path = project
|
||||
.update(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&file_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "project path not found".to_string())?;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||
.map_err(|e| e.to_string())?
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
buffer
|
||||
.update(cx, |buffer, cx| {
|
||||
let start = Point::new(*line_range.start(), 0)
|
||||
.min(buffer.max_point());
|
||||
let end = Point::new(*line_range.end() + 1, 0)
|
||||
.min(buffer.max_point());
|
||||
let content = buffer.text_for_range(start..end).collect();
|
||||
Mention::Text {
|
||||
content,
|
||||
tracked_buffers: vec![cx.entity()],
|
||||
}
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
self.mention_set.update(cx, |mention_set, _cx| {
|
||||
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
|
||||
});
|
||||
if has_file_context {
|
||||
if let Some((workspace, selections)) =
|
||||
self.workspace.upgrade().zip(editor_clipboard_selections)
|
||||
{
|
||||
let Some(first_selection) = selections.first() else {
|
||||
return;
|
||||
};
|
||||
if let Some(file_path) = &first_selection.file_path {
|
||||
// In case someone pastes selections from another window
|
||||
// with a different project, we don't want to insert the
|
||||
// crease (containing the absolute path) since the agent
|
||||
// cannot access files outside the project.
|
||||
let is_in_project = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some();
|
||||
if !is_in_project {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cx.stop_propagation();
|
||||
let insertion_target = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.start
|
||||
.text_anchor;
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let crease_text =
|
||||
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
|
||||
|
||||
let mention_uri = MentionUri::Selection {
|
||||
abs_path: Some(file_path.clone()),
|
||||
line_range: line_range.clone(),
|
||||
};
|
||||
|
||||
let mention_text = mention_uri.as_link().to_string();
|
||||
let (excerpt_id, text_anchor, content_len) =
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let (excerpt_id, _, buffer_snapshot) =
|
||||
snapshot.as_singleton().unwrap();
|
||||
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
|
||||
|
||||
editor.insert(&mention_text, window, cx);
|
||||
editor.insert(" ", window, cx);
|
||||
|
||||
(*excerpt_id, text_anchor, mention_text.len())
|
||||
});
|
||||
|
||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
crease_text.into(),
|
||||
mention_uri.icon_path(cx),
|
||||
None,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
drop(tx);
|
||||
|
||||
let mention_task = cx
|
||||
.spawn({
|
||||
let project = project.clone();
|
||||
async move |_, cx| {
|
||||
let project_path = project
|
||||
.update(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&file_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "project path not found".to_string())?;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
buffer
|
||||
.update(cx, |buffer, cx| {
|
||||
let start = Point::new(*line_range.start(), 0)
|
||||
.min(buffer.max_point());
|
||||
let end = Point::new(*line_range.end() + 1, 0)
|
||||
.min(buffer.max_point());
|
||||
let content =
|
||||
buffer.text_for_range(start..end).collect();
|
||||
Mention::Text {
|
||||
content,
|
||||
tracked_buffers: vec![cx.entity()],
|
||||
}
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
self.mention_set.update(cx, |mention_set, _cx| {
|
||||
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if self.prompt_capabilities.borrow().image
|
||||
@@ -687,13 +690,6 @@ impl MessageEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let editor = self.editor.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
|
||||
});
|
||||
}
|
||||
|
||||
pub fn insert_dragged_files(
|
||||
&mut self,
|
||||
paths: Vec<project::ProjectPath>,
|
||||
@@ -971,7 +967,6 @@ impl Render for MessageEditor {
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(Self::chat_with_follow))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::paste_raw))
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.flex_1()
|
||||
.child({
|
||||
|
||||
@@ -186,17 +186,6 @@ 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()
|
||||
@@ -211,6 +200,17 @@ 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::{AgentModelIcon, AgentModelInfo, AgentModelList, AgentModelSelector};
|
||||
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
|
||||
use agent_client_protocol::ModelId;
|
||||
use agent_servers::AgentServer;
|
||||
use agent_settings::AgentSettings;
|
||||
@@ -221,7 +221,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let favorites = if self.selector.supports_favorites() {
|
||||
AgentSettings::get_global(cx).favorite_model_ids()
|
||||
Arc::new(AgentSettings::get_global(cx).favorite_model_ids())
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
@@ -242,7 +242,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.filtered_entries =
|
||||
info_list_to_picker_entries(filtered_models, &favorites);
|
||||
info_list_to_picker_entries(filtered_models, favorites);
|
||||
// Finds the currently selected model in the list
|
||||
let new_index = this
|
||||
.delegate
|
||||
@@ -350,11 +350,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
})
|
||||
.child(
|
||||
ModelSelectorListItem::new(ix, model_info.name.clone())
|
||||
.map(|this| match &model_info.icon {
|
||||
Some(AgentModelIcon::Path(path)) => this.icon_path(path.clone()),
|
||||
Some(AgentModelIcon::Named(icon)) => this.icon(*icon),
|
||||
None => this,
|
||||
})
|
||||
.when_some(model_info.icon, |this, icon| this.icon(icon))
|
||||
.is_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.when(supports_favorites, |this| {
|
||||
@@ -410,7 +406,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
|
||||
fn info_list_to_picker_entries(
|
||||
model_list: AgentModelList,
|
||||
favorites: &HashSet<ModelId>,
|
||||
favorites: Arc<HashSet<ModelId>>,
|
||||
) -> Vec<AcpModelPickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
@@ -576,11 +572,13 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_favorites(models: Vec<&str>) -> HashSet<ModelId> {
|
||||
models
|
||||
.into_iter()
|
||||
.map(|m| ModelId::new(m.to_string()))
|
||||
.collect()
|
||||
fn create_favorites(models: Vec<&str>) -> Arc<HashSet<ModelId>> {
|
||||
Arc::new(
|
||||
models
|
||||
.into_iter()
|
||||
.map(|m| ModelId::new(m.to_string()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_entry_model_ids(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
|
||||
@@ -611,7 +609,7 @@ mod tests {
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/gemini"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
@@ -627,7 +625,7 @@ mod tests {
|
||||
let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
|
||||
let favorites = create_favorites(vec![]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
@@ -643,7 +641,7 @@ mod tests {
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/claude"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
|
||||
for entry in &entries {
|
||||
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
|
||||
@@ -664,7 +662,7 @@ mod tests {
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
let model_ids = get_entry_model_ids(&entries);
|
||||
|
||||
assert_eq!(model_ids[0], "zed/gemini");
|
||||
@@ -685,7 +683,7 @@ mod tests {
|
||||
|
||||
let favorites = create_favorites(vec!["zed/claude"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
let labels = get_entry_labels(&entries);
|
||||
|
||||
assert_eq!(
|
||||
@@ -725,7 +723,7 @@ mod tests {
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/gemini"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelSelector};
|
||||
use acp_thread::{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.clone());
|
||||
let model_icon = model.as_ref().and_then(|model| model.icon);
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
@@ -125,14 +125,7 @@ impl Render for AcpModelSelectorPopover {
|
||||
ButtonLike::new("active-model")
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.when_some(model_icon, |this, icon| {
|
||||
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),
|
||||
)
|
||||
this.child(Icon::new(icon).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, Utc};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
@@ -402,22 +402,7 @@ impl AcpThreadHistory {
|
||||
let selected = ix == self.selected_index;
|
||||
let hovered = Some(ix) == self.hovered_index;
|
||||
let timestamp = entry.updated_at().timestamp();
|
||||
|
||||
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);
|
||||
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -438,14 +423,11 @@ impl AcpThreadHistory {
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(display_text)
|
||||
Label::new(thread_timestamp)
|
||||
.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);
|
||||
|
||||
@@ -34,7 +34,7 @@ use language::Buffer;
|
||||
|
||||
use language_model::LanguageModelRegistry;
|
||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||
use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};
|
||||
use project::{Project, ProjectEntryId};
|
||||
use prompt_store::{PromptId, PromptStore};
|
||||
use rope::Point;
|
||||
use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
|
||||
@@ -260,7 +260,6 @@ impl ThreadFeedbackState {
|
||||
|
||||
pub struct AcpThreadView {
|
||||
agent: Rc<dyn AgentServer>,
|
||||
agent_server_store: Entity<AgentServerStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
thread_state: ThreadState,
|
||||
@@ -338,13 +337,7 @@ impl AcpThreadView {
|
||||
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
|
||||
let available_commands = Rc::new(RefCell::new(vec![]));
|
||||
|
||||
let agent_server_store = project.read(cx).agent_server_store().clone();
|
||||
let agent_display_name = agent_server_store
|
||||
.read(cx)
|
||||
.agent_display_name(&ExternalAgentServerName(agent.name()))
|
||||
.unwrap_or_else(|| agent.name());
|
||||
|
||||
let placeholder = placeholder_text(agent_display_name.as_ref(), false);
|
||||
let placeholder = placeholder_text(agent.name().as_ref(), false);
|
||||
|
||||
let message_editor = cx.new(|cx| {
|
||||
let mut editor = MessageEditor::new(
|
||||
@@ -383,6 +376,7 @@ impl AcpThreadView {
|
||||
)
|
||||
});
|
||||
|
||||
let agent_server_store = project.read(cx).agent_server_store().clone();
|
||||
let subscriptions = [
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
|
||||
cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),
|
||||
@@ -412,7 +406,6 @@ impl AcpThreadView {
|
||||
|
||||
Self {
|
||||
agent: agent.clone(),
|
||||
agent_server_store,
|
||||
workspace: workspace.clone(),
|
||||
project: project.clone(),
|
||||
entry_view_state,
|
||||
@@ -744,7 +737,7 @@ impl AcpThreadView {
|
||||
cx: &mut App,
|
||||
) {
|
||||
let agent_name = agent.name();
|
||||
let (configuration_view, subscription) = if let Some(provider_id) = &err.provider_id {
|
||||
let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id {
|
||||
let registry = LanguageModelRegistry::global(cx);
|
||||
|
||||
let sub = window.subscribe(®istry, cx, {
|
||||
@@ -786,6 +779,7 @@ impl AcpThreadView {
|
||||
configuration_view,
|
||||
description: err
|
||||
.description
|
||||
.clone()
|
||||
.map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
|
||||
_subscription: subscription,
|
||||
};
|
||||
@@ -1094,7 +1088,10 @@ impl AcpThreadView {
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired::new(),
|
||||
AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
},
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
@@ -1503,13 +1500,7 @@ impl AcpThreadView {
|
||||
let has_commands = !available_commands.is_empty();
|
||||
self.available_commands.replace(available_commands);
|
||||
|
||||
let agent_display_name = self
|
||||
.agent_server_store
|
||||
.read(cx)
|
||||
.agent_display_name(&ExternalAgentServerName(self.agent.name()))
|
||||
.unwrap_or_else(|| self.agent.name());
|
||||
|
||||
let new_placeholder = placeholder_text(agent_display_name.as_ref(), has_commands);
|
||||
let new_placeholder = placeholder_text(self.agent.name().as_ref(), has_commands);
|
||||
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text(&new_placeholder, window, cx);
|
||||
@@ -1672,6 +1663,44 @@ impl AcpThreadView {
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if method.0.as_ref() == "anthropic-api-key" {
|
||||
let registry = LanguageModelRegistry::global(cx);
|
||||
let provider = registry
|
||||
.read(cx)
|
||||
.provider(&language_model::ANTHROPIC_PROVIDER_ID)
|
||||
.unwrap();
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
let connection = connection.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
if !provider.is_authenticated(cx) {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired {
|
||||
description: Some("ANTHROPIC_API_KEY must be set".to_owned()),
|
||||
provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
|
||||
},
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
this.update(cx, |this, cx| {
|
||||
this.thread_state = Self::initial_state(
|
||||
agent,
|
||||
None,
|
||||
this.workspace.clone(),
|
||||
this.project.clone(),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else if method.0.as_ref() == "vertex-ai"
|
||||
&& std::env::var("GOOGLE_API_KEY").is_err()
|
||||
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
|
||||
@@ -2124,7 +2153,6 @@ impl AcpThreadView {
|
||||
chunks,
|
||||
indented: _,
|
||||
}) => {
|
||||
let mut is_blank = true;
|
||||
let is_last = entry_ix + 1 == total_entries;
|
||||
|
||||
let style = default_markdown_style(false, false, window, cx);
|
||||
@@ -2134,55 +2162,36 @@ impl AcpThreadView {
|
||||
.children(chunks.iter().enumerate().filter_map(
|
||||
|(chunk_ix, chunk)| match chunk {
|
||||
AssistantMessageChunk::Message { block } => {
|
||||
block.markdown().and_then(|md| {
|
||||
let this_is_blank = md.read(cx).source().trim().is_empty();
|
||||
is_blank = is_blank && this_is_blank;
|
||||
if this_is_blank {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
self.render_markdown(md.clone(), style.clone())
|
||||
.into_any_element(),
|
||||
)
|
||||
block.markdown().map(|md| {
|
||||
self.render_markdown(md.clone(), style.clone())
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
AssistantMessageChunk::Thought { block } => {
|
||||
block.markdown().and_then(|md| {
|
||||
let this_is_blank = md.read(cx).source().trim().is_empty();
|
||||
is_blank = is_blank && this_is_blank;
|
||||
if this_is_blank {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
self.render_thinking_block(
|
||||
entry_ix,
|
||||
chunk_ix,
|
||||
md.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element(),
|
||||
block.markdown().map(|md| {
|
||||
self.render_thinking_block(
|
||||
entry_ix,
|
||||
chunk_ix,
|
||||
md.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
},
|
||||
))
|
||||
.into_any();
|
||||
|
||||
if is_blank {
|
||||
Empty.into_any()
|
||||
} else {
|
||||
v_flex()
|
||||
.px_5()
|
||||
.py_1p5()
|
||||
.when(is_last, |this| this.pb_4())
|
||||
.w_full()
|
||||
.text_ui(cx)
|
||||
.child(message_body)
|
||||
.into_any()
|
||||
}
|
||||
v_flex()
|
||||
.px_5()
|
||||
.py_1p5()
|
||||
.when(is_first_indented, |this| this.pt_0p5())
|
||||
.when(is_last, |this| this.pb_4())
|
||||
.w_full()
|
||||
.text_ui(cx)
|
||||
.child(message_body)
|
||||
.into_any()
|
||||
}
|
||||
AgentThreadEntry::ToolCall(tool_call) => {
|
||||
let has_terminals = tool_call.terminals().next().is_some();
|
||||
@@ -2214,7 +2223,7 @@ impl AcpThreadView {
|
||||
div()
|
||||
.relative()
|
||||
.w_full()
|
||||
.pl_5()
|
||||
.pl(rems_from_px(20.0))
|
||||
.bg(cx.theme().colors().panel_background.opacity(0.2))
|
||||
.child(
|
||||
div()
|
||||
@@ -2431,12 +2440,6 @@ impl AcpThreadView {
|
||||
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
|
||||
|
||||
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
|
||||
let input_output_header = |label: SharedString| {
|
||||
Label::new(label)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx)
|
||||
};
|
||||
|
||||
let tool_output_display =
|
||||
if is_open {
|
||||
@@ -2478,25 +2481,7 @@ impl AcpThreadView {
|
||||
| ToolCallStatus::Completed
|
||||
| ToolCallStatus::Failed
|
||||
| ToolCallStatus::Canceled => v_flex()
|
||||
.when(!is_edit && !is_terminal_tool, |this| {
|
||||
this.mt_1p5().w_full().child(
|
||||
v_flex()
|
||||
.ml(rems(0.4))
|
||||
.px_3p5()
|
||||
.pb_1()
|
||||
.gap_1()
|
||||
.border_l_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(input_output_header("Raw Input:".into()))
|
||||
.children(tool_call.raw_input_markdown.clone().map(|input| {
|
||||
self.render_markdown(
|
||||
input,
|
||||
default_markdown_style(false, false, window, cx),
|
||||
)
|
||||
}))
|
||||
.child(input_output_header("Output:".into())),
|
||||
)
|
||||
})
|
||||
.w_full()
|
||||
.children(tool_call.content.iter().enumerate().map(
|
||||
|(content_ix, content)| {
|
||||
div().child(self.render_tool_call_content(
|
||||
@@ -2595,7 +2580,7 @@ impl AcpThreadView {
|
||||
.gap_px()
|
||||
.when(is_collapsible, |this| {
|
||||
this.child(
|
||||
Disclosure::new(("expand-output", entry_ix), is_open)
|
||||
Disclosure::new(("expand", entry_ix), is_open)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.visible_on_hover(&card_header_id)
|
||||
@@ -2718,7 +2703,7 @@ impl AcpThreadView {
|
||||
..default_markdown_style(false, true, window, cx)
|
||||
},
|
||||
))
|
||||
.tooltip(Tooltip::text("Go to File"))
|
||||
.tooltip(Tooltip::text("Jump to File"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.open_tool_call_location(entry_ix, 0, window, cx);
|
||||
}))
|
||||
@@ -2781,20 +2766,20 @@ impl AcpThreadView {
|
||||
let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
|
||||
|
||||
v_flex()
|
||||
.mt_1p5()
|
||||
.gap_2()
|
||||
.map(|this| {
|
||||
if card_layout {
|
||||
this.when(context_ix > 0, |this| {
|
||||
this.pt_2()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
})
|
||||
} else {
|
||||
this.ml(rems(0.4))
|
||||
.px_3p5()
|
||||
.border_l_1()
|
||||
.when(!card_layout, |this| {
|
||||
this.ml(rems(0.4))
|
||||
.px_3p5()
|
||||
.border_l_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
})
|
||||
.when(card_layout, |this| {
|
||||
this.px_2().pb_2().when(context_ix > 0, |this| {
|
||||
this.border_t_1()
|
||||
.pt_2()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
}
|
||||
})
|
||||
})
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
@@ -3515,119 +3500,138 @@ impl AcpThreadView {
|
||||
pending_auth_method: Option<&acp::AuthMethodId>,
|
||||
window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
) -> Div {
|
||||
let show_description =
|
||||
configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
|
||||
|
||||
let auth_methods = connection.auth_methods();
|
||||
|
||||
let agent_display_name = self
|
||||
.agent_server_store
|
||||
.read(cx)
|
||||
.agent_display_name(&ExternalAgentServerName(self.agent.name()))
|
||||
.unwrap_or_else(|| self.agent.name());
|
||||
|
||||
let show_fallback_description = auth_methods.len() > 1
|
||||
&& configuration_view.is_none()
|
||||
&& description.is_none()
|
||||
&& pending_auth_method.is_none();
|
||||
|
||||
let auth_buttons = || {
|
||||
h_flex().justify_end().flex_wrap().gap_1().children(
|
||||
connection
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.map(|(ix, method)| {
|
||||
let (method_id, name) = if self.project.read(cx).is_via_remote_server()
|
||||
&& method.id.0.as_ref() == "oauth-personal"
|
||||
&& method.name == "Log in with Google"
|
||||
{
|
||||
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
|
||||
} else {
|
||||
(method.id.0.clone(), method.name.clone())
|
||||
};
|
||||
|
||||
let agent_telemetry_id = connection.telemetry_id();
|
||||
|
||||
Button::new(method_id.clone(), name)
|
||||
.label_size(LabelSize::Small)
|
||||
.map(|this| {
|
||||
if ix == 0 {
|
||||
this.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
} else {
|
||||
this.style(ButtonStyle::Outlined)
|
||||
}
|
||||
})
|
||||
.when_some(method.description.clone(), |this, description| {
|
||||
this.tooltip(Tooltip::text(description))
|
||||
})
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
agent = agent_telemetry_id,
|
||||
method = method_id
|
||||
);
|
||||
|
||||
this.authenticate(
|
||||
acp::AuthMethodId::new(method_id.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
if pending_auth_method.is_some() {
|
||||
return Callout::new()
|
||||
.icon(IconName::Info)
|
||||
.title(format!("Authenticating to {}…", agent_display_name))
|
||||
.actions_slot(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(2)
|
||||
.into_any_element(),
|
||||
v_flex().flex_1().size_full().justify_end().child(
|
||||
v_flex()
|
||||
.p_2()
|
||||
.pr_3()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().status().warning.opacity(0.04))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.color(Color::Warning)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(Label::new("Authentication Required").size(LabelSize::Small)),
|
||||
)
|
||||
.into_any_element();
|
||||
}
|
||||
.children(description.map(|desc| {
|
||||
div().text_ui(cx).child(self.render_markdown(
|
||||
desc.clone(),
|
||||
default_markdown_style(false, false, window, cx),
|
||||
))
|
||||
}))
|
||||
.children(
|
||||
configuration_view
|
||||
.cloned()
|
||||
.map(|view| div().w_full().child(view)),
|
||||
)
|
||||
.when(show_description, |el| {
|
||||
el.child(
|
||||
Label::new(format!(
|
||||
"You are not currently authenticated with {}.{}",
|
||||
self.agent.name(),
|
||||
if auth_methods.len() > 1 {
|
||||
" Please choose one of the following options:"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mb_1()
|
||||
.ml_5(),
|
||||
)
|
||||
})
|
||||
.when_some(pending_auth_method, |el, _| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.py_4()
|
||||
.w_full()
|
||||
.justify_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(2),
|
||||
)
|
||||
.child(Label::new("Authenticating…").size(LabelSize::Small)),
|
||||
)
|
||||
})
|
||||
.when(!auth_methods.is_empty(), |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.flex_wrap()
|
||||
.gap_1()
|
||||
.when(!show_description, |this| {
|
||||
this.border_t_1()
|
||||
.mt_1()
|
||||
.pt_2()
|
||||
.border_color(cx.theme().colors().border.opacity(0.8))
|
||||
})
|
||||
.children(connection.auth_methods().iter().enumerate().rev().map(
|
||||
|(ix, method)| {
|
||||
let (method_id, name) = if self
|
||||
.project
|
||||
.read(cx)
|
||||
.is_via_remote_server()
|
||||
&& method.id.0.as_ref() == "oauth-personal"
|
||||
&& method.name == "Log in with Google"
|
||||
{
|
||||
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
|
||||
} else {
|
||||
(method.id.0.clone(), method.name.clone())
|
||||
};
|
||||
|
||||
Callout::new()
|
||||
.icon(IconName::Info)
|
||||
.title(format!("Authenticate to {}", agent_display_name))
|
||||
.when(auth_methods.len() == 1, |this| {
|
||||
this.actions_slot(auth_buttons())
|
||||
})
|
||||
.description_slot(
|
||||
v_flex()
|
||||
.text_ui(cx)
|
||||
.map(|this| {
|
||||
if show_fallback_description {
|
||||
this.child(
|
||||
Label::new("Choose one of the following authentication options:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else {
|
||||
this.children(
|
||||
configuration_view
|
||||
.cloned()
|
||||
.map(|view| div().w_full().child(view)),
|
||||
)
|
||||
.children(description.map(|desc| {
|
||||
self.render_markdown(
|
||||
desc.clone(),
|
||||
default_markdown_style(false, false, window, cx),
|
||||
)
|
||||
}))
|
||||
}
|
||||
})
|
||||
.when(auth_methods.len() > 1, |this| {
|
||||
this.gap_1().child(auth_buttons())
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
let agent_telemetry_id = connection.telemetry_id();
|
||||
|
||||
Button::new(method_id.clone(), name)
|
||||
.label_size(LabelSize::Small)
|
||||
.map(|this| {
|
||||
if ix == 0 {
|
||||
this.style(ButtonStyle::Tinted(TintColor::Warning))
|
||||
} else {
|
||||
this.style(ButtonStyle::Outlined)
|
||||
}
|
||||
})
|
||||
.when_some(
|
||||
method.description.clone(),
|
||||
|this, description| {
|
||||
this.tooltip(Tooltip::text(description))
|
||||
},
|
||||
)
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
agent = agent_telemetry_id,
|
||||
method = method_id
|
||||
);
|
||||
|
||||
this.authenticate(
|
||||
acp::AuthMethodId::new(method_id.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
},
|
||||
)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_load_error(
|
||||
@@ -5876,6 +5880,10 @@ impl AcpThreadView {
|
||||
};
|
||||
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
let err = AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
};
|
||||
this.clear_thread_error(cx);
|
||||
if let Some(message) = this.in_flight_prompt.take() {
|
||||
this.message_editor.update(cx, |editor, cx| {
|
||||
@@ -5884,14 +5892,7 @@ impl AcpThreadView {
|
||||
}
|
||||
let this = cx.weak_entity();
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired::new(),
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
||||
})
|
||||
}
|
||||
}))
|
||||
@@ -5904,10 +5905,14 @@ impl AcpThreadView {
|
||||
};
|
||||
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
let err = AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
};
|
||||
self.clear_thread_error(cx);
|
||||
let this = cx.weak_entity();
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(this, AuthRequired::new(), agent, connection, window, cx);
|
||||
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6010,19 +6015,16 @@ impl Render for AcpThreadView {
|
||||
configuration_view,
|
||||
pending_auth_method,
|
||||
..
|
||||
} => v_flex()
|
||||
.flex_1()
|
||||
.size_full()
|
||||
.justify_end()
|
||||
.child(self.render_auth_required_state(
|
||||
} => self
|
||||
.render_auth_required_state(
|
||||
connection,
|
||||
description.as_ref(),
|
||||
configuration_view.as_ref(),
|
||||
pending_auth_method.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
.into_any(),
|
||||
ThreadState::Loading { .. } => v_flex()
|
||||
.flex_1()
|
||||
.child(self.render_recent_history(cx))
|
||||
|
||||
@@ -22,8 +22,7 @@ use gpui::{
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
IconOrSvg, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_models::AllLanguageModelSettings;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
@@ -118,7 +117,7 @@ impl AgentConfiguration {
|
||||
}
|
||||
|
||||
fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
for provider in providers {
|
||||
self.add_provider_configuration_view(&provider, window, cx);
|
||||
}
|
||||
@@ -262,12 +261,9 @@ impl AgentConfiguration {
|
||||
.w_full()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
match provider.icon() {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(name) => Icon::new(name),
|
||||
}
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
Icon::new(provider.icon())
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -420,7 +416,7 @@ impl AgentConfiguration {
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
|
||||
let popover_menu = PopoverMenu::new("add-provider-popover")
|
||||
.trigger(
|
||||
|
||||
@@ -4,7 +4,6 @@ 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;
|
||||
@@ -104,14 +103,7 @@ impl Render for AgentModelSelector {
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.when_some(provider_icon, |this, icon| {
|
||||
this.child(
|
||||
match icon {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(name) => Icon::new(name),
|
||||
}
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
|
||||
})
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.child(
|
||||
@@ -123,7 +115,7 @@ impl Render for AgentModelSelector {
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
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)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.iter()
|
||||
.any(|provider| {
|
||||
provider.is_authenticated(cx)
|
||||
|
||||
@@ -348,8 +348,7 @@ 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::ProvidersChanged => {
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
update_active_language_model_from_settings(cx);
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -1586,7 +1586,7 @@ pub(crate) fn search_rules(
|
||||
None
|
||||
} else {
|
||||
Some(RulesContextEntry {
|
||||
prompt_id: metadata.id.as_user()?,
|
||||
prompt_id: metadata.id.user_id()?,
|
||||
title: metadata.title?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ use gpui::{
|
||||
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
|
||||
};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, IconOrSvg, LanguageModel, LanguageModelId,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
|
||||
AuthenticateError, ConfiguredModel, 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.visible_providers();
|
||||
let providers = lm_registry.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: IconOrSvg,
|
||||
icon: IconName,
|
||||
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)
|
||||
.visible_providers()
|
||||
.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)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.into_iter()
|
||||
.filter(|provider| provider.is_authenticated(cx))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -566,10 +566,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
|
||||
Some(
|
||||
ModelSelectorListItem::new(ix, model_info.model.name().0)
|
||||
.map(|this| match &model_info.icon {
|
||||
IconOrSvg::Icon(icon_name) => this.icon(*icon_name),
|
||||
IconOrSvg::Svg(icon_path) => this.icon_path(icon_path.clone()),
|
||||
})
|
||||
.icon(model_info.icon)
|
||||
.is_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.is_favorite(is_favorite)
|
||||
@@ -705,7 +702,7 @@ mod tests {
|
||||
.any(|(fav_provider, fav_name)| *fav_provider == provider && *fav_name == name);
|
||||
ModelInfo {
|
||||
model: Arc::new(TestLanguageModel::new(name, provider)),
|
||||
icon: IconOrSvg::Icon(IconName::Ai),
|
||||
icon: IconName::Ai,
|
||||
is_favorite,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -191,9 +191,6 @@ 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()
|
||||
@@ -206,6 +203,9 @@ 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,8 +33,7 @@ use language::{
|
||||
language_settings::{SoftWrap, all_language_settings},
|
||||
};
|
||||
use language_model::{
|
||||
ConfigurationError, IconOrSvg, LanguageModelExt, LanguageModelImage, LanguageModelRegistry,
|
||||
Role,
|
||||
ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelRegistry, Role,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::{Picker, popover_menu::PickerPopoverMenu};
|
||||
@@ -72,7 +71,7 @@ use workspace::{
|
||||
pane,
|
||||
searchable::{SearchEvent, SearchableItem},
|
||||
};
|
||||
use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
|
||||
use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
|
||||
|
||||
use crate::CycleFavoriteModels;
|
||||
|
||||
@@ -1699,9 +1698,6 @@ impl TextThreadEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let editor_clipboard_selections = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.entries().first().cloned())
|
||||
@@ -1712,101 +1708,84 @@ impl TextThreadEditor {
|
||||
_ => None,
|
||||
});
|
||||
|
||||
// Insert creases for pasted clipboard selections that:
|
||||
// 1. Contain exactly one selection
|
||||
// 2. Have an associated file path
|
||||
// 3. Span multiple lines (not single-line selections)
|
||||
// 4. Belong to a file that exists in the current project
|
||||
let should_insert_creases = util::maybe!({
|
||||
let selections = editor_clipboard_selections.as_ref()?;
|
||||
if selections.len() > 1 {
|
||||
return Some(false);
|
||||
}
|
||||
let selection = selections.first()?;
|
||||
let file_path = selection.file_path.as_ref()?;
|
||||
let line_range = selection.line_range.as_ref()?;
|
||||
let has_file_context = editor_clipboard_selections
|
||||
.as_ref()
|
||||
.is_some_and(|selections| {
|
||||
selections
|
||||
.iter()
|
||||
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
|
||||
});
|
||||
|
||||
if line_range.start() == line_range.end() {
|
||||
return Some(false);
|
||||
}
|
||||
if has_file_context {
|
||||
if let Some(clipboard_item) = cx.read_from_clipboard() {
|
||||
if let Some(ClipboardEntry::String(clipboard_text)) =
|
||||
clipboard_item.entries().first()
|
||||
{
|
||||
if let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
|
||||
Some(
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some(),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let text = clipboard_text.text();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let mut current_offset = 0;
|
||||
let weak_editor = cx.entity().downgrade();
|
||||
|
||||
if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
|
||||
if let Some(ClipboardEntry::String(clipboard_text)) = clipboard_item.entries().first() {
|
||||
if let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let selected_text =
|
||||
&text[current_offset..current_offset + selection.len];
|
||||
let fence = assistant_slash_commands::codeblock_fence_for_path(
|
||||
file_path.to_str(),
|
||||
Some(line_range.clone()),
|
||||
);
|
||||
let formatted_text = format!("{fence}{selected_text}\n```");
|
||||
|
||||
let text = clipboard_text.text();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let mut current_offset = 0;
|
||||
let weak_editor = cx.entity().downgrade();
|
||||
let insert_point = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.head();
|
||||
let start_row = MultiBufferRow(insert_point.row);
|
||||
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let selected_text =
|
||||
&text[current_offset..current_offset + selection.len];
|
||||
let fence = assistant_slash_commands::codeblock_fence_for_path(
|
||||
file_path.to_str(),
|
||||
Some(line_range.clone()),
|
||||
);
|
||||
let formatted_text = format!("{fence}{selected_text}\n```");
|
||||
editor.insert(&formatted_text, window, cx);
|
||||
|
||||
let insert_point = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.head();
|
||||
let start_row = MultiBufferRow(insert_point.row);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let anchor_before = snapshot.anchor_after(insert_point);
|
||||
let anchor_after = editor
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.head()
|
||||
.bias_left(&snapshot);
|
||||
|
||||
editor.insert(&formatted_text, window, cx);
|
||||
editor.insert("\n", window, cx);
|
||||
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let anchor_before = snapshot.anchor_after(insert_point);
|
||||
let anchor_after = editor
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.head()
|
||||
.bias_left(&snapshot);
|
||||
let crease_text = acp_thread::selection_name(
|
||||
Some(file_path.as_ref()),
|
||||
&line_range,
|
||||
);
|
||||
|
||||
editor.insert("\n", window, cx);
|
||||
let fold_placeholder = quote_selection_fold_placeholder(
|
||||
crease_text,
|
||||
weak_editor.clone(),
|
||||
);
|
||||
let crease = Crease::inline(
|
||||
anchor_before..anchor_after,
|
||||
fold_placeholder,
|
||||
render_quote_selection_output_toggle,
|
||||
|_, _, _, _| Empty.into_any(),
|
||||
);
|
||||
editor.insert_creases(vec![crease], cx);
|
||||
editor.fold_at(start_row, window, cx);
|
||||
|
||||
let crease_text = acp_thread::selection_name(
|
||||
Some(file_path.as_ref()),
|
||||
&line_range,
|
||||
);
|
||||
|
||||
let fold_placeholder = quote_selection_fold_placeholder(
|
||||
crease_text,
|
||||
weak_editor.clone(),
|
||||
);
|
||||
let crease = Crease::inline(
|
||||
anchor_before..anchor_after,
|
||||
fold_placeholder,
|
||||
render_quote_selection_output_toggle,
|
||||
|_, _, _, _| Empty.into_any(),
|
||||
);
|
||||
editor.insert_creases(vec![crease], cx);
|
||||
editor.fold_at(start_row, window, cx);
|
||||
|
||||
current_offset += selection.len;
|
||||
if !selection.is_entire_line && current_offset < text.len() {
|
||||
current_offset += 1;
|
||||
current_offset += selection.len;
|
||||
if !selection.is_entire_line && current_offset < text.len() {
|
||||
current_offset += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1965,12 +1944,6 @@ impl TextThreadEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.paste(&editor::actions::Paste, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
@@ -2232,10 +2205,10 @@ impl TextThreadEditor {
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
|
||||
let provider_icon = active_provider
|
||||
.as_ref()
|
||||
.map(|p| p.icon())
|
||||
.unwrap_or(IconOrSvg::Icon(IconName::Ai));
|
||||
let provider_icon = match active_provider {
|
||||
Some(provider) => provider.icon(),
|
||||
None => IconName::Ai,
|
||||
};
|
||||
|
||||
let focus_handle = self.editor().focus_handle(cx);
|
||||
|
||||
@@ -2245,13 +2218,6 @@ 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();
|
||||
@@ -2299,7 +2265,7 @@ impl TextThreadEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(provider_icon_element)
|
||||
.child(Icon::new(provider_icon).color(color).size(IconSize::XSmall))
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(color)
|
||||
@@ -2661,7 +2627,6 @@ impl Render for TextThreadEditor {
|
||||
.capture_action(cx.listener(TextThreadEditor::copy))
|
||||
.capture_action(cx.listener(TextThreadEditor::cut))
|
||||
.capture_action(cx.listener(TextThreadEditor::paste))
|
||||
.on_action(cx.listener(TextThreadEditor::paste_raw))
|
||||
.capture_action(cx.listener(TextThreadEditor::cycle_message_role))
|
||||
.capture_action(cx.listener(TextThreadEditor::confirm_command))
|
||||
.on_action(cx.listener(TextThreadEditor::assist))
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
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,
|
||||
@@ -44,7 +39,7 @@ impl RenderOnce for ModelSelectorHeader {
|
||||
pub struct ModelSelectorListItem {
|
||||
index: usize,
|
||||
title: SharedString,
|
||||
icon: Option<ModelIcon>,
|
||||
icon: Option<IconName>,
|
||||
is_selected: bool,
|
||||
is_focused: bool,
|
||||
is_favorite: bool,
|
||||
@@ -65,12 +60,7 @@ impl ModelSelectorListItem {
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: IconName) -> Self {
|
||||
self.icon = Some(ModelIcon::Name(icon));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon_path(mut self, path: SharedString) -> Self {
|
||||
self.icon = Some(ModelIcon::Path(path));
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -115,12 +105,9 @@ impl RenderOnce for ModelSelectorListItem {
|
||||
.gap_1p5()
|
||||
.when_some(self.icon, |this, icon| {
|
||||
this.child(
|
||||
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),
|
||||
Icon::new(icon)
|
||||
.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, Utc};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
@@ -411,22 +411,7 @@ impl AcpThreadHistory {
|
||||
let selected = ix == self.selected_index;
|
||||
let hovered = Some(ix) == self.hovered_index;
|
||||
let timestamp = entry.updated_at().timestamp();
|
||||
|
||||
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);
|
||||
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -447,14 +432,11 @@ impl AcpThreadHistory {
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(display_text)
|
||||
Label::new(thread_timestamp)
|
||||
.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::{IconOrSvg, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||
use ui::{Divider, List, ListBulletItem, prelude::*};
|
||||
|
||||
pub struct ApiKeysWithProviders {
|
||||
configured_providers: Vec<(IconOrSvg, SharedString)>,
|
||||
configured_providers: Vec<(IconName, SharedString)>,
|
||||
}
|
||||
|
||||
impl ApiKeysWithProviders {
|
||||
@@ -13,8 +13,7 @@ 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::ProvidersChanged => {
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
this.configured_providers = Self::compute_configured_providers(cx)
|
||||
}
|
||||
_ => {}
|
||||
@@ -27,9 +26,9 @@ impl ApiKeysWithProviders {
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_configured_providers(cx: &App) -> Vec<(IconOrSvg, SharedString)> {
|
||||
fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.iter()
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
@@ -48,14 +47,7 @@ impl Render for ApiKeysWithProviders {
|
||||
.map(|(icon, name)| {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.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(Icon::new(icon).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>,
|
||||
has_configured_providers: bool,
|
||||
configured_providers: Vec<(IconName, SharedString)>,
|
||||
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
}
|
||||
|
||||
@@ -27,9 +27,8 @@ 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(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
this.has_configured_providers = Self::has_configured_providers(cx)
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
this.configured_providers = Self::compute_available_providers(cx)
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
@@ -39,16 +38,20 @@ impl AgentPanelOnboarding {
|
||||
Self {
|
||||
user_store,
|
||||
client,
|
||||
has_configured_providers: Self::has_configured_providers(cx),
|
||||
configured_providers: Self::compute_available_providers(cx),
|
||||
continue_with_zed_ai: Arc::new(continue_with_zed_ai),
|
||||
}
|
||||
}
|
||||
|
||||
fn has_configured_providers(cx: &App) -> bool {
|
||||
fn compute_available_providers(cx: &App) -> Vec<(IconName, SharedString)> {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.iter()
|
||||
.any(|provider| provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID)
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
})
|
||||
.map(|provider| (provider.icon(), provider.name().0))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +81,7 @@ impl Render for AgentPanelOnboarding {
|
||||
}),
|
||||
)
|
||||
.map(|this| {
|
||||
if enrolled_in_trial || is_pro_user || self.has_configured_providers {
|
||||
if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() {
|
||||
this
|
||||
} else {
|
||||
this.child(ApiKeysWithoutProviders::new())
|
||||
|
||||
@@ -103,9 +103,8 @@ impl Model {
|
||||
|
||||
pub fn max_output_tokens(&self) -> Option<u64> {
|
||||
match self {
|
||||
// Their API treats this max against the context window, which means we hit the limit a lot
|
||||
// Using the default value of None in the API instead
|
||||
Self::Chat | Self::Reasoner => None,
|
||||
Self::Chat => Some(8_192),
|
||||
Self::Reasoner => Some(64_000),
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
} => *max_output_tokens,
|
||||
|
||||
@@ -7,6 +7,8 @@ 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"
|
||||
@@ -15,6 +17,7 @@ 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
|
||||
@@ -24,4 +27,4 @@ workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "docs_preprocessor"
|
||||
path = "src/main.rs"
|
||||
path = "src/main.rs"
|
||||
|
||||
@@ -22,13 +22,16 @@ 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(load_all_actions);
|
||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_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) {
|
||||
@@ -69,8 +72,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.as_str() {
|
||||
for alias in action.deprecated_aliases {
|
||||
if alias == &action_name {
|
||||
return PreprocessorError::DeprecatedActionUsed {
|
||||
used: action_name,
|
||||
should_be: action.name.to_string(),
|
||||
@@ -211,7 +214,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 is_missing_action(action) {
|
||||
if find_action_by_name(action).is_none() {
|
||||
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||
action.to_string(),
|
||||
));
|
||||
@@ -241,12 +244,10 @@ 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 {
|
||||
if actions_available() {
|
||||
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||
name.to_string(),
|
||||
));
|
||||
}
|
||||
return format!("<code class=\"hljs\">{}</code>", name);
|
||||
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||
name.to_string(),
|
||||
));
|
||||
return String::new();
|
||||
};
|
||||
format!("<code class=\"hljs\">{}</code>", &action.human_name)
|
||||
})
|
||||
@@ -256,19 +257,11 @@ 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.as_str().cmp(name))
|
||||
.binary_search_by(|action| action.name.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,
|
||||
@@ -391,13 +384,18 @@ 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() {
|
||||
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")?;
|
||||
if let Some((action_name, _)) = settings::KeymapFile::parse_action(action)
|
||||
.map_err(|err| anyhow::format_err!(err))
|
||||
.context("Failed to parse action")?
|
||||
{
|
||||
anyhow::ensure!(
|
||||
!is_missing_action(action_name),
|
||||
find_action_by_name(action_name).is_some(),
|
||||
"Action not found: {}",
|
||||
action_name
|
||||
);
|
||||
@@ -493,35 +491,27 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct ActionDef {
|
||||
name: String,
|
||||
name: &'static str,
|
||||
human_name: String,
|
||||
deprecated_aliases: Vec<String>,
|
||||
#[serde(rename = "documentation")]
|
||||
docs: Option<String>,
|
||||
deprecated_aliases: &'static [&'static str],
|
||||
docs: Option<&'static str>,
|
||||
}
|
||||
|
||||
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 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 handle_postprocessing() -> Result<()> {
|
||||
@@ -657,7 +647,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.as_str());
|
||||
actions_sorted.sort_by_key(|a| a.name);
|
||||
|
||||
// Start the definition list with custom styling for better spacing
|
||||
output.push_str("<dl style=\"line-height: 1.8;\">\n");
|
||||
@@ -674,7 +664,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.as_ref() {
|
||||
if let Some(description) = action.docs {
|
||||
output.push_str(
|
||||
&description
|
||||
.replace("&", "&")
|
||||
@@ -684,7 +674,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): ");
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
use anyhow::{Context as _, Result};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{
|
||||
App, AppContext as _, Entity, Global, SharedString, Task,
|
||||
App, AppContext as _, Entity, SharedString, Task,
|
||||
http_client::{self, AsyncBody, Method},
|
||||
};
|
||||
use language::{OffsetRangeExt as _, ToOffset, ToPoint as _};
|
||||
@@ -300,19 +300,14 @@ pub const MERCURY_CREDENTIALS_URL: SharedString =
|
||||
SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions");
|
||||
pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token";
|
||||
pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("MERCURY_AI_TOKEN");
|
||||
|
||||
struct GlobalMercuryApiKey(Entity<ApiKeyState>);
|
||||
|
||||
impl Global for GlobalMercuryApiKey {}
|
||||
pub static MERCURY_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn mercury_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
if let Some(global) = cx.try_global::<GlobalMercuryApiKey>() {
|
||||
return global.0.clone();
|
||||
}
|
||||
let entity =
|
||||
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()));
|
||||
cx.set_global(GlobalMercuryApiKey(entity.clone()));
|
||||
entity
|
||||
MERCURY_API_KEY
|
||||
.get_or_init(|| {
|
||||
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()))
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn load_mercury_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{
|
||||
App, AppContext as _, Entity, Global, SharedString, Task,
|
||||
App, AppContext as _, Entity, SharedString, Task,
|
||||
http_client::{self, AsyncBody, Method},
|
||||
};
|
||||
use language::{Point, ToOffset as _};
|
||||
@@ -272,19 +272,14 @@ pub const SWEEP_CREDENTIALS_URL: SharedString =
|
||||
SharedString::new_static("https://autocomplete.sweep.dev");
|
||||
pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token";
|
||||
pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("SWEEP_AI_TOKEN");
|
||||
|
||||
struct GlobalSweepApiKey(Entity<ApiKeyState>);
|
||||
|
||||
impl Global for GlobalSweepApiKey {}
|
||||
pub static SWEEP_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn sweep_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
if let Some(global) = cx.try_global::<GlobalSweepApiKey>() {
|
||||
return global.0.clone();
|
||||
}
|
||||
let entity =
|
||||
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()));
|
||||
cx.set_global(GlobalSweepApiKey(entity.clone()));
|
||||
entity
|
||||
SWEEP_API_KEY
|
||||
.get_or_init(|| {
|
||||
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()))
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn load_sweep_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {
|
||||
|
||||
@@ -348,61 +348,6 @@ where
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_bracket_colorization_after_language_swap(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.colorize_brackets = Some(true);
|
||||
});
|
||||
|
||||
let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
|
||||
language_registry.add(markdown_lang());
|
||||
language_registry.add(rust_lang());
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
buffer.set_language_registry(language_registry.clone());
|
||||
buffer.set_language(Some(markdown_lang()), cx);
|
||||
});
|
||||
|
||||
cx.set_state(indoc! {r#"
|
||||
fn main() {
|
||||
let v: Vec<Stringˇ> = vec![];
|
||||
}
|
||||
"#});
|
||||
cx.executor().advance_clock(Duration::from_millis(100));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
r#"fn main«1()1» «1{
|
||||
let v: Vec<String> = vec!«2[]2»;
|
||||
}1»
|
||||
|
||||
1 hsla(207.80, 16.20%, 69.19%, 1.00)
|
||||
2 hsla(29.00, 54.00%, 65.88%, 1.00)
|
||||
"#,
|
||||
&bracket_colors_markup(&mut cx),
|
||||
"Markdown does not colorize <> brackets"
|
||||
);
|
||||
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
buffer.set_language(Some(rust_lang()), cx);
|
||||
});
|
||||
cx.executor().advance_clock(Duration::from_millis(100));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
r#"fn main«1()1» «1{
|
||||
let v: Vec«2<String>2» = vec!«2[]2»;
|
||||
}1»
|
||||
|
||||
1 hsla(207.80, 16.20%, 69.19%, 1.00)
|
||||
2 hsla(29.00, 54.00%, 65.88%, 1.00)
|
||||
"#,
|
||||
&bracket_colors_markup(&mut cx),
|
||||
"After switching to Rust, <> brackets are now colorized"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
|
||||
@@ -51,8 +51,6 @@ pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
|
||||
pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
|
||||
pub const COMPLETION_MENU_MIN_WIDTH: Pixels = px(280.);
|
||||
pub const COMPLETION_MENU_MAX_WIDTH: Pixels = px(540.);
|
||||
pub const CODE_ACTION_MENU_MIN_WIDTH: Pixels = px(220.);
|
||||
pub const CODE_ACTION_MENU_MAX_WIDTH: Pixels = px(540.);
|
||||
|
||||
// Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
|
||||
// documentation not yet being parsed.
|
||||
@@ -181,7 +179,7 @@ impl CodeContextMenu {
|
||||
) -> Option<AnyElement> {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => menu.render_aside(max_size, window, cx),
|
||||
CodeContextMenu::CodeActions(menu) => menu.render_aside(max_size, window, cx),
|
||||
CodeContextMenu::CodeActions(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -893,7 +891,7 @@ impl CompletionsMenu {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(text.trim().to_string())
|
||||
Label::new(text.clone())
|
||||
.ml_4()
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
@@ -1421,6 +1419,26 @@ pub enum CodeActionsItem {
|
||||
}
|
||||
|
||||
impl CodeActionsItem {
|
||||
fn as_task(&self) -> Option<&ResolvedTask> {
|
||||
let Self::Task(_, task) = self else {
|
||||
return None;
|
||||
};
|
||||
Some(task)
|
||||
}
|
||||
|
||||
fn as_code_action(&self) -> Option<&CodeAction> {
|
||||
let Self::CodeAction { action, .. } = self else {
|
||||
return None;
|
||||
};
|
||||
Some(action)
|
||||
}
|
||||
fn as_debug_scenario(&self) -> Option<&DebugScenario> {
|
||||
let Self::DebugScenario(scenario) = self else {
|
||||
return None;
|
||||
};
|
||||
Some(scenario)
|
||||
}
|
||||
|
||||
pub fn label(&self) -> String {
|
||||
match self {
|
||||
Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
|
||||
@@ -1428,14 +1446,6 @@ impl CodeActionsItem {
|
||||
Self::DebugScenario(scenario) => scenario.label.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn menu_label(&self) -> String {
|
||||
match self {
|
||||
Self::CodeAction { action, .. } => action.lsp_action.title().replace("\n", ""),
|
||||
Self::Task(_, task) => task.resolved_label.replace("\n", ""),
|
||||
Self::DebugScenario(scenario) => format!("debug: {}", scenario.label),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CodeActionsMenu {
|
||||
@@ -1545,33 +1555,60 @@ impl CodeActionsMenu {
|
||||
let item_ix = range.start + ix;
|
||||
let selected = item_ix == selected_item;
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
ListItem::new(item_ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.overflow_x()
|
||||
.child(
|
||||
div()
|
||||
.min_w(CODE_ACTION_MENU_MIN_WIDTH)
|
||||
.max_w(CODE_ACTION_MENU_MAX_WIDTH)
|
||||
.overflow_hidden()
|
||||
.text_ellipsis()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.when(selected, |this| this.text_color(colors.text_accent))
|
||||
.child(action.menu_label()),
|
||||
)
|
||||
.on_click(cx.listener(move |editor, _, window, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}))
|
||||
div().min_w(px(220.)).max_w(px(540.)).child(
|
||||
ListItem::new(item_ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.when_some(action.as_code_action(), |this, action| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child(
|
||||
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
|
||||
action.lsp_action.title().replace("\n", ""),
|
||||
)
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(action.as_task(), |this, task| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child(task.resolved_label.replace("\n", ""))
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(action.as_debug_scenario(), |this, scenario| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child("debug: ")
|
||||
.child(scenario.label.clone())
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |editor, _, window, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
})),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
@@ -1598,46 +1635,4 @@ impl CodeActionsMenu {
|
||||
|
||||
Popover::new().child(list).into_any_element()
|
||||
}
|
||||
|
||||
fn render_aside(
|
||||
&mut self,
|
||||
max_size: Size<Pixels>,
|
||||
window: &mut Window,
|
||||
_cx: &mut Context<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
let Some(action) = self.actions.get(self.selected_item) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let label = action.menu_label();
|
||||
let text_system = window.text_system();
|
||||
let mut line_wrapper = text_system.line_wrapper(
|
||||
window.text_style().font(),
|
||||
window.text_style().font_size.to_pixels(window.rem_size()),
|
||||
);
|
||||
let is_truncated = line_wrapper.should_truncate_line(
|
||||
&label,
|
||||
CODE_ACTION_MENU_MAX_WIDTH,
|
||||
"…",
|
||||
gpui::TruncateFrom::End,
|
||||
);
|
||||
|
||||
if is_truncated.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
Popover::new()
|
||||
.child(
|
||||
div()
|
||||
.child(label)
|
||||
.id("code_actions_menu_extended")
|
||||
.px(MENU_ASIDE_X_PADDING / 2.)
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
.occlude(),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,11 @@ pub use multi_buffer::{
|
||||
pub use split::SplittableEditor;
|
||||
pub use text::Bias;
|
||||
|
||||
use ::git::{Restore, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
|
||||
use ::git::{
|
||||
Restore,
|
||||
blame::{BlameEntry, ParsedCommitMessage},
|
||||
status::FileStatus,
|
||||
};
|
||||
use aho_corasick::{AhoCorasick, AhoCorasickBuilder, BuildError};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use blink_manager::BlinkManager;
|
||||
@@ -163,7 +167,6 @@ use project::{
|
||||
project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter, ProjectSettings},
|
||||
};
|
||||
use rand::seq::SliceRandom;
|
||||
use regex::Regex;
|
||||
use rpc::{ErrorCode, ErrorExt, proto::PeerId};
|
||||
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager};
|
||||
use selections_collection::{MutableSelectionsCollection, SelectionsCollection};
|
||||
@@ -4788,146 +4791,82 @@ impl Editor {
|
||||
let end = selection.end;
|
||||
let selection_is_empty = start == end;
|
||||
let language_scope = buffer.language_scope_at(start);
|
||||
let (delimiter, newline_config) = if let Some(language) = &language_scope {
|
||||
let needs_extra_newline = NewlineConfig::insert_extra_newline_brackets(
|
||||
&buffer,
|
||||
start..end,
|
||||
language,
|
||||
)
|
||||
|| NewlineConfig::insert_extra_newline_tree_sitter(
|
||||
&buffer,
|
||||
start..end,
|
||||
);
|
||||
let (comment_delimiter, doc_delimiter, newline_formatting) =
|
||||
if let Some(language) = &language_scope {
|
||||
let mut newline_formatting =
|
||||
NewlineFormatting::new(&buffer, start..end, language);
|
||||
|
||||
let mut newline_config = NewlineConfig::Newline {
|
||||
additional_indent: IndentSize::spaces(0),
|
||||
extra_line_additional_indent: if needs_extra_newline {
|
||||
Some(IndentSize::spaces(0))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
prevent_auto_indent: false,
|
||||
// Comment extension on newline is allowed only for cursor selections
|
||||
let comment_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
return comment_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
);
|
||||
});
|
||||
|
||||
let doc_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
return documentation_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
&mut newline_formatting,
|
||||
);
|
||||
});
|
||||
|
||||
(comment_delimiter, doc_delimiter, newline_formatting)
|
||||
} else {
|
||||
(None, None, NewlineFormatting::default())
|
||||
};
|
||||
|
||||
let comment_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
let prevent_auto_indent = doc_delimiter.is_some();
|
||||
let delimiter = comment_delimiter.or(doc_delimiter);
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline {
|
||||
return None;
|
||||
}
|
||||
let capacity_for_delimiter =
|
||||
delimiter.as_deref().map(str::len).unwrap_or_default();
|
||||
let mut new_text = String::with_capacity(
|
||||
1 + capacity_for_delimiter
|
||||
+ existing_indent.len as usize
|
||||
+ newline_formatting.indent_on_newline.len as usize
|
||||
+ newline_formatting.indent_on_extra_newline.len as usize,
|
||||
);
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(newline_formatting.indent_on_newline.chars());
|
||||
|
||||
return comment_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
);
|
||||
});
|
||||
if let Some(delimiter) = &delimiter {
|
||||
new_text.push_str(delimiter);
|
||||
}
|
||||
|
||||
let doc_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline {
|
||||
return None;
|
||||
}
|
||||
|
||||
return documentation_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
&mut newline_config,
|
||||
);
|
||||
});
|
||||
|
||||
let list_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_list_on_newline {
|
||||
return None;
|
||||
}
|
||||
|
||||
return list_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
&mut newline_config,
|
||||
);
|
||||
});
|
||||
|
||||
(
|
||||
comment_delimiter.or(doc_delimiter).or(list_delimiter),
|
||||
newline_config,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
None,
|
||||
NewlineConfig::Newline {
|
||||
additional_indent: IndentSize::spaces(0),
|
||||
extra_line_additional_indent: None,
|
||||
prevent_auto_indent: false,
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let (edit_start, new_text, prevent_auto_indent) = match &newline_config {
|
||||
NewlineConfig::ClearCurrentLine => {
|
||||
let row_start =
|
||||
buffer.point_to_offset(Point::new(start_point.row, 0));
|
||||
(row_start, String::new(), false)
|
||||
}
|
||||
NewlineConfig::UnindentCurrentLine { continuation } => {
|
||||
let row_start =
|
||||
buffer.point_to_offset(Point::new(start_point.row, 0));
|
||||
let tab_size = buffer.language_settings_at(start, cx).tab_size;
|
||||
let tab_size_indent = IndentSize::spaces(tab_size.get());
|
||||
let reduced_indent =
|
||||
existing_indent.with_delta(Ordering::Less, tab_size_indent);
|
||||
let mut new_text = String::new();
|
||||
new_text.extend(reduced_indent.chars());
|
||||
new_text.push_str(continuation);
|
||||
(row_start, new_text, true)
|
||||
}
|
||||
NewlineConfig::Newline {
|
||||
additional_indent,
|
||||
extra_line_additional_indent,
|
||||
prevent_auto_indent,
|
||||
} => {
|
||||
let capacity_for_delimiter =
|
||||
delimiter.as_deref().map(str::len).unwrap_or_default();
|
||||
let extra_line_len = extra_line_additional_indent
|
||||
.map(|i| 1 + existing_indent.len as usize + i.len as usize)
|
||||
.unwrap_or(0);
|
||||
let mut new_text = String::with_capacity(
|
||||
1 + capacity_for_delimiter
|
||||
+ existing_indent.len as usize
|
||||
+ additional_indent.len as usize
|
||||
+ extra_line_len,
|
||||
);
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(additional_indent.chars());
|
||||
if let Some(delimiter) = &delimiter {
|
||||
new_text.push_str(delimiter);
|
||||
}
|
||||
if let Some(extra_indent) = extra_line_additional_indent {
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(extra_indent.chars());
|
||||
}
|
||||
(start, new_text, *prevent_auto_indent)
|
||||
}
|
||||
};
|
||||
if newline_formatting.insert_extra_newline {
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(newline_formatting.indent_on_extra_newline.chars());
|
||||
}
|
||||
|
||||
let anchor = buffer.anchor_after(end);
|
||||
let new_selection = selection.map(|_| anchor);
|
||||
(
|
||||
((edit_start..end, new_text), prevent_auto_indent),
|
||||
(newline_config.has_extra_line(), new_selection),
|
||||
((start..end, new_text), prevent_auto_indent),
|
||||
(newline_formatting.insert_extra_newline, new_selection),
|
||||
)
|
||||
})
|
||||
.unzip()
|
||||
@@ -10452,22 +10391,6 @@ impl Editor {
|
||||
}
|
||||
prev_edited_row = selection.end.row;
|
||||
|
||||
// If cursor is after a list prefix, make selection non-empty to trigger line indent
|
||||
if selection.is_empty() {
|
||||
let cursor = selection.head();
|
||||
let settings = buffer.language_settings_at(cursor, cx);
|
||||
if settings.indent_list_on_tab {
|
||||
if let Some(language) = snapshot.language_scope_at(Point::new(cursor.row, 0)) {
|
||||
if is_list_prefix_row(MultiBufferRow(cursor.row), &snapshot, &language) {
|
||||
row_delta = Self::indent_selection(
|
||||
buffer, &snapshot, selection, &mut edits, row_delta, cx,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the selection is non-empty, then increase the indentation of the selected lines.
|
||||
if !selection.is_empty() {
|
||||
row_delta =
|
||||
@@ -23436,7 +23359,7 @@ fn documentation_delimiter_for_newline(
|
||||
start_point: &Point,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
language: &LanguageScope,
|
||||
newline_config: &mut NewlineConfig,
|
||||
newline_formatting: &mut NewlineFormatting,
|
||||
) -> Option<Arc<str>> {
|
||||
let BlockCommentConfig {
|
||||
start: start_tag,
|
||||
@@ -23488,9 +23411,6 @@ fn documentation_delimiter_for_newline(
|
||||
}
|
||||
};
|
||||
|
||||
let mut needs_extra_line = false;
|
||||
let mut extra_line_additional_indent = IndentSize::spaces(0);
|
||||
|
||||
let cursor_is_before_end_tag_if_exists = {
|
||||
let mut char_position = 0u32;
|
||||
let mut end_tag_offset = None;
|
||||
@@ -23508,11 +23428,11 @@ fn documentation_delimiter_for_newline(
|
||||
let cursor_is_before_end_tag = column <= end_tag_offset;
|
||||
if cursor_is_after_start_tag {
|
||||
if cursor_is_before_end_tag {
|
||||
needs_extra_line = true;
|
||||
newline_formatting.insert_extra_newline = true;
|
||||
}
|
||||
let cursor_is_at_start_of_end_tag = column == end_tag_offset;
|
||||
if cursor_is_at_start_of_end_tag {
|
||||
extra_line_additional_indent.len = *len;
|
||||
newline_formatting.indent_on_extra_newline.len = *len;
|
||||
}
|
||||
}
|
||||
cursor_is_before_end_tag
|
||||
@@ -23524,240 +23444,39 @@ fn documentation_delimiter_for_newline(
|
||||
if (cursor_is_after_start_tag || cursor_is_after_delimiter)
|
||||
&& cursor_is_before_end_tag_if_exists
|
||||
{
|
||||
let additional_indent = if cursor_is_after_start_tag {
|
||||
IndentSize::spaces(*len)
|
||||
} else {
|
||||
IndentSize::spaces(0)
|
||||
};
|
||||
|
||||
*newline_config = NewlineConfig::Newline {
|
||||
additional_indent,
|
||||
extra_line_additional_indent: if needs_extra_line {
|
||||
Some(extra_line_additional_indent)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
prevent_auto_indent: true,
|
||||
};
|
||||
if cursor_is_after_start_tag {
|
||||
newline_formatting.indent_on_newline.len = *len;
|
||||
}
|
||||
Some(delimiter.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
const ORDERED_LIST_MAX_MARKER_LEN: usize = 16;
|
||||
|
||||
fn list_delimiter_for_newline(
|
||||
start_point: &Point,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
language: &LanguageScope,
|
||||
newline_config: &mut NewlineConfig,
|
||||
) -> Option<Arc<str>> {
|
||||
let (snapshot, range) = buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
|
||||
|
||||
let num_of_whitespaces = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
|
||||
let task_list_entries: Vec<_> = language
|
||||
.task_list()
|
||||
.into_iter()
|
||||
.flat_map(|config| {
|
||||
config
|
||||
.prefixes
|
||||
.iter()
|
||||
.map(|prefix| (prefix.as_ref(), config.continuation.as_ref()))
|
||||
})
|
||||
.collect();
|
||||
let unordered_list_entries: Vec<_> = language
|
||||
.unordered_list()
|
||||
.iter()
|
||||
.map(|marker| (marker.as_ref(), marker.as_ref()))
|
||||
.collect();
|
||||
|
||||
let all_entries: Vec<_> = task_list_entries
|
||||
.into_iter()
|
||||
.chain(unordered_list_entries)
|
||||
.collect();
|
||||
|
||||
if let Some(max_prefix_len) = all_entries.iter().map(|(p, _)| p.len()).max() {
|
||||
let candidate: String = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(max_prefix_len)
|
||||
.collect();
|
||||
|
||||
if let Some((prefix, continuation)) = all_entries
|
||||
.iter()
|
||||
.filter(|(prefix, _)| candidate.starts_with(*prefix))
|
||||
.max_by_key(|(prefix, _)| prefix.len())
|
||||
{
|
||||
let end_of_prefix = num_of_whitespaces + prefix.len();
|
||||
let cursor_is_after_prefix = end_of_prefix <= start_point.column as usize;
|
||||
let has_content_after_marker = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip(end_of_prefix)
|
||||
.any(|c| !c.is_whitespace());
|
||||
|
||||
if has_content_after_marker && cursor_is_after_prefix {
|
||||
return Some((*continuation).into());
|
||||
}
|
||||
|
||||
if start_point.column as usize == end_of_prefix {
|
||||
if num_of_whitespaces == 0 {
|
||||
*newline_config = NewlineConfig::ClearCurrentLine;
|
||||
} else {
|
||||
*newline_config = NewlineConfig::UnindentCurrentLine {
|
||||
continuation: (*continuation).into(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let candidate: String = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(ORDERED_LIST_MAX_MARKER_LEN)
|
||||
.collect();
|
||||
|
||||
for ordered_config in language.ordered_list() {
|
||||
let regex = match Regex::new(&ordered_config.pattern) {
|
||||
Ok(r) => r,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if let Some(captures) = regex.captures(&candidate) {
|
||||
let full_match = captures.get(0)?;
|
||||
let marker_len = full_match.len();
|
||||
let end_of_prefix = num_of_whitespaces + marker_len;
|
||||
let cursor_is_after_prefix = end_of_prefix <= start_point.column as usize;
|
||||
|
||||
let has_content_after_marker = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip(end_of_prefix)
|
||||
.any(|c| !c.is_whitespace());
|
||||
|
||||
if has_content_after_marker && cursor_is_after_prefix {
|
||||
let number: u32 = captures.get(1)?.as_str().parse().ok()?;
|
||||
let continuation = ordered_config
|
||||
.format
|
||||
.replace("{1}", &(number + 1).to_string());
|
||||
return Some(continuation.into());
|
||||
}
|
||||
|
||||
if start_point.column as usize == end_of_prefix {
|
||||
let continuation = ordered_config.format.replace("{1}", "1");
|
||||
if num_of_whitespaces == 0 {
|
||||
*newline_config = NewlineConfig::ClearCurrentLine;
|
||||
} else {
|
||||
*newline_config = NewlineConfig::UnindentCurrentLine {
|
||||
continuation: continuation.into(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
#[derive(Debug, Default)]
|
||||
struct NewlineFormatting {
|
||||
insert_extra_newline: bool,
|
||||
indent_on_newline: IndentSize,
|
||||
indent_on_extra_newline: IndentSize,
|
||||
}
|
||||
|
||||
fn is_list_prefix_row(
|
||||
row: MultiBufferRow,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
language: &LanguageScope,
|
||||
) -> bool {
|
||||
let Some((snapshot, range)) = buffer.buffer_line_for_row(row) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let num_of_whitespaces = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
|
||||
let task_list_prefixes: Vec<_> = language
|
||||
.task_list()
|
||||
.into_iter()
|
||||
.flat_map(|config| {
|
||||
config
|
||||
.prefixes
|
||||
.iter()
|
||||
.map(|p| p.as_ref())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect();
|
||||
let unordered_list_markers: Vec<_> = language
|
||||
.unordered_list()
|
||||
.iter()
|
||||
.map(|marker| marker.as_ref())
|
||||
.collect();
|
||||
let all_prefixes: Vec<_> = task_list_prefixes
|
||||
.into_iter()
|
||||
.chain(unordered_list_markers)
|
||||
.collect();
|
||||
if let Some(max_prefix_len) = all_prefixes.iter().map(|p| p.len()).max() {
|
||||
let candidate: String = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(max_prefix_len)
|
||||
.collect();
|
||||
if all_prefixes
|
||||
.iter()
|
||||
.any(|prefix| candidate.starts_with(*prefix))
|
||||
{
|
||||
return true;
|
||||
impl NewlineFormatting {
|
||||
fn new(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
range: Range<MultiBufferOffset>,
|
||||
language: &LanguageScope,
|
||||
) -> Self {
|
||||
Self {
|
||||
insert_extra_newline: Self::insert_extra_newline_brackets(
|
||||
buffer,
|
||||
range.clone(),
|
||||
language,
|
||||
) || Self::insert_extra_newline_tree_sitter(buffer, range),
|
||||
indent_on_newline: IndentSize::spaces(0),
|
||||
indent_on_extra_newline: IndentSize::spaces(0),
|
||||
}
|
||||
}
|
||||
|
||||
let ordered_list_candidate: String = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip(num_of_whitespaces)
|
||||
.take(ORDERED_LIST_MAX_MARKER_LEN)
|
||||
.collect();
|
||||
for ordered_config in language.ordered_list() {
|
||||
let regex = match Regex::new(&ordered_config.pattern) {
|
||||
Ok(r) => r,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if let Some(captures) = regex.captures(&ordered_list_candidate) {
|
||||
return captures.get(0).is_some();
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum NewlineConfig {
|
||||
/// Insert newline with optional additional indent and optional extra blank line
|
||||
Newline {
|
||||
additional_indent: IndentSize,
|
||||
extra_line_additional_indent: Option<IndentSize>,
|
||||
prevent_auto_indent: bool,
|
||||
},
|
||||
/// Clear the current line
|
||||
ClearCurrentLine,
|
||||
/// Unindent the current line and add continuation
|
||||
UnindentCurrentLine { continuation: Arc<str> },
|
||||
}
|
||||
|
||||
impl NewlineConfig {
|
||||
fn has_extra_line(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Newline {
|
||||
extra_line_additional_indent: Some(_),
|
||||
..
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn insert_extra_newline_brackets(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
range: Range<MultiBufferOffset>,
|
||||
|
||||
@@ -215,8 +215,7 @@ impl Settings for EditorSettings {
|
||||
},
|
||||
scrollbar: Scrollbar {
|
||||
show: scrollbar.show.map(Into::into).unwrap(),
|
||||
git_diff: scrollbar.git_diff.unwrap()
|
||||
&& content.git.unwrap().enabled.unwrap().is_git_diff_enabled(),
|
||||
git_diff: scrollbar.git_diff.unwrap(),
|
||||
selected_text: scrollbar.selected_text.unwrap(),
|
||||
selected_symbol: scrollbar.selected_symbol.unwrap(),
|
||||
search_results: scrollbar.search_results.unwrap(),
|
||||
|
||||
@@ -20880,36 +20880,6 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_up(&MoveUp, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
|
||||
});
|
||||
cx.assert_state_with_diff(
|
||||
indoc! { "
|
||||
ˇone
|
||||
- two
|
||||
three
|
||||
five
|
||||
"}
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_down(&MoveDown, window, cx);
|
||||
editor.move_down(&MoveDown, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
|
||||
});
|
||||
cx.assert_state_with_diff(
|
||||
indoc! { "
|
||||
one
|
||||
- two
|
||||
ˇthree
|
||||
- four
|
||||
five
|
||||
"}
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
cx.set_state(indoc! { "
|
||||
one
|
||||
ˇTWO
|
||||
@@ -20949,66 +20919,6 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggling_adjacent_diff_hunks_2(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let diff_base = r#"
|
||||
lineA
|
||||
lineB
|
||||
lineC
|
||||
lineD
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
cx.set_state(
|
||||
&r#"
|
||||
ˇlineA1
|
||||
lineB
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
cx.set_head_text(&diff_base);
|
||||
executor.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
- lineA
|
||||
+ ˇlineA1
|
||||
lineB
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_down(&MoveDown, window, cx);
|
||||
editor.move_right(&MoveRight, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
- lineA
|
||||
+ lineA1
|
||||
lˇineB
|
||||
- lineC
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edits_around_expanded_deletion_hunks(
|
||||
executor: BackgroundExecutor,
|
||||
@@ -28021,7 +27931,7 @@ async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
|
||||
"
|
||||
});
|
||||
|
||||
// Case 2: Test adding new line after nested list continues the list with unchecked task
|
||||
// Case 2: Test adding new line after nested list preserves indent of previous line
|
||||
cx.set_state(&indoc! {"
|
||||
- [ ] Item 1
|
||||
- [ ] Item 1.a
|
||||
@@ -28038,12 +27948,20 @@ async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
|
||||
- [x] Item 2
|
||||
- [x] Item 2.a
|
||||
- [x] Item 2.b
|
||||
- [ ] ˇ"
|
||||
ˇ"
|
||||
});
|
||||
|
||||
// Case 3: Test adding content to continued list item
|
||||
// Case 3: Test adding a new nested list item preserves indent
|
||||
cx.set_state(&indoc! {"
|
||||
- [ ] Item 1
|
||||
- [ ] Item 1.a
|
||||
- [x] Item 2
|
||||
- [x] Item 2.a
|
||||
- [x] Item 2.b
|
||||
ˇ"
|
||||
});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("Item 2.c", window, cx);
|
||||
editor.handle_input("-", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
@@ -28052,10 +27970,22 @@ async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
|
||||
- [x] Item 2
|
||||
- [x] Item 2.a
|
||||
- [x] Item 2.b
|
||||
- [ ] Item 2.cˇ"
|
||||
-ˇ"
|
||||
});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input(" [x] Item 2.c", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] Item 1
|
||||
- [ ] Item 1.a
|
||||
- [x] Item 2
|
||||
- [x] Item 2.a
|
||||
- [x] Item 2.b
|
||||
- [x] Item 2.cˇ"
|
||||
});
|
||||
|
||||
// Case 4: Test adding new line after nested ordered list continues with next number
|
||||
// Case 4: Test adding new line after nested ordered list preserves indent of previous line
|
||||
cx.set_state(indoc! {"
|
||||
1. Item 1
|
||||
1. Item 1.a
|
||||
@@ -28072,12 +28002,44 @@ async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
|
||||
2. Item 2
|
||||
1. Item 2.a
|
||||
2. Item 2.b
|
||||
3. ˇ"
|
||||
ˇ"
|
||||
});
|
||||
|
||||
// Case 5: Adding content to continued ordered list item
|
||||
// Case 5: Adding new ordered list item preserves indent
|
||||
cx.set_state(indoc! {"
|
||||
1. Item 1
|
||||
1. Item 1.a
|
||||
2. Item 2
|
||||
1. Item 2.a
|
||||
2. Item 2.b
|
||||
ˇ"
|
||||
});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("Item 2.c", window, cx);
|
||||
editor.handle_input("3", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. Item 1
|
||||
1. Item 1.a
|
||||
2. Item 2
|
||||
1. Item 2.a
|
||||
2. Item 2.b
|
||||
3ˇ"
|
||||
});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input(".", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. Item 1
|
||||
1. Item 1.a
|
||||
2. Item 2
|
||||
1. Item 2.a
|
||||
2. Item 2.b
|
||||
3.ˇ"
|
||||
});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input(" Item 2.c", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
@@ -29445,524 +29407,6 @@ async fn test_find_references_single_case(cx: &mut TestAppContext) {
|
||||
cx.assert_editor_state(after);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = Some(2.try_into().unwrap());
|
||||
});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
// Case 1: Adding newline after (whitespace + prefix + any non-whitespace) adds marker
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] taskˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] ˇ
|
||||
"});
|
||||
|
||||
// Case 2: Works with checked task items too
|
||||
cx.set_state(indoc! {"
|
||||
- [x] completed taskˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [x] completed task
|
||||
- [ ] ˇ
|
||||
"});
|
||||
|
||||
// Case 3: Cursor position doesn't matter - content after marker is what counts
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] taˇsk
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] ta
|
||||
- [ ] ˇsk
|
||||
"});
|
||||
|
||||
// Case 4: Adding newline after (whitespace + prefix + some whitespace) does NOT add marker
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(
|
||||
indoc! {"
|
||||
- [ ]$$
|
||||
ˇ
|
||||
"}
|
||||
.replace("$", " ")
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
// Case 5: Adding newline with content adds marker preserving indentation
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] indentedˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] indented
|
||||
- [ ] ˇ
|
||||
"});
|
||||
|
||||
// Case 6: Adding newline with cursor right after prefix, unindents
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] sub task
|
||||
- [ ] ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] sub task
|
||||
- [ ] ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
|
||||
// Case 7: Adding newline with cursor right after prefix, removes marker
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] sub task
|
||||
- [ ] ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] sub task
|
||||
ˇ
|
||||
"});
|
||||
|
||||
// Case 8: Cursor before or inside prefix does not add marker
|
||||
cx.set_state(indoc! {"
|
||||
ˇ- [ ] task
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
ˇ- [ ] task
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
- [ˇ ] task
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [
|
||||
ˇ
|
||||
] task
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_unordered_list_continuation(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = Some(2.try_into().unwrap());
|
||||
});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
// Case 1: Adding newline after (whitespace + marker + any non-whitespace) adds marker
|
||||
cx.set_state(indoc! {"
|
||||
- itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- item
|
||||
- ˇ
|
||||
"});
|
||||
|
||||
// Case 2: Works with different markers
|
||||
cx.set_state(indoc! {"
|
||||
* starred itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
* starred item
|
||||
* ˇ
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
+ plus itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
+ plus item
|
||||
+ ˇ
|
||||
"});
|
||||
|
||||
// Case 3: Cursor position doesn't matter - content after marker is what counts
|
||||
cx.set_state(indoc! {"
|
||||
- itˇem
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- it
|
||||
- ˇem
|
||||
"});
|
||||
|
||||
// Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
|
||||
cx.set_state(indoc! {"
|
||||
- ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(
|
||||
indoc! {"
|
||||
- $
|
||||
ˇ
|
||||
"}
|
||||
.replace("$", " ")
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
// Case 5: Adding newline with content adds marker preserving indentation
|
||||
cx.set_state(indoc! {"
|
||||
- item
|
||||
- indentedˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- item
|
||||
- indented
|
||||
- ˇ
|
||||
"});
|
||||
|
||||
// Case 6: Adding newline with cursor right after marker, unindents
|
||||
cx.set_state(indoc! {"
|
||||
- item
|
||||
- sub item
|
||||
- ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- item
|
||||
- sub item
|
||||
- ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
|
||||
// Case 7: Adding newline with cursor right after marker, removes marker
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- item
|
||||
- sub item
|
||||
- ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- item
|
||||
- sub item
|
||||
ˇ
|
||||
"});
|
||||
|
||||
// Case 8: Cursor before or inside prefix does not add marker
|
||||
cx.set_state(indoc! {"
|
||||
ˇ- item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
ˇ- item
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
-ˇ item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
-
|
||||
ˇitem
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_ordered_list_continuation(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = Some(2.try_into().unwrap());
|
||||
});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
// Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
|
||||
cx.set_state(indoc! {"
|
||||
1. first itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. first item
|
||||
2. ˇ
|
||||
"});
|
||||
|
||||
// Case 2: Works with larger numbers
|
||||
cx.set_state(indoc! {"
|
||||
10. tenth itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
10. tenth item
|
||||
11. ˇ
|
||||
"});
|
||||
|
||||
// Case 3: Cursor position doesn't matter - content after marker is what counts
|
||||
cx.set_state(indoc! {"
|
||||
1. itˇem
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. it
|
||||
2. ˇem
|
||||
"});
|
||||
|
||||
// Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
|
||||
cx.set_state(indoc! {"
|
||||
1. ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(
|
||||
indoc! {"
|
||||
1. $
|
||||
ˇ
|
||||
"}
|
||||
.replace("$", " ")
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
// Case 5: Adding newline with content adds marker preserving indentation
|
||||
cx.set_state(indoc! {"
|
||||
1. item
|
||||
2. indentedˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. item
|
||||
2. indented
|
||||
3. ˇ
|
||||
"});
|
||||
|
||||
// Case 6: Adding newline with cursor right after marker, unindents
|
||||
cx.set_state(indoc! {"
|
||||
1. item
|
||||
2. sub item
|
||||
3. ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. item
|
||||
2. sub item
|
||||
1. ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
|
||||
// Case 7: Adding newline with cursor right after marker, removes marker
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. item
|
||||
2. sub item
|
||||
1. ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. item
|
||||
2. sub item
|
||||
ˇ
|
||||
"});
|
||||
|
||||
// Case 8: Cursor before or inside prefix does not add marker
|
||||
cx.set_state(indoc! {"
|
||||
ˇ1. item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
ˇ1. item
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
1ˇ. item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1
|
||||
ˇ. item
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_should_not_autoindent_ordered_list(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = Some(2.try_into().unwrap());
|
||||
});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
// Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
|
||||
cx.set_state(indoc! {"
|
||||
1. first item
|
||||
1. sub first item
|
||||
2. sub second item
|
||||
3. ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. first item
|
||||
1. sub first item
|
||||
2. sub second item
|
||||
1. ˇ
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tab_list_indent(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = Some(2.try_into().unwrap());
|
||||
});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
// Case 1: Unordered list - cursor after prefix, adds indent before prefix
|
||||
cx.set_state(indoc! {"
|
||||
- ˇitem
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$- ˇitem
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 2: Task list - cursor after prefix
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] ˇtask
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$- [ ] ˇtask
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 3: Ordered list - cursor after prefix
|
||||
cx.set_state(indoc! {"
|
||||
1. ˇfirst
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$1. ˇfirst
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 4: With existing indentation - adds more indent
|
||||
let initial = indoc! {"
|
||||
$$- ˇitem
|
||||
"};
|
||||
cx.set_state(initial.replace("$", " ").as_str());
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$$$- ˇitem
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 5: Empty list item
|
||||
cx.set_state(indoc! {"
|
||||
- ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$- ˇ
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 6: Cursor at end of line with content
|
||||
cx.set_state(indoc! {"
|
||||
- itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$- itemˇ
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 7: Cursor at start of list item, indents it
|
||||
cx.set_state(indoc! {"
|
||||
- item
|
||||
ˇ - sub item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
- item
|
||||
ˇ - sub item
|
||||
"};
|
||||
cx.assert_editor_state(expected);
|
||||
|
||||
// Case 8: Cursor at start of list item, moves the cursor when "indent_list_on_tab" is false
|
||||
cx.update_editor(|_, _, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.all_languages.defaults.indent_list_on_tab = Some(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
cx.set_state(indoc! {"
|
||||
- item
|
||||
ˇ - sub item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
- item
|
||||
ˇ- sub item
|
||||
"};
|
||||
cx.assert_editor_state(expected);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_local_worktree_trust(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -37,7 +37,11 @@ use crate::{
|
||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use file_icons::FileIcons;
|
||||
use git::{Oid, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
|
||||
use git::{
|
||||
Oid,
|
||||
blame::{BlameEntry, ParsedCommitMessage},
|
||||
status::FileStatus,
|
||||
};
|
||||
use gpui::{
|
||||
Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
|
||||
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
||||
@@ -5417,12 +5421,6 @@ 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,
|
||||
|
||||
@@ -3,9 +3,9 @@ use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
|
||||
use git::{
|
||||
GitHostingProviderRegistry, Oid,
|
||||
blame::{Blame, BlameEntry},
|
||||
commit::ParsedCommitMessage,
|
||||
GitHostingProviderRegistry, GitRemote, Oid,
|
||||
blame::{Blame, BlameEntry, ParsedCommitMessage},
|
||||
parse_git_remote_url,
|
||||
};
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, Context, Entity, Hsla, ScrollHandle, Subscription, Task,
|
||||
@@ -525,7 +525,12 @@ impl GitBlame {
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
|
||||
.and_then(|(repo, _)| repo.read(cx).default_remote_url());
|
||||
.and_then(|(repo, _)| {
|
||||
repo.read(cx)
|
||||
.remote_upstream_url
|
||||
.clone()
|
||||
.or(repo.read(cx).remote_origin_url.clone())
|
||||
});
|
||||
let blame_buffer = project
|
||||
.update(cx, |project, cx| project.blame_buffer(&buffer, None, cx));
|
||||
Ok(async move {
|
||||
@@ -549,19 +554,13 @@ impl GitBlame {
|
||||
entries,
|
||||
snapshot.max_point().row,
|
||||
);
|
||||
let commit_details = messages
|
||||
.into_iter()
|
||||
.map(|(oid, message)| {
|
||||
let parsed_commit_message =
|
||||
ParsedCommitMessage::parse(
|
||||
oid.to_string(),
|
||||
message,
|
||||
remote_url.as_deref(),
|
||||
Some(provider_registry.clone()),
|
||||
);
|
||||
(oid, parsed_commit_message)
|
||||
})
|
||||
.collect();
|
||||
let commit_details = parse_commit_messages(
|
||||
messages,
|
||||
remote_url,
|
||||
provider_registry.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
res.push((
|
||||
id,
|
||||
snapshot,
|
||||
@@ -681,6 +680,55 @@ fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree
|
||||
entries
|
||||
}
|
||||
|
||||
async fn parse_commit_messages(
|
||||
messages: impl IntoIterator<Item = (Oid, String)>,
|
||||
remote_url: Option<String>,
|
||||
provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
) -> HashMap<Oid, ParsedCommitMessage> {
|
||||
let mut commit_details = HashMap::default();
|
||||
|
||||
let parsed_remote_url = remote_url
|
||||
.as_deref()
|
||||
.and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url));
|
||||
|
||||
for (oid, message) in messages {
|
||||
let permalink = if let Some((provider, git_remote)) = parsed_remote_url.as_ref() {
|
||||
Some(provider.build_commit_permalink(
|
||||
git_remote,
|
||||
git::BuildCommitPermalinkParams {
|
||||
sha: oid.to_string().as_str(),
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let remote = parsed_remote_url
|
||||
.as_ref()
|
||||
.map(|(provider, remote)| GitRemote {
|
||||
host: provider.clone(),
|
||||
owner: remote.owner.clone().into(),
|
||||
repo: remote.repo.clone().into(),
|
||||
});
|
||||
|
||||
let pull_request = parsed_remote_url
|
||||
.as_ref()
|
||||
.and_then(|(provider, remote)| provider.extract_pull_request(remote, &message));
|
||||
|
||||
commit_details.insert(
|
||||
oid,
|
||||
ParsedCommitMessage {
|
||||
message: message.into(),
|
||||
permalink,
|
||||
remote,
|
||||
pull_request,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
commit_details
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
};
|
||||
use gpui::{Bounds, Context, Pixels, Window};
|
||||
use language::Point;
|
||||
use multi_buffer::{Anchor, ToPoint};
|
||||
use multi_buffer::Anchor;
|
||||
use std::cmp;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
@@ -186,19 +186,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
let style = self.style(cx).clone();
|
||||
let sticky_headers = self.sticky_headers(&style, cx).unwrap_or_default();
|
||||
let visible_sticky_headers = sticky_headers
|
||||
.iter()
|
||||
.filter(|h| {
|
||||
let buffer_snapshot = display_map.buffer_snapshot();
|
||||
let buffer_range =
|
||||
h.range.start.to_point(buffer_snapshot)..h.range.end.to_point(buffer_snapshot);
|
||||
|
||||
buffer_range.contains(&Point::new(target_top as u32, 0))
|
||||
})
|
||||
.count();
|
||||
|
||||
let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
|
||||
0.
|
||||
} else {
|
||||
@@ -231,7 +218,7 @@ impl Editor {
|
||||
let was_autoscrolled = match strategy {
|
||||
AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
|
||||
let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
|
||||
let target_top = (target_top - margin - visible_sticky_headers as f64).max(0.0);
|
||||
let target_top = (target_top - margin).max(0.0);
|
||||
let target_bottom = target_bottom + margin;
|
||||
let start_row = scroll_position.y;
|
||||
let end_row = start_row + visible_lines;
|
||||
|
||||
@@ -205,49 +205,6 @@ 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");
|
||||
@@ -319,49 +276,6 @@ 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");
|
||||
|
||||
@@ -1 +1 @@
|
||||
../../LICENSE-GPL
|
||||
LICENSE-GPL
|
||||
@@ -19,9 +19,6 @@ 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>>>,
|
||||
@@ -32,7 +29,6 @@ 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 {
|
||||
@@ -58,7 +54,6 @@ impl ExtensionHostProxy {
|
||||
slash_command_proxy: RwLock::default(),
|
||||
context_server_proxy: RwLock::default(),
|
||||
debug_adapter_provider_proxy: RwLock::default(),
|
||||
language_model_provider_proxy: RwLock::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,15 +90,6 @@ 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 {
|
||||
@@ -460,37 +446,3 @@ 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,8 +93,6 @@ 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 {
|
||||
@@ -290,16 +288,6 @@ 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
|
||||
@@ -370,7 +358,6 @@ fn manifest_from_old_manifest(
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +391,6 @@ mod tests {
|
||||
capabilities: vec![],
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -331,6 +331,7 @@ static mut EXTENSION: Option<Box<dyn Extension>> = None;
|
||||
pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes"));
|
||||
|
||||
mod wit {
|
||||
|
||||
wit_bindgen::generate!({
|
||||
skip: ["init-extension"],
|
||||
path: "./wit/since_v0.8.0",
|
||||
@@ -523,12 +524,6 @@ impl wit::Guest for Component {
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||
pub struct LanguageServerId(String);
|
||||
|
||||
impl LanguageServerId {
|
||||
pub fn new(value: String) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for LanguageServerId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
@@ -545,12 +540,6 @@ impl fmt::Display for LanguageServerId {
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||
pub struct ContextServerId(String);
|
||||
|
||||
impl ContextServerId {
|
||||
pub fn new(value: String) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ContextServerId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
|
||||
@@ -148,7 +148,6 @@ fn manifest() -> ExtensionManifest {
|
||||
)],
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,6 @@ mod tests {
|
||||
capabilities: vec![],
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -165,7 +165,6 @@ 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,
|
||||
},
|
||||
@@ -197,7 +196,6 @@ 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,
|
||||
},
|
||||
@@ -378,7 +376,6 @@ 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,
|
||||
},
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::Oid;
|
||||
use crate::commit::get_messages;
|
||||
use crate::repository::RepoPath;
|
||||
use crate::{GitRemote, Oid};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::AsyncWriteExt;
|
||||
use gpui::SharedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::Stdio;
|
||||
use std::{ops::Range, path::Path};
|
||||
@@ -20,6 +21,14 @@ pub struct Blame {
|
||||
pub messages: HashMap<Oid, String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ParsedCommitMessage {
|
||||
pub message: SharedString,
|
||||
pub permalink: Option<url::Url>,
|
||||
pub pull_request: Option<crate::hosting_provider::PullRequest>,
|
||||
pub remote: Option<GitRemote>,
|
||||
}
|
||||
|
||||
impl Blame {
|
||||
pub async fn for_path(
|
||||
git_binary: &Path,
|
||||
|
||||
@@ -1,52 +1,7 @@
|
||||
use crate::{
|
||||
BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, parse_git_remote_url,
|
||||
status::StatusCode,
|
||||
};
|
||||
use crate::{Oid, status::StatusCode};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::SharedString;
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ParsedCommitMessage {
|
||||
pub message: SharedString,
|
||||
pub permalink: Option<url::Url>,
|
||||
pub pull_request: Option<crate::hosting_provider::PullRequest>,
|
||||
pub remote: Option<GitRemote>,
|
||||
}
|
||||
|
||||
impl ParsedCommitMessage {
|
||||
pub fn parse(
|
||||
sha: String,
|
||||
message: String,
|
||||
remote_url: Option<&str>,
|
||||
provider_registry: Option<Arc<GitHostingProviderRegistry>>,
|
||||
) -> Self {
|
||||
if let Some((hosting_provider, remote)) = provider_registry
|
||||
.and_then(|reg| remote_url.and_then(|url| parse_git_remote_url(reg, url)))
|
||||
{
|
||||
let pull_request = hosting_provider.extract_pull_request(&remote, &message);
|
||||
Self {
|
||||
message: message.into(),
|
||||
permalink: Some(
|
||||
hosting_provider
|
||||
.build_commit_permalink(&remote, BuildCommitPermalinkParams { sha: &sha }),
|
||||
),
|
||||
pull_request,
|
||||
remote: Some(GitRemote {
|
||||
host: hosting_provider,
|
||||
owner: remote.owner.into(),
|
||||
repo: remote.repo.into(),
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
message: message.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
use std::path::Path;
|
||||
|
||||
pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
|
||||
if shas.is_empty() {
|
||||
|
||||
@@ -3,7 +3,10 @@ use crate::{
|
||||
commit_view::CommitView,
|
||||
};
|
||||
use editor::{BlameRenderer, Editor, hover_markdown_style};
|
||||
use git::{blame::BlameEntry, commit::ParsedCommitMessage, repository::CommitSummary};
|
||||
use git::{
|
||||
blame::{BlameEntry, ParsedCommitMessage},
|
||||
repository::CommitSummary,
|
||||
};
|
||||
use gpui::{
|
||||
ClipboardItem, Entity, Hsla, MouseButton, ScrollHandle, Subscription, TextStyle,
|
||||
TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
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();
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use editor::hover_markdown_style;
|
||||
use futures::Future;
|
||||
use git::blame::BlameEntry;
|
||||
use git::repository::CommitSummary;
|
||||
use git::{GitRemote, commit::ParsedCommitMessage};
|
||||
use git::{GitRemote, blame::ParsedCommitMessage};
|
||||
use gpui::{
|
||||
App, Asset, ClipboardItem, Element, Entity, MouseButton, ParentElement, Render, ScrollHandle,
|
||||
StatefulInteractiveElement, WeakEntity, prelude::*,
|
||||
|
||||
@@ -15,13 +15,12 @@ use askpass::AskPassDelegate;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::RewrapOptions;
|
||||
use editor::{
|
||||
Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset,
|
||||
actions::ExpandAllDiffHunks,
|
||||
};
|
||||
use futures::StreamExt as _;
|
||||
use git::commit::ParsedCommitMessage;
|
||||
use git::blame::ParsedCommitMessage;
|
||||
use git::repository::{
|
||||
Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitter,
|
||||
PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking,
|
||||
@@ -31,14 +30,15 @@ use git::stash::GitStash;
|
||||
use git::status::StageStatus;
|
||||
use git::{Amend, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus};
|
||||
use git::{
|
||||
ExpandCommitEditor, GitHostingProviderRegistry, RestoreTrackedFiles, StageAll, StashAll,
|
||||
StashApply, StashPop, TrashUntrackedFiles, UnstageAll,
|
||||
ExpandCommitEditor, RestoreTrackedFiles, StageAll, StashAll, StashApply, StashPop,
|
||||
TrashUntrackedFiles, UnstageAll,
|
||||
};
|
||||
use gpui::{
|
||||
Action, AsyncApp, AsyncWindowContext, Bounds, ClickEvent, Corner, DismissEvent, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, KeyContext, MouseButton, MouseDownEvent, Point,
|
||||
PromptLevel, ScrollStrategy, Subscription, Task, UniformListScrollHandle, WeakEntity, actions,
|
||||
anchored, deferred, point, size, uniform_list,
|
||||
EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior,
|
||||
ListSizingBehavior, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy,
|
||||
Subscription, Task, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point,
|
||||
size, uniform_list,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, File};
|
||||
@@ -58,7 +58,7 @@ use project::{
|
||||
git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op},
|
||||
project_settings::{GitPathStyle, ProjectSettings},
|
||||
};
|
||||
use prompt_store::{BuiltInPrompt, PromptId, PromptStore, RULES_FILE_NAMES};
|
||||
use prompt_store::{PromptId, PromptStore, RULES_FILE_NAMES};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore, StatusStyle};
|
||||
use std::future::Future;
|
||||
@@ -212,7 +212,8 @@ const GIT_PANEL_KEY: &str = "GitPanel";
|
||||
|
||||
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
// TODO: We should revise this part. It seems the indentation width is not aligned with the one in project panel
|
||||
const TREE_INDENT: f32 = 16.0;
|
||||
const TREE_INDENT: f32 = 12.0;
|
||||
const TREE_INDENT_GUIDE_OFFSET: f32 = 16.0;
|
||||
|
||||
pub fn register(workspace: &mut Workspace) {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
@@ -2181,13 +2182,7 @@ impl GitPanel {
|
||||
let editor = cx.new(|cx| Editor::for_buffer(buffer, None, window, cx));
|
||||
let wrapped_message = editor.update(cx, |editor, cx| {
|
||||
editor.select_all(&Default::default(), window, cx);
|
||||
editor.rewrap_impl(
|
||||
RewrapOptions {
|
||||
override_language_settings: false,
|
||||
preserve_existing_whitespace: true,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
editor.rewrap(&Default::default(), window, cx);
|
||||
editor.text(cx)
|
||||
});
|
||||
if wrapped_message.trim().is_empty() {
|
||||
@@ -2579,26 +2574,25 @@ impl GitPanel {
|
||||
is_using_legacy_zed_pro: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> String {
|
||||
const DEFAULT_PROMPT: &str = include_str!("commit_message_prompt.txt");
|
||||
|
||||
// Remove this once we stop supporting legacy Zed Pro
|
||||
// In legacy Zed Pro, Git commit summary generation did not count as a
|
||||
// prompt. If the user changes the prompt, our classification will fail,
|
||||
// meaning that users will be charged for generating commit messages.
|
||||
if is_using_legacy_zed_pro {
|
||||
return BuiltInPrompt::CommitMessage.default_content().to_string();
|
||||
return DEFAULT_PROMPT.to_string();
|
||||
}
|
||||
|
||||
let load = async {
|
||||
let store = cx.update(|cx| PromptStore::global(cx)).ok()?.await.ok()?;
|
||||
store
|
||||
.update(cx, |s, cx| {
|
||||
s.load(PromptId::BuiltIn(BuiltInPrompt::CommitMessage), cx)
|
||||
})
|
||||
.update(cx, |s, cx| s.load(PromptId::CommitMessage, cx))
|
||||
.ok()?
|
||||
.await
|
||||
.ok()
|
||||
};
|
||||
load.await
|
||||
.unwrap_or_else(|| BuiltInPrompt::CommitMessage.default_content().to_string())
|
||||
load.await.unwrap_or_else(|| DEFAULT_PROMPT.to_string())
|
||||
}
|
||||
|
||||
/// Generates a commit message using an LLM.
|
||||
@@ -2849,15 +2843,93 @@ 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();
|
||||
|
||||
crate::clone::clone_and_open(
|
||||
repo.into(),
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
Arc::new(|_workspace: &mut workspace::Workspace, _window, _cx| {}),
|
||||
);
|
||||
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();
|
||||
}
|
||||
|
||||
pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -4625,10 +4697,7 @@ impl GitPanel {
|
||||
},
|
||||
)
|
||||
.with_render_fn(cx.entity(), |_, params, _, _| {
|
||||
// Magic number to align the tree item is 3 here
|
||||
// because we're using 12px as the left-side padding
|
||||
// and 3 makes the alignment work with the bounding box of the icon
|
||||
let left_offset = px(TREE_INDENT + 3_f32);
|
||||
let left_offset = px(TREE_INDENT_GUIDE_OFFSET);
|
||||
let indent_size = params.indent_size;
|
||||
let item_height = params.item_height;
|
||||
|
||||
@@ -4656,6 +4725,10 @@ impl GitPanel {
|
||||
})
|
||||
.size_full()
|
||||
.flex_grow()
|
||||
.with_sizing_behavior(ListSizingBehavior::Auto)
|
||||
.with_horizontal_sizing_behavior(
|
||||
ListHorizontalSizingBehavior::Unconstrained,
|
||||
)
|
||||
.with_width_from_item(self.max_width_item_index)
|
||||
.track_scroll(&self.scroll_handle),
|
||||
)
|
||||
@@ -4679,7 +4752,7 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
fn entry_label(&self, label: impl Into<SharedString>, color: Color) -> Label {
|
||||
Label::new(label.into()).color(color)
|
||||
Label::new(label.into()).color(color).single_line()
|
||||
}
|
||||
|
||||
fn list_item_height(&self) -> Rems {
|
||||
@@ -4701,8 +4774,8 @@ impl GitPanel {
|
||||
.h(self.list_item_height())
|
||||
.w_full()
|
||||
.items_end()
|
||||
.px_3()
|
||||
.pb_1()
|
||||
.px(rems(0.75)) // ~12px
|
||||
.pb(rems(0.3125)) // ~ 5px
|
||||
.child(
|
||||
Label::new(header.title())
|
||||
.color(Color::Muted)
|
||||
@@ -4890,68 +4963,113 @@ impl GitPanel {
|
||||
let marked_bg_alpha = 0.12;
|
||||
let state_opacity_step = 0.04;
|
||||
|
||||
let info_color = cx.theme().status().info;
|
||||
|
||||
let base_bg = match (selected, marked) {
|
||||
(true, true) => info_color.alpha(selected_bg_alpha + marked_bg_alpha),
|
||||
(true, false) => info_color.alpha(selected_bg_alpha),
|
||||
(false, true) => info_color.alpha(marked_bg_alpha),
|
||||
(true, true) => cx
|
||||
.theme()
|
||||
.status()
|
||||
.info
|
||||
.alpha(selected_bg_alpha + marked_bg_alpha),
|
||||
(true, false) => cx.theme().status().info.alpha(selected_bg_alpha),
|
||||
(false, true) => cx.theme().status().info.alpha(marked_bg_alpha),
|
||||
_ => cx.theme().colors().ghost_element_background,
|
||||
};
|
||||
|
||||
let (hover_bg, active_bg) = if selected {
|
||||
(
|
||||
info_color.alpha(selected_bg_alpha + state_opacity_step),
|
||||
info_color.alpha(selected_bg_alpha + state_opacity_step * 2.0),
|
||||
)
|
||||
let hover_bg = if selected {
|
||||
cx.theme()
|
||||
.status()
|
||||
.info
|
||||
.alpha(selected_bg_alpha + state_opacity_step)
|
||||
} else {
|
||||
(
|
||||
cx.theme().colors().ghost_element_hover,
|
||||
cx.theme().colors().ghost_element_active,
|
||||
)
|
||||
cx.theme().colors().ghost_element_hover
|
||||
};
|
||||
|
||||
let name_row = h_flex()
|
||||
.min_w_0()
|
||||
.flex_1()
|
||||
let active_bg = if selected {
|
||||
cx.theme()
|
||||
.status()
|
||||
.info
|
||||
.alpha(selected_bg_alpha + state_opacity_step * 2.0)
|
||||
} else {
|
||||
cx.theme().colors().ghost_element_active
|
||||
};
|
||||
|
||||
let mut name_row = h_flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(git_status_icon(status))
|
||||
.map(|this| {
|
||||
if tree_view {
|
||||
this.pl(px(depth as f32 * TREE_INDENT)).child(
|
||||
self.entry_label(display_name, label_color)
|
||||
.when(status.is_deleted(), Label::strikethrough)
|
||||
.truncate(),
|
||||
)
|
||||
} else {
|
||||
this.child(self.path_formatted(
|
||||
entry.parent_dir(path_style),
|
||||
path_color,
|
||||
display_name,
|
||||
label_color,
|
||||
path_style,
|
||||
git_path_style,
|
||||
status.is_deleted(),
|
||||
))
|
||||
}
|
||||
});
|
||||
.flex_1()
|
||||
.pl(if tree_view {
|
||||
px(depth as f32 * TREE_INDENT)
|
||||
} else {
|
||||
px(0.)
|
||||
})
|
||||
.child(git_status_icon(status));
|
||||
|
||||
name_row = if tree_view {
|
||||
name_row.child(
|
||||
self.entry_label(display_name, label_color)
|
||||
.when(status.is_deleted(), Label::strikethrough)
|
||||
.truncate(),
|
||||
)
|
||||
} else {
|
||||
name_row.child(h_flex().items_center().flex_1().map(|this| {
|
||||
self.path_formatted(
|
||||
this,
|
||||
entry.parent_dir(path_style),
|
||||
path_color,
|
||||
display_name,
|
||||
label_color,
|
||||
path_style,
|
||||
git_path_style,
|
||||
status.is_deleted(),
|
||||
)
|
||||
}))
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id(id)
|
||||
.h(self.list_item_height())
|
||||
.w_full()
|
||||
.pl_3()
|
||||
.pr_1()
|
||||
.gap_1p5()
|
||||
.border_1()
|
||||
.border_r_2()
|
||||
.when(selected && self.focus_handle.is_focused(window), |el| {
|
||||
el.border_color(cx.theme().colors().panel_focused_border)
|
||||
})
|
||||
.px(rems(0.75)) // ~12px
|
||||
.overflow_hidden()
|
||||
.flex_none()
|
||||
.gap_1p5()
|
||||
.bg(base_bg)
|
||||
.hover(|s| s.bg(hover_bg))
|
||||
.active(|s| s.bg(active_bg))
|
||||
.child(name_row)
|
||||
.hover(|this| this.bg(hover_bg))
|
||||
.active(|this| this.bg(active_bg))
|
||||
.on_click({
|
||||
cx.listener(move |this, event: &ClickEvent, window, cx| {
|
||||
this.selected_entry = Some(ix);
|
||||
cx.notify();
|
||||
if event.modifiers().secondary() {
|
||||
this.open_file(&Default::default(), window, cx)
|
||||
} else {
|
||||
this.open_diff(&Default::default(), window, cx);
|
||||
this.focus_handle.focus(window, cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
move |event: &MouseDownEvent, window, cx| {
|
||||
// why isn't this happening automatically? we are passing MouseButton::Right to `on_mouse_down`?
|
||||
if event.button != MouseButton::Right {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(this) = handle.upgrade() else {
|
||||
return;
|
||||
};
|
||||
this.update(cx, |this, cx| {
|
||||
this.deploy_entry_context_menu(event.position, ix, window, cx);
|
||||
});
|
||||
cx.stop_propagation();
|
||||
},
|
||||
)
|
||||
.child(name_row.overflow_x_hidden())
|
||||
.child(
|
||||
div()
|
||||
.id(checkbox_wrapper_id)
|
||||
@@ -5001,35 +5119,6 @@ impl GitPanel {
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
cx.listener(move |this, event: &ClickEvent, window, cx| {
|
||||
this.selected_entry = Some(ix);
|
||||
cx.notify();
|
||||
if event.modifiers().secondary() {
|
||||
this.open_file(&Default::default(), window, cx)
|
||||
} else {
|
||||
this.open_diff(&Default::default(), window, cx);
|
||||
this.focus_handle.focus(window, cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
move |event: &MouseDownEvent, window, cx| {
|
||||
// why isn't this happening automatically? we are passing MouseButton::Right to `on_mouse_down`?
|
||||
if event.button != MouseButton::Right {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(this) = handle.upgrade() else {
|
||||
return;
|
||||
};
|
||||
this.update(cx, |this, cx| {
|
||||
this.deploy_entry_context_menu(event.position, ix, window, cx);
|
||||
});
|
||||
cx.stop_propagation();
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -5054,23 +5143,29 @@ impl GitPanel {
|
||||
let selected_bg_alpha = 0.08;
|
||||
let state_opacity_step = 0.04;
|
||||
|
||||
let info_color = cx.theme().status().info;
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
let (base_bg, hover_bg, active_bg) = if selected {
|
||||
(
|
||||
info_color.alpha(selected_bg_alpha),
|
||||
info_color.alpha(selected_bg_alpha + state_opacity_step),
|
||||
info_color.alpha(selected_bg_alpha + state_opacity_step * 2.0),
|
||||
)
|
||||
let base_bg = if selected {
|
||||
cx.theme().status().info.alpha(selected_bg_alpha)
|
||||
} else {
|
||||
(
|
||||
colors.ghost_element_background,
|
||||
colors.ghost_element_hover,
|
||||
colors.ghost_element_active,
|
||||
)
|
||||
cx.theme().colors().ghost_element_background
|
||||
};
|
||||
|
||||
let hover_bg = if selected {
|
||||
cx.theme()
|
||||
.status()
|
||||
.info
|
||||
.alpha(selected_bg_alpha + state_opacity_step)
|
||||
} else {
|
||||
cx.theme().colors().ghost_element_hover
|
||||
};
|
||||
|
||||
let active_bg = if selected {
|
||||
cx.theme()
|
||||
.status()
|
||||
.info
|
||||
.alpha(selected_bg_alpha + state_opacity_step * 2.0)
|
||||
} else {
|
||||
cx.theme().colors().ghost_element_active
|
||||
};
|
||||
let folder_icon = if entry.expanded {
|
||||
IconName::FolderOpen
|
||||
} else {
|
||||
@@ -5093,8 +5188,9 @@ impl GitPanel {
|
||||
};
|
||||
|
||||
let name_row = h_flex()
|
||||
.min_w_0()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.flex_1()
|
||||
.pl(px(entry.depth as f32 * TREE_INDENT))
|
||||
.child(
|
||||
Icon::new(folder_icon)
|
||||
@@ -5106,21 +5202,28 @@ impl GitPanel {
|
||||
h_flex()
|
||||
.id(id)
|
||||
.h(self.list_item_height())
|
||||
.min_w_0()
|
||||
.w_full()
|
||||
.pl_3()
|
||||
.pr_1()
|
||||
.gap_1p5()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.border_1()
|
||||
.border_r_2()
|
||||
.when(selected && self.focus_handle.is_focused(window), |el| {
|
||||
el.border_color(cx.theme().colors().panel_focused_border)
|
||||
})
|
||||
.px(rems(0.75))
|
||||
.overflow_hidden()
|
||||
.flex_none()
|
||||
.gap_1p5()
|
||||
.bg(base_bg)
|
||||
.hover(|s| s.bg(hover_bg))
|
||||
.active(|s| s.bg(active_bg))
|
||||
.child(name_row)
|
||||
.hover(|this| this.bg(hover_bg))
|
||||
.active(|this| this.bg(active_bg))
|
||||
.on_click({
|
||||
let key = entry.key.clone();
|
||||
cx.listener(move |this, _event: &ClickEvent, window, cx| {
|
||||
this.selected_entry = Some(ix);
|
||||
this.toggle_directory(&key, window, cx);
|
||||
})
|
||||
})
|
||||
.child(name_row.overflow_x_hidden())
|
||||
.child(
|
||||
div()
|
||||
.id(checkbox_wrapper_id)
|
||||
@@ -5159,18 +5262,12 @@ impl GitPanel {
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let key = entry.key.clone();
|
||||
cx.listener(move |this, _event: &ClickEvent, window, cx| {
|
||||
this.selected_entry = Some(ix);
|
||||
this.toggle_directory(&key, window, cx);
|
||||
})
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn path_formatted(
|
||||
&self,
|
||||
parent: Div,
|
||||
directory: Option<String>,
|
||||
path_color: Color,
|
||||
file_name: String,
|
||||
@@ -5179,31 +5276,41 @@ impl GitPanel {
|
||||
git_path_style: GitPathStyle,
|
||||
strikethrough: bool,
|
||||
) -> Div {
|
||||
let file_name_first = git_path_style == GitPathStyle::FileNameFirst;
|
||||
let file_path_first = git_path_style == GitPathStyle::FilePathFirst;
|
||||
|
||||
let file_name = format!("{} ", file_name);
|
||||
|
||||
h_flex()
|
||||
.min_w_0()
|
||||
.overflow_hidden()
|
||||
.when(file_path_first, |this| this.flex_row_reverse())
|
||||
.child(
|
||||
div().flex_none().child(
|
||||
self.entry_label(file_name, label_color)
|
||||
.when(strikethrough, Label::strikethrough),
|
||||
),
|
||||
)
|
||||
.when_some(directory, |this, dir| {
|
||||
let path_name = if file_name_first {
|
||||
dir
|
||||
} else {
|
||||
format!("{dir}{}", path_style.primary_separator())
|
||||
};
|
||||
|
||||
parent
|
||||
.when(git_path_style == GitPathStyle::FileNameFirst, |this| {
|
||||
this.child(
|
||||
self.entry_label(path_name, path_color)
|
||||
.truncate_start()
|
||||
self.entry_label(
|
||||
match directory.as_ref().is_none_or(|d| d.is_empty()) {
|
||||
true => file_name.clone(),
|
||||
false => format!("{file_name} "),
|
||||
},
|
||||
label_color,
|
||||
)
|
||||
.when(strikethrough, Label::strikethrough),
|
||||
)
|
||||
})
|
||||
.when_some(directory, |this, dir| {
|
||||
match (
|
||||
!dir.is_empty(),
|
||||
git_path_style == GitPathStyle::FileNameFirst,
|
||||
) {
|
||||
(true, true) => this.child(
|
||||
self.entry_label(dir, path_color)
|
||||
.when(strikethrough, Label::strikethrough),
|
||||
),
|
||||
(true, false) => this.child(
|
||||
self.entry_label(
|
||||
format!("{dir}{}", path_style.primary_separator()),
|
||||
path_color,
|
||||
)
|
||||
.when(strikethrough, Label::strikethrough),
|
||||
),
|
||||
_ => this,
|
||||
}
|
||||
})
|
||||
.when(git_path_style == GitPathStyle::FilePathFirst, |this| {
|
||||
this.child(
|
||||
self.entry_label(file_name, label_color)
|
||||
.when(strikethrough, Label::strikethrough),
|
||||
)
|
||||
})
|
||||
@@ -5543,7 +5650,6 @@ impl GitPanelMessageTooltip {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let remote_url = repository.read(cx).default_remote_url();
|
||||
cx.new(|cx| {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let (details, workspace) = git_panel.update(cx, |git_panel, cx| {
|
||||
@@ -5553,21 +5659,16 @@ impl GitPanelMessageTooltip {
|
||||
)
|
||||
})?;
|
||||
let details = details.await?;
|
||||
let provider_registry = cx
|
||||
.update(|_, app| GitHostingProviderRegistry::default_global(app))
|
||||
.ok();
|
||||
|
||||
let commit_details = crate::commit_tooltip::CommitDetails {
|
||||
sha: details.sha.clone(),
|
||||
author_name: details.author_name.clone(),
|
||||
author_email: details.author_email.clone(),
|
||||
commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?,
|
||||
message: Some(ParsedCommitMessage::parse(
|
||||
details.sha.to_string(),
|
||||
details.message.to_string(),
|
||||
remote_url.as_deref(),
|
||||
provider_registry,
|
||||
)),
|
||||
message: Some(ParsedCommitMessage {
|
||||
message: details.message,
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
this.update(cx, |this: &mut GitPanelMessageTooltip, cx| {
|
||||
|
||||
@@ -10,7 +10,6 @@ use ui::{
|
||||
};
|
||||
|
||||
mod blame_ui;
|
||||
pub mod clone;
|
||||
|
||||
use git::{
|
||||
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||
|
||||
@@ -566,22 +566,22 @@ impl Model {
|
||||
|
||||
pub fn max_token_count(&self) -> u64 {
|
||||
match self {
|
||||
Self::Gemini25FlashLite
|
||||
| Self::Gemini25Flash
|
||||
| Self::Gemini25Pro
|
||||
| Self::Gemini3Pro
|
||||
| Self::Gemini3Flash => 1_048_576,
|
||||
Self::Gemini25FlashLite => 1_048_576,
|
||||
Self::Gemini25Flash => 1_048_576,
|
||||
Self::Gemini25Pro => 1_048_576,
|
||||
Self::Gemini3Pro => 1_048_576,
|
||||
Self::Gemini3Flash => 1_048_576,
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_output_tokens(&self) -> Option<u64> {
|
||||
match self {
|
||||
Model::Gemini25FlashLite
|
||||
| Model::Gemini25Flash
|
||||
| Model::Gemini25Pro
|
||||
| Model::Gemini3Pro
|
||||
| Model::Gemini3Flash => Some(65_536),
|
||||
Model::Gemini25FlashLite => Some(65_536),
|
||||
Model::Gemini25Flash => Some(65_536),
|
||||
Model::Gemini25Pro => Some(65_536),
|
||||
Model::Gemini3Pro => Some(65_536),
|
||||
Model::Gemini3Flash => Some(65_536),
|
||||
Model::Custom { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,14 +198,14 @@ wayland-backend = { version = "0.3.3", features = [
|
||||
"client_system",
|
||||
"dlopen",
|
||||
], optional = true }
|
||||
wayland-client = { version = "0.31.11", optional = true }
|
||||
wayland-cursor = { version = "0.31.11", optional = true }
|
||||
wayland-protocols = { version = "0.32.9", features = [
|
||||
wayland-client = { version = "0.31.2", optional = true }
|
||||
wayland-cursor = { version = "0.31.1", optional = true }
|
||||
wayland-protocols = { version = "0.31.2", features = [
|
||||
"client",
|
||||
"staging",
|
||||
"unstable",
|
||||
], optional = true }
|
||||
wayland-protocols-plasma = { version = "0.3.9", features = [
|
||||
wayland-protocols-plasma = { version = "0.2.0", features = [
|
||||
"client",
|
||||
], optional = true }
|
||||
wayland-protocols-wlr = { version = "0.3.9", features = [
|
||||
|
||||
@@ -5,7 +5,6 @@ use gpui::{
|
||||
|
||||
struct SubWindow {
|
||||
custom_titlebar: bool,
|
||||
is_dialog: bool,
|
||||
}
|
||||
|
||||
fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> impl IntoElement {
|
||||
@@ -24,10 +23,7 @@ fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> imp
|
||||
}
|
||||
|
||||
impl Render for SubWindow {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let window_bounds =
|
||||
WindowBounds::Windowed(Bounds::centered(None, size(px(250.0), px(200.0)), cx));
|
||||
|
||||
fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
@@ -56,28 +52,8 @@ impl Render for SubWindow {
|
||||
.child(
|
||||
div()
|
||||
.p_8()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child("SubWindow")
|
||||
.when(self.is_dialog, |div| {
|
||||
div.child(button("Open Nested Dialog", move |_, cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(window_bounds),
|
||||
kind: WindowKind::Dialog,
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: true,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
})
|
||||
.child(button("Close", |window, _| {
|
||||
window.remove_window();
|
||||
})),
|
||||
@@ -110,7 +86,6 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -126,39 +101,6 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Floating", move |_, cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(window_bounds),
|
||||
kind: WindowKind::Floating,
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Dialog", move |_, cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(window_bounds),
|
||||
kind: WindowKind::Dialog,
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: true,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -174,7 +116,6 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: true,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -190,7 +131,6 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -207,7 +147,6 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -223,7 +162,6 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -239,7 +177,6 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
@@ -316,7 +316,6 @@ impl SystemWindowTabController {
|
||||
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
|
||||
|
||||
let current_group = current_group?;
|
||||
// TODO: `.keys()` returns arbitrary order, what does "next" mean?
|
||||
let mut group_ids: Vec<_> = controller.tab_groups.keys().collect();
|
||||
let idx = group_ids.iter().position(|g| *g == current_group)?;
|
||||
let next_idx = (idx + 1) % group_ids.len();
|
||||
@@ -341,7 +340,6 @@ impl SystemWindowTabController {
|
||||
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
|
||||
|
||||
let current_group = current_group?;
|
||||
// TODO: `.keys()` returns arbitrary order, what does "previous" mean?
|
||||
let mut group_ids: Vec<_> = controller.tab_groups.keys().collect();
|
||||
let idx = group_ids.iter().position(|g| *g == current_group)?;
|
||||
let prev_idx = if idx == 0 {
|
||||
@@ -363,9 +361,12 @@ impl SystemWindowTabController {
|
||||
|
||||
/// Get all tabs in the same window.
|
||||
pub fn tabs(&self, id: WindowId) -> Option<&Vec<SystemWindowTab>> {
|
||||
self.tab_groups
|
||||
.values()
|
||||
.find(|tabs| tabs.iter().any(|tab| tab.id == id))
|
||||
let tab_group = self
|
||||
.tab_groups
|
||||
.iter()
|
||||
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| *group))?;
|
||||
|
||||
self.tab_groups.get(&tab_group)
|
||||
}
|
||||
|
||||
/// Initialize the visibility of the system window tab controller.
|
||||
@@ -440,7 +441,7 @@ impl SystemWindowTabController {
|
||||
/// Insert a tab into a tab group.
|
||||
pub fn add_tab(cx: &mut App, id: WindowId, tabs: Vec<SystemWindowTab>) {
|
||||
let mut controller = cx.global_mut::<SystemWindowTabController>();
|
||||
let Some(tab) = tabs.iter().find(|tab| tab.id == id).cloned() else {
|
||||
let Some(tab) = tabs.clone().into_iter().find(|tab| tab.id == id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -503,14 +504,16 @@ impl SystemWindowTabController {
|
||||
return;
|
||||
};
|
||||
|
||||
let initial_tabs_len = initial_tabs.len();
|
||||
let mut all_tabs = initial_tabs.clone();
|
||||
|
||||
for (_, mut tabs) in controller.tab_groups.drain() {
|
||||
tabs.retain(|tab| !all_tabs[..initial_tabs_len].contains(tab));
|
||||
all_tabs.extend(tabs);
|
||||
for tabs in controller.tab_groups.values() {
|
||||
all_tabs.extend(
|
||||
tabs.iter()
|
||||
.filter(|tab| !initial_tabs.contains(tab))
|
||||
.cloned(),
|
||||
);
|
||||
}
|
||||
|
||||
controller.tab_groups.clear();
|
||||
controller.tab_groups.insert(0, all_tabs);
|
||||
}
|
||||
|
||||
@@ -1077,9 +1080,11 @@ impl App {
|
||||
self.platform.window_appearance()
|
||||
}
|
||||
|
||||
/// Reads data from the platform clipboard.
|
||||
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_clipboard()
|
||||
/// Writes data to the primary selection buffer.
|
||||
/// Only available on Linux.
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
pub fn write_to_primary(&self, item: ClipboardItem) {
|
||||
self.platform.write_to_primary(item)
|
||||
}
|
||||
|
||||
/// Writes data to the platform clipboard.
|
||||
@@ -1094,31 +1099,9 @@ impl App {
|
||||
self.platform.read_from_primary()
|
||||
}
|
||||
|
||||
/// Writes data to the primary selection buffer.
|
||||
/// Only available on Linux.
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
pub fn write_to_primary(&self, item: ClipboardItem) {
|
||||
self.platform.write_to_primary(item)
|
||||
}
|
||||
|
||||
/// Reads data from macOS's "Find" pasteboard.
|
||||
///
|
||||
/// Used to share the current search string between apps.
|
||||
///
|
||||
/// https://developer.apple.com/documentation/appkit/nspasteboard/name-swift.struct/find
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_find_pasteboard()
|
||||
}
|
||||
|
||||
/// Writes data to macOS's "Find" pasteboard.
|
||||
///
|
||||
/// Used to share the current search string between apps.
|
||||
///
|
||||
/// https://developer.apple.com/documentation/appkit/nspasteboard/name-swift.struct/find
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn write_to_find_pasteboard(&self, item: ClipboardItem) {
|
||||
self.platform.write_to_find_pasteboard(item)
|
||||
/// Reads data from the platform clipboard.
|
||||
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_clipboard()
|
||||
}
|
||||
|
||||
/// Writes credentials to the platform keychain.
|
||||
|
||||
@@ -71,16 +71,6 @@ struct StateInner {
|
||||
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut Window, &mut App)>>,
|
||||
scrollbar_drag_start_height: Option<Pixels>,
|
||||
measuring_behavior: ListMeasuringBehavior,
|
||||
pending_scroll: Option<PendingScrollFraction>,
|
||||
}
|
||||
|
||||
/// Keeps track of a fractional scroll position within an item for restoration
|
||||
/// after remeasurement.
|
||||
struct PendingScrollFraction {
|
||||
/// The index of the item to scroll within.
|
||||
item_ix: usize,
|
||||
/// Fractional offset (0.0 to 1.0) within the item's height.
|
||||
fraction: f32,
|
||||
}
|
||||
|
||||
/// Whether the list is scrolling from top to bottom or bottom to top.
|
||||
@@ -235,7 +225,6 @@ impl ListState {
|
||||
reset: false,
|
||||
scrollbar_drag_start_height: None,
|
||||
measuring_behavior: ListMeasuringBehavior::default(),
|
||||
pending_scroll: None,
|
||||
})));
|
||||
this.splice(0..0, item_count);
|
||||
this
|
||||
@@ -265,45 +254,6 @@ impl ListState {
|
||||
self.splice(0..old_count, element_count);
|
||||
}
|
||||
|
||||
/// Remeasure all items while preserving proportional scroll position.
|
||||
///
|
||||
/// Use this when item heights may have changed (e.g., font size changes)
|
||||
/// but the number and identity of items remains the same.
|
||||
pub fn remeasure(&self) {
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
|
||||
let new_items = state.items.iter().map(|item| ListItem::Unmeasured {
|
||||
focus_handle: item.focus_handle(),
|
||||
});
|
||||
|
||||
// If there's a `logical_scroll_top`, we need to keep track of it as a
|
||||
// `PendingScrollFraction`, so we can later preserve that scroll
|
||||
// position proportionally to the item, in case the item's height
|
||||
// changes.
|
||||
if let Some(scroll_top) = state.logical_scroll_top {
|
||||
let mut cursor = state.items.cursor::<Count>(());
|
||||
cursor.seek(&Count(scroll_top.item_ix), Bias::Right);
|
||||
|
||||
if let Some(item) = cursor.item() {
|
||||
if let Some(size) = item.size() {
|
||||
let fraction = if size.height.0 > 0.0 {
|
||||
(scroll_top.offset_in_item.0 / size.height.0).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
state.pending_scroll = Some(PendingScrollFraction {
|
||||
item_ix: scroll_top.item_ix,
|
||||
fraction,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.items = SumTree::from_iter(new_items, ());
|
||||
state.measuring_behavior.reset();
|
||||
}
|
||||
|
||||
/// The number of items in this list.
|
||||
pub fn item_count(&self) -> usize {
|
||||
self.0.borrow().items.summary().count
|
||||
@@ -694,20 +644,6 @@ impl StateInner {
|
||||
let mut element = render_item(item_index, window, cx);
|
||||
let element_size = element.layout_as_root(available_item_space, window, cx);
|
||||
size = Some(element_size);
|
||||
|
||||
// If there's a pending scroll adjustment for the scroll-top
|
||||
// item, apply it, ensuring proportional scroll position is
|
||||
// maintained after re-measuring.
|
||||
if ix == 0 {
|
||||
if let Some(pending_scroll) = self.pending_scroll.take() {
|
||||
if pending_scroll.item_ix == scroll_top.item_ix {
|
||||
scroll_top.offset_in_item =
|
||||
Pixels(pending_scroll.fraction * element_size.height.0);
|
||||
self.logical_scroll_top = Some(scroll_top);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if visible_height < available_height {
|
||||
item_layouts.push_back(ItemLayout {
|
||||
index: item_index,
|
||||
@@ -1248,16 +1184,16 @@ impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Height {
|
||||
mod test {
|
||||
|
||||
use gpui::{ScrollDelta, ScrollWheelEvent};
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
self as gpui, AppContext, Context, Element, IntoElement, ListState, Render, Styled,
|
||||
TestAppContext, Window, div, list, point, px, size,
|
||||
};
|
||||
use crate::{self as gpui, TestAppContext};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
|
||||
use crate::{
|
||||
AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
|
||||
list, point, px, size,
|
||||
};
|
||||
|
||||
let cx = cx.add_empty_window();
|
||||
|
||||
let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
|
||||
@@ -1301,6 +1237,11 @@ mod test {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_scroll_by_positive_and_negative_distance(cx: &mut TestAppContext) {
|
||||
use crate::{
|
||||
AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div,
|
||||
list, point, px, size,
|
||||
};
|
||||
|
||||
let cx = cx.add_empty_window();
|
||||
|
||||
let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
|
||||
@@ -1343,68 +1284,4 @@ mod test {
|
||||
assert_eq!(offset.item_ix, 0);
|
||||
assert_eq!(offset.offset_in_item, px(0.));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_remeasure(cx: &mut TestAppContext) {
|
||||
let cx = cx.add_empty_window();
|
||||
|
||||
// Create a list with 10 items, each 100px tall. We'll keep a reference
|
||||
// to the item height so we can later change the height and assert how
|
||||
// `ListState` handles it.
|
||||
let item_height = Rc::new(Cell::new(100usize));
|
||||
let state = ListState::new(10, crate::ListAlignment::Top, px(10.));
|
||||
|
||||
struct TestView {
|
||||
state: ListState,
|
||||
item_height: Rc<Cell<usize>>,
|
||||
}
|
||||
|
||||
impl Render for TestView {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let height = self.item_height.get();
|
||||
list(self.state.clone(), move |_, _, _| {
|
||||
div().h(px(height as f32)).w_full().into_any()
|
||||
})
|
||||
.w_full()
|
||||
.h_full()
|
||||
}
|
||||
}
|
||||
|
||||
let state_clone = state.clone();
|
||||
let item_height_clone = item_height.clone();
|
||||
let view = cx.update(|_, cx| {
|
||||
cx.new(|_| TestView {
|
||||
state: state_clone,
|
||||
item_height: item_height_clone,
|
||||
})
|
||||
});
|
||||
|
||||
// Simulate scrolling 40px inside the element with index 2. Since the
|
||||
// original item height is 100px, this equates to 40% inside the item.
|
||||
state.scroll_to(gpui::ListOffset {
|
||||
item_ix: 2,
|
||||
offset_in_item: px(40.),
|
||||
});
|
||||
|
||||
cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
|
||||
view.clone()
|
||||
});
|
||||
|
||||
let offset = state.logical_scroll_top();
|
||||
assert_eq!(offset.item_ix, 2);
|
||||
assert_eq!(offset.offset_in_item, px(40.));
|
||||
|
||||
// Update the `item_height` to be 50px instead of 100px so we can assert
|
||||
// that the scroll position is proportionally preserved, that is,
|
||||
// instead of 40px from the top of item 2, it should be 20px, since the
|
||||
// item's height has been halved.
|
||||
item_height.set(50);
|
||||
state.remeasure();
|
||||
|
||||
cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| view);
|
||||
|
||||
let offset = state.logical_scroll_top();
|
||||
assert_eq!(offset.item_ix, 2);
|
||||
assert_eq!(offset.offset_in_item, px(20.));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, TruncateFrom, WhiteSpace, Window, WrappedLine,
|
||||
WrappedLineLayout, register_tooltip_mouse_handlers, set_tooltip_on_window,
|
||||
TextRun, TextStyle, TooltipId, 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_affix, truncate_from) =
|
||||
let (truncate_width, truncation_suffix) =
|
||||
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,24 +365,17 @@ impl TextLayout {
|
||||
});
|
||||
|
||||
match text_overflow {
|
||||
TextOverflow::Truncate(s) => (width, s, TruncateFrom::End),
|
||||
TextOverflow::TruncateStart(s) => (width, s, TruncateFrom::Start),
|
||||
TextOverflow::Truncate(s) => (width, s),
|
||||
}
|
||||
} else {
|
||||
(None, "".into(), TruncateFrom::End)
|
||||
(None, "".into())
|
||||
};
|
||||
|
||||
// Only use cached layout if:
|
||||
// 1. We have a cached size
|
||||
// 2. wrap_width matches (or both are None)
|
||||
// 3. truncate_width is None (if truncate_width is Some, we need to re-layout
|
||||
// because the previous layout may have been computed without truncation)
|
||||
if let Some(text_layout) = element_state.0.borrow().as_ref()
|
||||
&& let Some(size) = text_layout.size
|
||||
&& text_layout.size.is_some()
|
||||
&& (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
|
||||
&& truncate_width.is_none()
|
||||
{
|
||||
return size;
|
||||
return text_layout.size.unwrap();
|
||||
}
|
||||
|
||||
let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
|
||||
@@ -390,9 +383,8 @@ impl TextLayout {
|
||||
line_wrapper.truncate_line(
|
||||
text.clone(),
|
||||
truncate_width,
|
||||
&truncation_affix,
|
||||
&truncation_suffix,
|
||||
&runs,
|
||||
truncate_from,
|
||||
)
|
||||
} else {
|
||||
(text.clone(), Cow::Borrowed(&*runs))
|
||||
|
||||
@@ -262,18 +262,12 @@ pub(crate) trait Platform: 'static {
|
||||
fn set_cursor_style(&self, style: CursorStyle);
|
||||
fn should_auto_hide_scrollbars(&self) -> bool;
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem>;
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn write_to_primary(&self, item: ClipboardItem);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
|
||||
#[cfg(target_os = "macos")]
|
||||
fn write_to_find_pasteboard(&self, item: ClipboardItem);
|
||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem>;
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
|
||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
||||
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
||||
@@ -1354,10 +1348,6 @@ pub enum WindowKind {
|
||||
/// docks, notifications or wallpapers.
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
LayerShell(layer_shell::LayerShellOptions),
|
||||
|
||||
/// A window that appears on top of its parent window and blocks interaction with it
|
||||
/// until the modal window is closed
|
||||
Dialog,
|
||||
}
|
||||
|
||||
/// The appearance of the window, as defined by the operating system.
|
||||
|
||||
@@ -36,6 +36,12 @@ use wayland_client::{
|
||||
wl_shm_pool, wl_surface,
|
||||
},
|
||||
};
|
||||
use wayland_protocols::wp::cursor_shape::v1::client::{
|
||||
wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1,
|
||||
};
|
||||
use wayland_protocols::wp::fractional_scale::v1::client::{
|
||||
wp_fractional_scale_manager_v1, wp_fractional_scale_v1,
|
||||
};
|
||||
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{
|
||||
self, ZwpPrimarySelectionOfferV1,
|
||||
};
|
||||
@@ -55,14 +61,6 @@ use wayland_protocols::xdg::decoration::zv1::client::{
|
||||
zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
|
||||
};
|
||||
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
|
||||
use wayland_protocols::{
|
||||
wp::cursor_shape::v1::client::{wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1},
|
||||
xdg::dialog::v1::client::xdg_wm_dialog_v1::{self, XdgWmDialogV1},
|
||||
};
|
||||
use wayland_protocols::{
|
||||
wp::fractional_scale::v1::client::{wp_fractional_scale_manager_v1, wp_fractional_scale_v1},
|
||||
xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,
|
||||
};
|
||||
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
|
||||
use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
|
||||
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
|
||||
@@ -124,7 +122,6 @@ pub struct Globals {
|
||||
pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
|
||||
pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
|
||||
pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
|
||||
pub dialog: Option<xdg_wm_dialog_v1::XdgWmDialogV1>,
|
||||
pub executor: ForegroundExecutor,
|
||||
}
|
||||
|
||||
@@ -135,7 +132,6 @@ impl Globals {
|
||||
qh: QueueHandle<WaylandClientStatePtr>,
|
||||
seat: wl_seat::WlSeat,
|
||||
) -> Self {
|
||||
let dialog_v = XdgWmDialogV1::interface().version;
|
||||
Globals {
|
||||
activation: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
compositor: globals
|
||||
@@ -164,7 +160,6 @@ impl Globals {
|
||||
layer_shell: globals.bind(&qh, 1..=5, ()).ok(),
|
||||
blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
text_input_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(),
|
||||
executor,
|
||||
qh,
|
||||
}
|
||||
@@ -734,7 +729,10 @@ impl LinuxClient for WaylandClient {
|
||||
) -> anyhow::Result<Box<dyn PlatformWindow>> {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let parent = state.keyboard_focused_window.clone();
|
||||
let parent = state
|
||||
.keyboard_focused_window
|
||||
.as_ref()
|
||||
.and_then(|w| w.toplevel());
|
||||
|
||||
let (window, surface_id) = WaylandWindow::new(
|
||||
handle,
|
||||
@@ -753,12 +751,7 @@ impl LinuxClient for WaylandClient {
|
||||
fn set_cursor_style(&self, style: CursorStyle) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let need_update = state.cursor_style != Some(style)
|
||||
&& (state.mouse_focused_window.is_none()
|
||||
|| state
|
||||
.mouse_focused_window
|
||||
.as_ref()
|
||||
.is_some_and(|w| !w.is_blocked()));
|
||||
let need_update = state.cursor_style != Some(style);
|
||||
|
||||
if need_update {
|
||||
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
|
||||
@@ -1018,7 +1011,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_window(
|
||||
fn get_window(
|
||||
mut state: &mut RefMut<WaylandClientState>,
|
||||
surface_id: &ObjectId,
|
||||
) -> Option<WaylandWindowStatePtr> {
|
||||
@@ -1661,30 +1654,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
|
||||
|
||||
if let Some(window) = state.mouse_focused_window.clone() {
|
||||
if window.is_blocked() {
|
||||
let default_style = CursorStyle::Arrow;
|
||||
if state.cursor_style != Some(default_style) {
|
||||
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
|
||||
state.cursor_style = Some(default_style);
|
||||
|
||||
if let Some(cursor_shape_device) = &state.cursor_shape_device {
|
||||
cursor_shape_device.set_shape(serial, default_style.to_shape());
|
||||
} else {
|
||||
// cursor-shape-v1 isn't supported, set the cursor using a surface.
|
||||
let wl_pointer = state
|
||||
.wl_pointer
|
||||
.clone()
|
||||
.expect("window is focused by pointer");
|
||||
let scale = window.primary_output_scale();
|
||||
state.cursor.set_icon(
|
||||
&wl_pointer,
|
||||
serial,
|
||||
default_style.to_icon_names(),
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if state
|
||||
.keyboard_focused_window
|
||||
.as_ref()
|
||||
@@ -2256,27 +2225,3 @@ impl Dispatch<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, ()>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<XdgWmDialogV1, ()> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
_: &XdgWmDialogV1,
|
||||
_: <XdgWmDialogV1 as Proxy>::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<XdgDialogV1, ()> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &XdgDialogV1,
|
||||
_event: <XdgDialogV1 as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::{
|
||||
};
|
||||
|
||||
use blade_graphics as gpu;
|
||||
use collections::{FxHashSet, HashMap};
|
||||
use collections::HashMap;
|
||||
use futures::channel::oneshot::Receiver;
|
||||
|
||||
use raw_window_handle as rwh;
|
||||
@@ -20,7 +20,7 @@ use wayland_protocols::xdg::shell::client::xdg_surface;
|
||||
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self};
|
||||
use wayland_protocols::{
|
||||
wp::fractional_scale::v1::client::wp_fractional_scale_v1,
|
||||
xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,
|
||||
xdg::shell::client::xdg_toplevel::XdgToplevel,
|
||||
};
|
||||
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
|
||||
use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1;
|
||||
@@ -29,7 +29,7 @@ use crate::{
|
||||
AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
|
||||
PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
|
||||
ResizeEdge, Size, Tiling, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance,
|
||||
WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams, get_window,
|
||||
WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams,
|
||||
layer_shell::LayerShellNotSupportedError, px, size,
|
||||
};
|
||||
use crate::{
|
||||
@@ -87,8 +87,6 @@ struct InProgressConfigure {
|
||||
pub struct WaylandWindowState {
|
||||
surface_state: WaylandSurfaceState,
|
||||
acknowledged_first_configure: bool,
|
||||
parent: Option<WaylandWindowStatePtr>,
|
||||
children: FxHashSet<ObjectId>,
|
||||
pub surface: wl_surface::WlSurface,
|
||||
app_id: Option<String>,
|
||||
appearance: WindowAppearance,
|
||||
@@ -128,7 +126,7 @@ impl WaylandSurfaceState {
|
||||
surface: &wl_surface::WlSurface,
|
||||
globals: &Globals,
|
||||
params: &WindowParams,
|
||||
parent: Option<WaylandWindowStatePtr>,
|
||||
parent: Option<XdgToplevel>,
|
||||
) -> anyhow::Result<Self> {
|
||||
// For layer_shell windows, create a layer surface instead of an xdg surface
|
||||
if let WindowKind::LayerShell(options) = ¶ms.kind {
|
||||
@@ -180,28 +178,10 @@ impl WaylandSurfaceState {
|
||||
.get_xdg_surface(&surface, &globals.qh, surface.id());
|
||||
|
||||
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
|
||||
let xdg_parent = parent.as_ref().and_then(|w| w.toplevel());
|
||||
|
||||
if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog {
|
||||
toplevel.set_parent(xdg_parent.as_ref());
|
||||
if params.kind == WindowKind::Floating {
|
||||
toplevel.set_parent(parent.as_ref());
|
||||
}
|
||||
|
||||
let dialog = if params.kind == WindowKind::Dialog {
|
||||
let dialog = globals.dialog.as_ref().map(|dialog| {
|
||||
let xdg_dialog = dialog.get_xdg_dialog(&toplevel, &globals.qh, ());
|
||||
xdg_dialog.set_modal();
|
||||
xdg_dialog
|
||||
});
|
||||
|
||||
if let Some(parent) = parent.as_ref() {
|
||||
parent.add_child(surface.id());
|
||||
}
|
||||
|
||||
dialog
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(size) = params.window_min_size {
|
||||
toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
|
||||
}
|
||||
@@ -218,7 +198,6 @@ impl WaylandSurfaceState {
|
||||
xdg_surface,
|
||||
toplevel,
|
||||
decoration,
|
||||
dialog,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -227,7 +206,6 @@ pub struct WaylandXdgSurfaceState {
|
||||
xdg_surface: xdg_surface::XdgSurface,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||
dialog: Option<XdgDialogV1>,
|
||||
}
|
||||
|
||||
pub struct WaylandLayerSurfaceState {
|
||||
@@ -280,13 +258,7 @@ impl WaylandSurfaceState {
|
||||
xdg_surface,
|
||||
toplevel,
|
||||
decoration: _decoration,
|
||||
dialog,
|
||||
}) => {
|
||||
// drop the dialog before toplevel so compositor can explicitly unapply it's effects
|
||||
if let Some(dialog) = dialog {
|
||||
dialog.destroy();
|
||||
}
|
||||
|
||||
// The role object (toplevel) must always be destroyed before the xdg_surface.
|
||||
// See https://wayland.app/protocols/xdg-shell#xdg_surface:request:destroy
|
||||
toplevel.destroy();
|
||||
@@ -316,7 +288,6 @@ impl WaylandWindowState {
|
||||
globals: Globals,
|
||||
gpu_context: &BladeContext,
|
||||
options: WindowParams,
|
||||
parent: Option<WaylandWindowStatePtr>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let renderer = {
|
||||
let raw_window = RawWindow {
|
||||
@@ -348,8 +319,6 @@ impl WaylandWindowState {
|
||||
Ok(Self {
|
||||
surface_state,
|
||||
acknowledged_first_configure: false,
|
||||
parent,
|
||||
children: FxHashSet::default(),
|
||||
surface,
|
||||
app_id: None,
|
||||
blur: None,
|
||||
@@ -422,10 +391,6 @@ impl Drop for WaylandWindow {
|
||||
fn drop(&mut self) {
|
||||
let mut state = self.0.state.borrow_mut();
|
||||
let surface_id = state.surface.id();
|
||||
if let Some(parent) = state.parent.as_ref() {
|
||||
parent.state.borrow_mut().children.remove(&surface_id);
|
||||
}
|
||||
|
||||
let client = state.client.clone();
|
||||
|
||||
state.renderer.destroy();
|
||||
@@ -483,10 +448,10 @@ impl WaylandWindow {
|
||||
client: WaylandClientStatePtr,
|
||||
params: WindowParams,
|
||||
appearance: WindowAppearance,
|
||||
parent: Option<WaylandWindowStatePtr>,
|
||||
parent: Option<XdgToplevel>,
|
||||
) -> anyhow::Result<(Self, ObjectId)> {
|
||||
let surface = globals.compositor.create_surface(&globals.qh, ());
|
||||
let surface_state = WaylandSurfaceState::new(&surface, &globals, ¶ms, parent.clone())?;
|
||||
let surface_state = WaylandSurfaceState::new(&surface, &globals, ¶ms, parent)?;
|
||||
|
||||
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
|
||||
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
|
||||
@@ -508,7 +473,6 @@ impl WaylandWindow {
|
||||
globals,
|
||||
gpu_context,
|
||||
params,
|
||||
parent,
|
||||
)?)),
|
||||
callbacks: Rc::new(RefCell::new(Callbacks::default())),
|
||||
});
|
||||
@@ -537,16 +501,6 @@ impl WaylandWindowStatePtr {
|
||||
Rc::ptr_eq(&self.state, &other.state)
|
||||
}
|
||||
|
||||
pub fn add_child(&self, child: ObjectId) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.children.insert(child);
|
||||
}
|
||||
|
||||
pub fn is_blocked(&self) -> bool {
|
||||
let state = self.state.borrow();
|
||||
!state.children.is_empty()
|
||||
}
|
||||
|
||||
pub fn frame(&self) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.surface.frame(&state.globals.qh, state.surface.id());
|
||||
@@ -864,9 +818,6 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn handle_ime(&self, ime: ImeInput) {
|
||||
if self.is_blocked() {
|
||||
return;
|
||||
}
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
@@ -943,21 +894,6 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
let state = self.state.borrow();
|
||||
let client = state.client.get_client();
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let children = state.children.clone();
|
||||
drop(state);
|
||||
|
||||
for child in children {
|
||||
let mut client_state = client.borrow_mut();
|
||||
let window = get_window(&mut client_state, &child);
|
||||
drop(client_state);
|
||||
|
||||
if let Some(child) = window {
|
||||
child.close();
|
||||
}
|
||||
}
|
||||
let mut callbacks = self.callbacks.borrow_mut();
|
||||
if let Some(fun) = callbacks.close.take() {
|
||||
fun()
|
||||
@@ -965,9 +901,6 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn handle_input(&self, input: PlatformInput) {
|
||||
if self.is_blocked() {
|
||||
return;
|
||||
}
|
||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().input
|
||||
&& !fun(input.clone()).propagate
|
||||
{
|
||||
@@ -1092,26 +1025,13 @@ impl PlatformWindow for WaylandWindow {
|
||||
fn resize(&mut self, size: Size<Pixels>) {
|
||||
let state = self.borrow();
|
||||
let state_ptr = self.0.clone();
|
||||
|
||||
// Keep window geometry consistent with configure handling. On Wayland, window geometry is
|
||||
// surface-local: resizing should not attempt to translate the window; the compositor
|
||||
// controls placement. We also account for client-side decoration insets and tiling.
|
||||
let window_geometry = inset_by_tiling(
|
||||
Bounds {
|
||||
origin: Point::default(),
|
||||
size,
|
||||
},
|
||||
state.inset(),
|
||||
state.tiling,
|
||||
)
|
||||
.map(|v| v.0 as i32)
|
||||
.map_size(|v| if v <= 0 { 1 } else { v });
|
||||
let dp_size = size.to_device_pixels(self.scale_factor());
|
||||
|
||||
state.surface_state.set_geometry(
|
||||
window_geometry.origin.x,
|
||||
window_geometry.origin.y,
|
||||
window_geometry.size.width,
|
||||
window_geometry.size.height,
|
||||
state.bounds.origin.x.0 as i32,
|
||||
state.bounds.origin.y.0 as i32,
|
||||
dp_size.width.0,
|
||||
dp_size.height.0,
|
||||
);
|
||||
|
||||
state
|
||||
|
||||
@@ -222,7 +222,7 @@ pub struct X11ClientState {
|
||||
pub struct X11ClientStatePtr(pub Weak<RefCell<X11ClientState>>);
|
||||
|
||||
impl X11ClientStatePtr {
|
||||
pub fn get_client(&self) -> Option<X11Client> {
|
||||
fn get_client(&self) -> Option<X11Client> {
|
||||
self.0.upgrade().map(X11Client)
|
||||
}
|
||||
|
||||
@@ -752,7 +752,7 @@ impl X11Client {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
|
||||
fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
|
||||
let state = self.0.borrow();
|
||||
state
|
||||
.windows
|
||||
@@ -789,12 +789,12 @@ impl X11Client {
|
||||
let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32();
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
if atom == state.atoms.WM_DELETE_WINDOW && window.should_close() {
|
||||
if atom == state.atoms.WM_DELETE_WINDOW {
|
||||
// window "x" button clicked by user
|
||||
// Rest of the close logic is handled in drop_window()
|
||||
drop(state);
|
||||
window.close();
|
||||
state = self.0.borrow_mut();
|
||||
if window.should_close() {
|
||||
// Rest of the close logic is handled in drop_window()
|
||||
window.close();
|
||||
}
|
||||
} else if atom == state.atoms._NET_WM_SYNC_REQUEST {
|
||||
window.state.borrow_mut().last_sync_counter =
|
||||
Some(x11rb::protocol::sync::Int64 {
|
||||
@@ -1216,33 +1216,6 @@ impl X11Client {
|
||||
Event::XinputMotion(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
let mut state = self.0.borrow_mut();
|
||||
if window.is_blocked() {
|
||||
// We want to set the cursor to the default arrow
|
||||
// when the window is blocked
|
||||
let style = CursorStyle::Arrow;
|
||||
|
||||
let current_style = state
|
||||
.cursor_styles
|
||||
.get(&window.x_window)
|
||||
.unwrap_or(&CursorStyle::Arrow);
|
||||
if *current_style != style
|
||||
&& let Some(cursor) = state.get_cursor_icon(style)
|
||||
{
|
||||
state.cursor_styles.insert(window.x_window, style);
|
||||
check_reply(
|
||||
|| "Failed to set cursor style",
|
||||
state.xcb_connection.change_window_attributes(
|
||||
window.x_window,
|
||||
&ChangeWindowAttributesAux {
|
||||
cursor: Some(cursor),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
)
|
||||
.log_err();
|
||||
state.xcb_connection.flush().log_err();
|
||||
};
|
||||
}
|
||||
let pressed_button = pressed_button_from_mask(event.button_mask[0]);
|
||||
let position = point(
|
||||
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
@@ -1516,7 +1489,7 @@ impl LinuxClient for X11Client {
|
||||
let parent_window = state
|
||||
.keyboard_focused_window
|
||||
.and_then(|focused_window| state.windows.get(&focused_window))
|
||||
.map(|w| w.window.clone());
|
||||
.map(|window| window.window.x_window);
|
||||
let x_window = state
|
||||
.xcb_connection
|
||||
.generate_id()
|
||||
@@ -1571,15 +1544,7 @@ impl LinuxClient for X11Client {
|
||||
.cursor_styles
|
||||
.get(&focused_window)
|
||||
.unwrap_or(&CursorStyle::Arrow);
|
||||
|
||||
let window = state
|
||||
.mouse_focused_window
|
||||
.and_then(|w| state.windows.get(&w));
|
||||
|
||||
let should_change = *current_style != style
|
||||
&& (window.is_none() || window.is_some_and(|w| !w.is_blocked()));
|
||||
|
||||
if !should_change {
|
||||
if *current_style == style {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ use crate::{
|
||||
};
|
||||
|
||||
use blade_graphics as gpu;
|
||||
use collections::FxHashSet;
|
||||
use raw_window_handle as rwh;
|
||||
use util::{ResultExt, maybe};
|
||||
use x11rb::{
|
||||
@@ -75,7 +74,6 @@ x11rb::atom_manager! {
|
||||
_NET_WM_WINDOW_TYPE,
|
||||
_NET_WM_WINDOW_TYPE_NOTIFICATION,
|
||||
_NET_WM_WINDOW_TYPE_DIALOG,
|
||||
_NET_WM_STATE_MODAL,
|
||||
_NET_WM_SYNC,
|
||||
_NET_SUPPORTED,
|
||||
_MOTIF_WM_HINTS,
|
||||
@@ -251,8 +249,6 @@ pub struct Callbacks {
|
||||
|
||||
pub struct X11WindowState {
|
||||
pub destroyed: bool,
|
||||
parent: Option<X11WindowStatePtr>,
|
||||
children: FxHashSet<xproto::Window>,
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
atoms: XcbAtoms,
|
||||
@@ -398,7 +394,7 @@ impl X11WindowState {
|
||||
atoms: &XcbAtoms,
|
||||
scale_factor: f32,
|
||||
appearance: WindowAppearance,
|
||||
parent_window: Option<X11WindowStatePtr>,
|
||||
parent_window: Option<xproto::Window>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let x_screen_index = params
|
||||
.display_id
|
||||
@@ -550,8 +546,8 @@ impl X11WindowState {
|
||||
)?;
|
||||
}
|
||||
|
||||
if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog {
|
||||
if let Some(parent_window) = parent_window.as_ref().map(|w| w.x_window) {
|
||||
if params.kind == WindowKind::Floating {
|
||||
if let Some(parent_window) = parent_window {
|
||||
// WM_TRANSIENT_FOR hint indicating the main application window. For floating windows, we set
|
||||
// a parent window (WM_TRANSIENT_FOR) such that the window manager knows where to
|
||||
// place the floating window in relation to the main window.
|
||||
@@ -567,23 +563,11 @@ impl X11WindowState {
|
||||
),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
let parent = if params.kind == WindowKind::Dialog
|
||||
&& let Some(parent) = parent_window
|
||||
{
|
||||
parent.add_child(x_window);
|
||||
|
||||
Some(parent)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if params.kind == WindowKind::Dialog {
|
||||
// _NET_WM_WINDOW_TYPE_DIALOG indicates that this is a dialog (floating) window
|
||||
// https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html
|
||||
check_reply(
|
||||
|| "X11 ChangeProperty32 setting window type for dialog window failed.",
|
||||
|| "X11 ChangeProperty32 setting window type for floating window failed.",
|
||||
xcb.change_property32(
|
||||
xproto::PropMode::REPLACE,
|
||||
x_window,
|
||||
@@ -592,20 +576,6 @@ impl X11WindowState {
|
||||
&[atoms._NET_WM_WINDOW_TYPE_DIALOG],
|
||||
),
|
||||
)?;
|
||||
|
||||
// We set the modal state for dialog windows, so that the window manager
|
||||
// can handle it appropriately (e.g., prevent interaction with the parent window
|
||||
// while the dialog is open).
|
||||
check_reply(
|
||||
|| "X11 ChangeProperty32 setting modal state for dialog window failed.",
|
||||
xcb.change_property32(
|
||||
xproto::PropMode::REPLACE,
|
||||
x_window,
|
||||
atoms._NET_WM_STATE,
|
||||
xproto::AtomEnum::ATOM,
|
||||
&[atoms._NET_WM_STATE_MODAL],
|
||||
),
|
||||
)?;
|
||||
}
|
||||
|
||||
check_reply(
|
||||
@@ -697,8 +667,6 @@ impl X11WindowState {
|
||||
let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?);
|
||||
|
||||
Ok(Self {
|
||||
parent,
|
||||
children: FxHashSet::default(),
|
||||
client,
|
||||
executor,
|
||||
display,
|
||||
@@ -752,11 +720,6 @@ pub(crate) struct X11Window(pub X11WindowStatePtr);
|
||||
impl Drop for X11Window {
|
||||
fn drop(&mut self) {
|
||||
let mut state = self.0.state.borrow_mut();
|
||||
|
||||
if let Some(parent) = state.parent.as_ref() {
|
||||
parent.state.borrow_mut().children.remove(&self.0.x_window);
|
||||
}
|
||||
|
||||
state.renderer.destroy();
|
||||
|
||||
let destroy_x_window = maybe!({
|
||||
@@ -771,6 +734,8 @@ impl Drop for X11Window {
|
||||
.log_err();
|
||||
|
||||
if destroy_x_window.is_some() {
|
||||
// Mark window as destroyed so that we can filter out when X11 events
|
||||
// for it still come in.
|
||||
state.destroyed = true;
|
||||
|
||||
let this_ptr = self.0.clone();
|
||||
@@ -808,7 +773,7 @@ impl X11Window {
|
||||
atoms: &XcbAtoms,
|
||||
scale_factor: f32,
|
||||
appearance: WindowAppearance,
|
||||
parent_window: Option<X11WindowStatePtr>,
|
||||
parent_window: Option<xproto::Window>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let ptr = X11WindowStatePtr {
|
||||
state: Rc::new(RefCell::new(X11WindowState::new(
|
||||
@@ -1014,31 +979,7 @@ impl X11WindowStatePtr {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_child(&self, child: xproto::Window) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.children.insert(child);
|
||||
}
|
||||
|
||||
pub fn is_blocked(&self) -> bool {
|
||||
let state = self.state.borrow();
|
||||
!state.children.is_empty()
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
let state = self.state.borrow();
|
||||
let client = state.client.clone();
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let children = state.children.clone();
|
||||
drop(state);
|
||||
|
||||
if let Some(client) = client.get_client() {
|
||||
for child in children {
|
||||
if let Some(child_window) = client.get_window(child) {
|
||||
child_window.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut callbacks = self.callbacks.borrow_mut();
|
||||
if let Some(fun) = callbacks.close.take() {
|
||||
fun()
|
||||
@@ -1053,9 +994,6 @@ impl X11WindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn handle_input(&self, input: PlatformInput) {
|
||||
if self.is_blocked() {
|
||||
return;
|
||||
}
|
||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().input
|
||||
&& !fun(input.clone()).propagate
|
||||
{
|
||||
@@ -1078,9 +1016,6 @@ impl X11WindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn handle_ime_commit(&self, text: String) {
|
||||
if self.is_blocked() {
|
||||
return;
|
||||
}
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
@@ -1091,9 +1026,6 @@ impl X11WindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn handle_ime_preedit(&self, text: String) {
|
||||
if self.is_blocked() {
|
||||
return;
|
||||
}
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
@@ -1104,9 +1036,6 @@ impl X11WindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn handle_ime_unmark(&self) {
|
||||
if self.is_blocked() {
|
||||
return;
|
||||
}
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
@@ -1117,9 +1046,6 @@ impl X11WindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn handle_ime_delete(&self) {
|
||||
if self.is_blocked() {
|
||||
return;
|
||||
}
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
|
||||
@@ -5,7 +5,6 @@ mod display;
|
||||
mod display_link;
|
||||
mod events;
|
||||
mod keyboard;
|
||||
mod pasteboard;
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
mod screen_capture;
|
||||
@@ -22,6 +21,8 @@ use metal_renderer as renderer;
|
||||
#[cfg(feature = "macos-blade")]
|
||||
use crate::platform::blade as renderer;
|
||||
|
||||
mod attributed_string;
|
||||
|
||||
#[cfg(feature = "font-kit")]
|
||||
mod open_type;
|
||||
|
||||
|
||||
129
crates/gpui/src/platform/mac/attributed_string.rs
Normal file
129
crates/gpui/src/platform/mac/attributed_string.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use cocoa::base::id;
|
||||
use cocoa::foundation::NSRange;
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
|
||||
/// The `cocoa` crate does not define NSAttributedString (and related Cocoa classes),
|
||||
/// which are needed for copying rich text (that is, text intermingled with images)
|
||||
/// to the clipboard. This adds access to those APIs.
|
||||
#[allow(non_snake_case)]
|
||||
pub trait NSAttributedString: Sized {
|
||||
unsafe fn alloc(_: Self) -> id {
|
||||
msg_send![class!(NSAttributedString), alloc]
|
||||
}
|
||||
|
||||
unsafe fn init_attributed_string(self, string: id) -> id;
|
||||
unsafe fn appendAttributedString_(self, attr_string: id);
|
||||
unsafe fn RTFDFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id;
|
||||
unsafe fn RTFFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id;
|
||||
unsafe fn string(self) -> id;
|
||||
}
|
||||
|
||||
impl NSAttributedString for id {
|
||||
unsafe fn init_attributed_string(self, string: id) -> id {
|
||||
msg_send![self, initWithString: string]
|
||||
}
|
||||
|
||||
unsafe fn appendAttributedString_(self, attr_string: id) {
|
||||
let _: () = msg_send![self, appendAttributedString: attr_string];
|
||||
}
|
||||
|
||||
unsafe fn RTFDFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id {
|
||||
msg_send![self, RTFDFromRange: range documentAttributes: attrs]
|
||||
}
|
||||
|
||||
unsafe fn RTFFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id {
|
||||
msg_send![self, RTFFromRange: range documentAttributes: attrs]
|
||||
}
|
||||
|
||||
unsafe fn string(self) -> id {
|
||||
msg_send![self, string]
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NSMutableAttributedString: NSAttributedString {
|
||||
unsafe fn alloc(_: Self) -> id {
|
||||
msg_send![class!(NSMutableAttributedString), alloc]
|
||||
}
|
||||
}
|
||||
|
||||
impl NSMutableAttributedString for id {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::platform::mac::ns_string;
|
||||
|
||||
use super::*;
|
||||
use cocoa::appkit::NSImage;
|
||||
use cocoa::base::nil;
|
||||
use cocoa::foundation::NSAutoreleasePool;
|
||||
#[test]
|
||||
#[ignore] // This was SIGSEGV-ing on CI but not locally; need to investigate https://github.com/zed-industries/zed/actions/runs/10362363230/job/28684225486?pr=15782#step:4:1348
|
||||
fn test_nsattributed_string() {
|
||||
// TODO move these to parent module once it's actually ready to be used
|
||||
#[allow(non_snake_case)]
|
||||
pub trait NSTextAttachment: Sized {
|
||||
unsafe fn alloc(_: Self) -> id {
|
||||
msg_send![class!(NSTextAttachment), alloc]
|
||||
}
|
||||
}
|
||||
|
||||
impl NSTextAttachment for id {}
|
||||
|
||||
unsafe {
|
||||
let image: id = {
|
||||
let img: id = msg_send![class!(NSImage), alloc];
|
||||
let img: id = msg_send![img, initWithContentsOfFile: ns_string("test.jpeg")];
|
||||
let img: id = msg_send![img, autorelease];
|
||||
img
|
||||
};
|
||||
let _size = image.size();
|
||||
|
||||
let string = ns_string("Test String");
|
||||
let attr_string = NSMutableAttributedString::alloc(nil)
|
||||
.init_attributed_string(string)
|
||||
.autorelease();
|
||||
let hello_string = ns_string("Hello World");
|
||||
let hello_attr_string = NSAttributedString::alloc(nil)
|
||||
.init_attributed_string(hello_string)
|
||||
.autorelease();
|
||||
attr_string.appendAttributedString_(hello_attr_string);
|
||||
|
||||
let attachment: id = msg_send![NSTextAttachment::alloc(nil), autorelease];
|
||||
let _: () = msg_send![attachment, setImage: image];
|
||||
let image_attr_string =
|
||||
msg_send![class!(NSAttributedString), attributedStringWithAttachment: attachment];
|
||||
attr_string.appendAttributedString_(image_attr_string);
|
||||
|
||||
let another_string = ns_string("Another String");
|
||||
let another_attr_string = NSAttributedString::alloc(nil)
|
||||
.init_attributed_string(another_string)
|
||||
.autorelease();
|
||||
attr_string.appendAttributedString_(another_attr_string);
|
||||
|
||||
let _len: cocoa::foundation::NSUInteger = msg_send![attr_string, length];
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// pasteboard.clearContents();
|
||||
|
||||
let rtfd_data = attr_string.RTFDFromRange_documentAttributes_(
|
||||
NSRange::new(0, msg_send![attr_string, length]),
|
||||
nil,
|
||||
);
|
||||
assert_ne!(rtfd_data, nil);
|
||||
// if rtfd_data != nil {
|
||||
// pasteboard.setData_forType(rtfd_data, NSPasteboardTypeRTFD);
|
||||
// }
|
||||
|
||||
// let rtf_data = attributed_string.RTFFromRange_documentAttributes_(
|
||||
// NSRange::new(0, attributed_string.length()),
|
||||
// nil,
|
||||
// );
|
||||
// if rtf_data != nil {
|
||||
// pasteboard.setData_forType(rtf_data, NSPasteboardTypeRTF);
|
||||
// }
|
||||
|
||||
// let plain_text = attributed_string.string();
|
||||
// pasteboard.setString_forType(plain_text, NSPasteboardTypeString);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,9 +46,9 @@ pub unsafe fn new_renderer(
|
||||
_native_window: *mut c_void,
|
||||
_native_view: *mut c_void,
|
||||
_bounds: crate::Size<f32>,
|
||||
transparent: bool,
|
||||
_transparent: bool,
|
||||
) -> Renderer {
|
||||
MetalRenderer::new(context, transparent)
|
||||
MetalRenderer::new(context)
|
||||
}
|
||||
|
||||
pub(crate) struct InstanceBufferPool {
|
||||
@@ -128,7 +128,7 @@ pub struct PathRasterizationVertex {
|
||||
}
|
||||
|
||||
impl MetalRenderer {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>, transparent: bool) -> Self {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>) -> Self {
|
||||
// Prefer low‐power integrated GPUs on Intel Mac. On Apple
|
||||
// Silicon, there is only ever one GPU, so this is equivalent to
|
||||
// `metal::Device::system_default()`.
|
||||
@@ -152,13 +152,8 @@ impl MetalRenderer {
|
||||
let layer = metal::MetalLayer::new();
|
||||
layer.set_device(&device);
|
||||
layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
|
||||
// Support direct-to-display rendering if the window is not transparent
|
||||
// https://developer.apple.com/documentation/metal/managing-your-game-window-for-metal-in-macos
|
||||
layer.set_opaque(!transparent);
|
||||
layer.set_opaque(false);
|
||||
layer.set_maximum_drawable_count(3);
|
||||
// We already present at display sync with the display link
|
||||
// This allows to use direct-to-display even in window mode
|
||||
layer.set_display_sync_enabled(false);
|
||||
unsafe {
|
||||
let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
|
||||
let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
|
||||
@@ -357,8 +352,8 @@ impl MetalRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_transparency(&self, transparent: bool) {
|
||||
self.layer.set_opaque(!transparent);
|
||||
pub fn update_transparency(&self, _transparent: bool) {
|
||||
// todo(mac)?
|
||||
}
|
||||
|
||||
pub fn destroy(&self) {
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
use core::slice;
|
||||
use std::ffi::c_void;
|
||||
|
||||
use cocoa::{
|
||||
appkit::{NSPasteboard, NSPasteboardTypePNG, NSPasteboardTypeString, NSPasteboardTypeTIFF},
|
||||
base::{id, nil},
|
||||
foundation::NSData,
|
||||
};
|
||||
use objc::{msg_send, runtime::Object, sel, sel_impl};
|
||||
use strum::IntoEnumIterator as _;
|
||||
|
||||
use crate::{
|
||||
ClipboardEntry, ClipboardItem, ClipboardString, Image, ImageFormat, asset_cache::hash,
|
||||
platform::mac::ns_string,
|
||||
};
|
||||
|
||||
pub struct Pasteboard {
|
||||
inner: id,
|
||||
text_hash_type: id,
|
||||
metadata_type: id,
|
||||
}
|
||||
|
||||
impl Pasteboard {
|
||||
pub fn general() -> Self {
|
||||
unsafe { Self::new(NSPasteboard::generalPasteboard(nil)) }
|
||||
}
|
||||
|
||||
pub fn find() -> Self {
|
||||
unsafe { Self::new(NSPasteboard::pasteboardWithName(nil, NSPasteboardNameFind)) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn unique() -> Self {
|
||||
unsafe { Self::new(NSPasteboard::pasteboardWithUniqueName(nil)) }
|
||||
}
|
||||
|
||||
unsafe fn new(inner: id) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
text_hash_type: unsafe { ns_string("zed-text-hash") },
|
||||
metadata_type: unsafe { ns_string("zed-metadata") },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self) -> Option<ClipboardItem> {
|
||||
// First, see if it's a string.
|
||||
unsafe {
|
||||
let pasteboard_types: id = self.inner.types();
|
||||
let string_type: id = ns_string("public.utf8-plain-text");
|
||||
|
||||
if msg_send![pasteboard_types, containsObject: string_type] {
|
||||
let data = self.inner.dataForType(string_type);
|
||||
if data == nil {
|
||||
return None;
|
||||
} else if data.bytes().is_null() {
|
||||
// https://developer.apple.com/documentation/foundation/nsdata/1410616-bytes?language=objc
|
||||
// "If the length of the NSData object is 0, this property returns nil."
|
||||
return Some(self.read_string(&[]));
|
||||
} else {
|
||||
let bytes =
|
||||
slice::from_raw_parts(data.bytes() as *mut u8, data.length() as usize);
|
||||
|
||||
return Some(self.read_string(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
// If it wasn't a string, try the various supported image types.
|
||||
for format in ImageFormat::iter() {
|
||||
if let Some(item) = self.read_image(format) {
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If it wasn't a string or a supported image type, give up.
|
||||
None
|
||||
}
|
||||
|
||||
fn read_image(&self, format: ImageFormat) -> Option<ClipboardItem> {
|
||||
let mut ut_type: UTType = format.into();
|
||||
|
||||
unsafe {
|
||||
let types: id = self.inner.types();
|
||||
if msg_send![types, containsObject: ut_type.inner()] {
|
||||
self.data_for_type(ut_type.inner_mut()).map(|bytes| {
|
||||
let bytes = bytes.to_vec();
|
||||
let id = hash(&bytes);
|
||||
|
||||
ClipboardItem {
|
||||
entries: vec![ClipboardEntry::Image(Image { format, bytes, id })],
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_string(&self, text_bytes: &[u8]) -> ClipboardItem {
|
||||
unsafe {
|
||||
let text = String::from_utf8_lossy(text_bytes).to_string();
|
||||
let metadata = self
|
||||
.data_for_type(self.text_hash_type)
|
||||
.and_then(|hash_bytes| {
|
||||
let hash_bytes = hash_bytes.try_into().ok()?;
|
||||
let hash = u64::from_be_bytes(hash_bytes);
|
||||
let metadata = self.data_for_type(self.metadata_type)?;
|
||||
|
||||
if hash == ClipboardString::text_hash(&text) {
|
||||
String::from_utf8(metadata.to_vec()).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
ClipboardItem {
|
||||
entries: vec![ClipboardEntry::String(ClipboardString { text, metadata })],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn data_for_type(&self, kind: id) -> Option<&[u8]> {
|
||||
unsafe {
|
||||
let data = self.inner.dataForType(kind);
|
||||
if data == nil {
|
||||
None
|
||||
} else {
|
||||
Some(slice::from_raw_parts(
|
||||
data.bytes() as *mut u8,
|
||||
data.length() as usize,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&self, item: ClipboardItem) {
|
||||
unsafe {
|
||||
match item.entries.as_slice() {
|
||||
[] => {
|
||||
// Writing an empty list of entries just clears the clipboard.
|
||||
self.inner.clearContents();
|
||||
}
|
||||
[ClipboardEntry::String(string)] => {
|
||||
self.write_plaintext(string);
|
||||
}
|
||||
[ClipboardEntry::Image(image)] => {
|
||||
self.write_image(image);
|
||||
}
|
||||
[ClipboardEntry::ExternalPaths(_)] => {}
|
||||
_ => {
|
||||
// Agus NB: We're currently only writing string entries to the clipboard when we have more than one.
|
||||
//
|
||||
// This was the existing behavior before I refactored the outer clipboard code:
|
||||
// https://github.com/zed-industries/zed/blob/65f7412a0265552b06ce122655369d6cc7381dd6/crates/gpui/src/platform/mac/platform.rs#L1060-L1110
|
||||
//
|
||||
// Note how `any_images` is always `false`. We should fix that, but that's orthogonal to the refactor.
|
||||
|
||||
let mut combined = ClipboardString {
|
||||
text: String::new(),
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
for entry in item.entries {
|
||||
match entry {
|
||||
ClipboardEntry::String(text) => {
|
||||
combined.text.push_str(&text.text());
|
||||
if combined.metadata.is_none() {
|
||||
combined.metadata = text.metadata;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
self.write_plaintext(&combined);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_plaintext(&self, string: &ClipboardString) {
|
||||
unsafe {
|
||||
self.inner.clearContents();
|
||||
|
||||
let text_bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
string.text.as_ptr() as *const c_void,
|
||||
string.text.len() as u64,
|
||||
);
|
||||
self.inner
|
||||
.setData_forType(text_bytes, NSPasteboardTypeString);
|
||||
|
||||
if let Some(metadata) = string.metadata.as_ref() {
|
||||
let hash_bytes = ClipboardString::text_hash(&string.text).to_be_bytes();
|
||||
let hash_bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
hash_bytes.as_ptr() as *const c_void,
|
||||
hash_bytes.len() as u64,
|
||||
);
|
||||
self.inner.setData_forType(hash_bytes, self.text_hash_type);
|
||||
|
||||
let metadata_bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
metadata.as_ptr() as *const c_void,
|
||||
metadata.len() as u64,
|
||||
);
|
||||
self.inner
|
||||
.setData_forType(metadata_bytes, self.metadata_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn write_image(&self, image: &Image) {
|
||||
unsafe {
|
||||
self.inner.clearContents();
|
||||
|
||||
let bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
image.bytes.as_ptr() as *const c_void,
|
||||
image.bytes.len() as u64,
|
||||
);
|
||||
|
||||
self.inner
|
||||
.setData_forType(bytes, Into::<UTType>::into(image.format).inner_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[link(name = "AppKit", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
/// [Apple's documentation](https://developer.apple.com/documentation/appkit/nspasteboardnamefind?language=objc)
|
||||
pub static NSPasteboardNameFind: id;
|
||||
}
|
||||
|
||||
impl From<ImageFormat> for UTType {
|
||||
fn from(value: ImageFormat) -> Self {
|
||||
match value {
|
||||
ImageFormat::Png => Self::png(),
|
||||
ImageFormat::Jpeg => Self::jpeg(),
|
||||
ImageFormat::Tiff => Self::tiff(),
|
||||
ImageFormat::Webp => Self::webp(),
|
||||
ImageFormat::Gif => Self::gif(),
|
||||
ImageFormat::Bmp => Self::bmp(),
|
||||
ImageFormat::Svg => Self::svg(),
|
||||
ImageFormat::Ico => Self::ico(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/
|
||||
pub struct UTType(id);
|
||||
|
||||
impl UTType {
|
||||
pub fn png() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/png
|
||||
Self(unsafe { NSPasteboardTypePNG }) // This is a rare case where there's a built-in NSPasteboardType
|
||||
}
|
||||
|
||||
pub fn jpeg() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/jpeg
|
||||
Self(unsafe { ns_string("public.jpeg") })
|
||||
}
|
||||
|
||||
pub fn gif() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/gif
|
||||
Self(unsafe { ns_string("com.compuserve.gif") })
|
||||
}
|
||||
|
||||
pub fn webp() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/webp
|
||||
Self(unsafe { ns_string("org.webmproject.webp") })
|
||||
}
|
||||
|
||||
pub fn bmp() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/bmp
|
||||
Self(unsafe { ns_string("com.microsoft.bmp") })
|
||||
}
|
||||
|
||||
pub fn svg() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/svg
|
||||
Self(unsafe { ns_string("public.svg-image") })
|
||||
}
|
||||
|
||||
pub fn ico() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ico
|
||||
Self(unsafe { ns_string("com.microsoft.ico") })
|
||||
}
|
||||
|
||||
pub fn tiff() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/tiff
|
||||
Self(unsafe { NSPasteboardTypeTIFF }) // This is a rare case where there's a built-in NSPasteboardType
|
||||
}
|
||||
|
||||
fn inner(&self) -> *const Object {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn inner_mut(&self) -> *mut Object {
|
||||
self.0 as *mut _
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use cocoa::{appkit::NSPasteboardTypeString, foundation::NSData};
|
||||
|
||||
use crate::{ClipboardEntry, ClipboardItem, ClipboardString};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_string() {
|
||||
let pasteboard = Pasteboard::unique();
|
||||
assert_eq!(pasteboard.read(), None);
|
||||
|
||||
let item = ClipboardItem::new_string("1".to_string());
|
||||
pasteboard.write(item.clone());
|
||||
assert_eq!(pasteboard.read(), Some(item));
|
||||
|
||||
let item = ClipboardItem {
|
||||
entries: vec![ClipboardEntry::String(
|
||||
ClipboardString::new("2".to_string()).with_json_metadata(vec![3, 4]),
|
||||
)],
|
||||
};
|
||||
pasteboard.write(item.clone());
|
||||
assert_eq!(pasteboard.read(), Some(item));
|
||||
|
||||
let text_from_other_app = "text from other app";
|
||||
unsafe {
|
||||
let bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
text_from_other_app.as_ptr() as *const c_void,
|
||||
text_from_other_app.len() as u64,
|
||||
);
|
||||
pasteboard
|
||||
.inner
|
||||
.setData_forType(bytes, NSPasteboardTypeString);
|
||||
}
|
||||
assert_eq!(
|
||||
pasteboard.read(),
|
||||
Some(ClipboardItem::new_string(text_from_other_app.to_string()))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,29 @@
|
||||
use super::{
|
||||
BoolExt, MacKeyboardLayout, MacKeyboardMapper, events::key_to_native, ns_string, renderer,
|
||||
BoolExt, MacKeyboardLayout, MacKeyboardMapper,
|
||||
attributed_string::{NSAttributedString, NSMutableAttributedString},
|
||||
events::key_to_native,
|
||||
ns_string, renderer,
|
||||
};
|
||||
use crate::{
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor,
|
||||
KeyContext, Keymap, MacDispatcher, MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu,
|
||||
PathPromptOptions, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
PlatformTextSystem, PlatformWindow, Result, SystemMenuType, Task, WindowAppearance,
|
||||
WindowParams, platform::mac::pasteboard::Pasteboard,
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
|
||||
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
|
||||
MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||
PlatformWindow, Result, SystemMenuType, Task, WindowAppearance, WindowParams, hash,
|
||||
};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use block::ConcreteBlock;
|
||||
use cocoa::{
|
||||
appkit::{
|
||||
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
|
||||
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSSavePanel,
|
||||
NSVisualEffectState, NSVisualEffectView, NSWindow,
|
||||
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
|
||||
NSPasteboardTypePNG, NSPasteboardTypeRTF, NSPasteboardTypeRTFD, NSPasteboardTypeString,
|
||||
NSPasteboardTypeTIFF, NSSavePanel, NSVisualEffectState, NSVisualEffectView, NSWindow,
|
||||
},
|
||||
base::{BOOL, NO, YES, id, nil, selector},
|
||||
foundation::{
|
||||
NSArray, NSAutoreleasePool, NSBundle, NSInteger, NSProcessInfo, NSString, NSUInteger, NSURL,
|
||||
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSRange, NSString,
|
||||
NSUInteger, NSURL,
|
||||
},
|
||||
};
|
||||
use core_foundation::{
|
||||
@@ -44,6 +49,7 @@ use ptr::null_mut;
|
||||
use semver::Version;
|
||||
use std::{
|
||||
cell::Cell,
|
||||
convert::TryInto,
|
||||
ffi::{CStr, OsStr, c_void},
|
||||
os::{raw::c_char, unix::ffi::OsStrExt},
|
||||
path::{Path, PathBuf},
|
||||
@@ -52,6 +58,7 @@ use std::{
|
||||
slice, str,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use strum::IntoEnumIterator;
|
||||
use util::{
|
||||
ResultExt,
|
||||
command::{new_smol_command, new_std_command},
|
||||
@@ -157,8 +164,9 @@ pub(crate) struct MacPlatformState {
|
||||
text_system: Arc<dyn PlatformTextSystem>,
|
||||
renderer_context: renderer::Context,
|
||||
headless: bool,
|
||||
general_pasteboard: Pasteboard,
|
||||
find_pasteboard: Pasteboard,
|
||||
pasteboard: id,
|
||||
text_hash_pasteboard_type: id,
|
||||
metadata_pasteboard_type: id,
|
||||
reopen: Option<Box<dyn FnMut()>>,
|
||||
on_keyboard_layout_change: Option<Box<dyn FnMut()>>,
|
||||
quit: Option<Box<dyn FnMut()>>,
|
||||
@@ -198,8 +206,9 @@ impl MacPlatform {
|
||||
background_executor: BackgroundExecutor::new(dispatcher.clone()),
|
||||
foreground_executor: ForegroundExecutor::new(dispatcher),
|
||||
renderer_context: renderer::Context::default(),
|
||||
general_pasteboard: Pasteboard::general(),
|
||||
find_pasteboard: Pasteboard::find(),
|
||||
pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
|
||||
text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
|
||||
metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
|
||||
reopen: None,
|
||||
quit: None,
|
||||
menu_command: None,
|
||||
@@ -215,6 +224,20 @@ impl MacPlatform {
|
||||
}))
|
||||
}
|
||||
|
||||
unsafe fn read_from_pasteboard(&self, pasteboard: *mut Object, kind: id) -> Option<&[u8]> {
|
||||
unsafe {
|
||||
let data = pasteboard.dataForType(kind);
|
||||
if data == nil {
|
||||
None
|
||||
} else {
|
||||
Some(slice::from_raw_parts(
|
||||
data.bytes() as *mut u8,
|
||||
data.length() as usize,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_menu_bar(
|
||||
&self,
|
||||
menus: &Vec<Menu>,
|
||||
@@ -1011,24 +1034,119 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
use crate::ClipboardEntry;
|
||||
|
||||
unsafe {
|
||||
// We only want to use NSAttributedString if there are multiple entries to write.
|
||||
if item.entries.len() <= 1 {
|
||||
match item.entries.first() {
|
||||
Some(entry) => match entry {
|
||||
ClipboardEntry::String(string) => {
|
||||
self.write_plaintext_to_clipboard(string);
|
||||
}
|
||||
ClipboardEntry::Image(image) => {
|
||||
self.write_image_to_clipboard(image);
|
||||
}
|
||||
ClipboardEntry::ExternalPaths(_) => {}
|
||||
},
|
||||
None => {
|
||||
// Writing an empty list of entries just clears the clipboard.
|
||||
let state = self.0.lock();
|
||||
state.pasteboard.clearContents();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut any_images = false;
|
||||
let attributed_string = {
|
||||
let mut buf = NSMutableAttributedString::alloc(nil)
|
||||
// TODO can we skip this? Or at least part of it?
|
||||
.init_attributed_string(ns_string(""))
|
||||
.autorelease();
|
||||
|
||||
for entry in item.entries {
|
||||
if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry
|
||||
{
|
||||
let to_append = NSAttributedString::alloc(nil)
|
||||
.init_attributed_string(ns_string(&text))
|
||||
.autorelease();
|
||||
|
||||
buf.appendAttributedString_(to_append);
|
||||
}
|
||||
}
|
||||
|
||||
buf
|
||||
};
|
||||
|
||||
let state = self.0.lock();
|
||||
state.pasteboard.clearContents();
|
||||
|
||||
// Only set rich text clipboard types if we actually have 1+ images to include.
|
||||
if any_images {
|
||||
let rtfd_data = attributed_string.RTFDFromRange_documentAttributes_(
|
||||
NSRange::new(0, msg_send![attributed_string, length]),
|
||||
nil,
|
||||
);
|
||||
if rtfd_data != nil {
|
||||
state
|
||||
.pasteboard
|
||||
.setData_forType(rtfd_data, NSPasteboardTypeRTFD);
|
||||
}
|
||||
|
||||
let rtf_data = attributed_string.RTFFromRange_documentAttributes_(
|
||||
NSRange::new(0, attributed_string.length()),
|
||||
nil,
|
||||
);
|
||||
if rtf_data != nil {
|
||||
state
|
||||
.pasteboard
|
||||
.setData_forType(rtf_data, NSPasteboardTypeRTF);
|
||||
}
|
||||
}
|
||||
|
||||
let plain_text = attributed_string.string();
|
||||
state
|
||||
.pasteboard
|
||||
.setString_forType(plain_text, NSPasteboardTypeString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
let state = self.0.lock();
|
||||
state.general_pasteboard.read()
|
||||
}
|
||||
let pasteboard = state.pasteboard;
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
let state = self.0.lock();
|
||||
state.general_pasteboard.write(item);
|
||||
}
|
||||
// First, see if it's a string.
|
||||
unsafe {
|
||||
let types: id = pasteboard.types();
|
||||
let string_type: id = ns_string("public.utf8-plain-text");
|
||||
|
||||
fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
|
||||
let state = self.0.lock();
|
||||
state.find_pasteboard.read()
|
||||
}
|
||||
if msg_send![types, containsObject: string_type] {
|
||||
let data = pasteboard.dataForType(string_type);
|
||||
if data == nil {
|
||||
return None;
|
||||
} else if data.bytes().is_null() {
|
||||
// https://developer.apple.com/documentation/foundation/nsdata/1410616-bytes?language=objc
|
||||
// "If the length of the NSData object is 0, this property returns nil."
|
||||
return Some(self.read_string_from_clipboard(&state, &[]));
|
||||
} else {
|
||||
let bytes =
|
||||
slice::from_raw_parts(data.bytes() as *mut u8, data.length() as usize);
|
||||
|
||||
fn write_to_find_pasteboard(&self, item: ClipboardItem) {
|
||||
let state = self.0.lock();
|
||||
state.find_pasteboard.write(item);
|
||||
return Some(self.read_string_from_clipboard(&state, bytes));
|
||||
}
|
||||
}
|
||||
|
||||
// If it wasn't a string, try the various supported image types.
|
||||
for format in ImageFormat::iter() {
|
||||
if let Some(item) = try_clipboard_image(pasteboard, format) {
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If it wasn't a string or a supported image type, give up.
|
||||
None
|
||||
}
|
||||
|
||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
|
||||
@@ -1137,6 +1255,116 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
impl MacPlatform {
|
||||
unsafe fn read_string_from_clipboard(
|
||||
&self,
|
||||
state: &MacPlatformState,
|
||||
text_bytes: &[u8],
|
||||
) -> ClipboardItem {
|
||||
unsafe {
|
||||
let text = String::from_utf8_lossy(text_bytes).to_string();
|
||||
let metadata = self
|
||||
.read_from_pasteboard(state.pasteboard, state.text_hash_pasteboard_type)
|
||||
.and_then(|hash_bytes| {
|
||||
let hash_bytes = hash_bytes.try_into().ok()?;
|
||||
let hash = u64::from_be_bytes(hash_bytes);
|
||||
let metadata = self
|
||||
.read_from_pasteboard(state.pasteboard, state.metadata_pasteboard_type)?;
|
||||
|
||||
if hash == ClipboardString::text_hash(&text) {
|
||||
String::from_utf8(metadata.to_vec()).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
ClipboardItem {
|
||||
entries: vec![ClipboardEntry::String(ClipboardString { text, metadata })],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn write_plaintext_to_clipboard(&self, string: &ClipboardString) {
|
||||
unsafe {
|
||||
let state = self.0.lock();
|
||||
state.pasteboard.clearContents();
|
||||
|
||||
let text_bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
string.text.as_ptr() as *const c_void,
|
||||
string.text.len() as u64,
|
||||
);
|
||||
state
|
||||
.pasteboard
|
||||
.setData_forType(text_bytes, NSPasteboardTypeString);
|
||||
|
||||
if let Some(metadata) = string.metadata.as_ref() {
|
||||
let hash_bytes = ClipboardString::text_hash(&string.text).to_be_bytes();
|
||||
let hash_bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
hash_bytes.as_ptr() as *const c_void,
|
||||
hash_bytes.len() as u64,
|
||||
);
|
||||
state
|
||||
.pasteboard
|
||||
.setData_forType(hash_bytes, state.text_hash_pasteboard_type);
|
||||
|
||||
let metadata_bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
metadata.as_ptr() as *const c_void,
|
||||
metadata.len() as u64,
|
||||
);
|
||||
state
|
||||
.pasteboard
|
||||
.setData_forType(metadata_bytes, state.metadata_pasteboard_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn write_image_to_clipboard(&self, image: &Image) {
|
||||
unsafe {
|
||||
let state = self.0.lock();
|
||||
state.pasteboard.clearContents();
|
||||
|
||||
let bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
image.bytes.as_ptr() as *const c_void,
|
||||
image.bytes.len() as u64,
|
||||
);
|
||||
|
||||
state
|
||||
.pasteboard
|
||||
.setData_forType(bytes, Into::<UTType>::into(image.format).inner_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_clipboard_image(pasteboard: id, format: ImageFormat) -> Option<ClipboardItem> {
|
||||
let mut ut_type: UTType = format.into();
|
||||
|
||||
unsafe {
|
||||
let types: id = pasteboard.types();
|
||||
if msg_send![types, containsObject: ut_type.inner()] {
|
||||
let data = pasteboard.dataForType(ut_type.inner_mut());
|
||||
if data == nil {
|
||||
None
|
||||
} else {
|
||||
let bytes = Vec::from(slice::from_raw_parts(
|
||||
data.bytes() as *mut u8,
|
||||
data.length() as usize,
|
||||
));
|
||||
let id = hash(&bytes);
|
||||
|
||||
Some(ClipboardItem {
|
||||
entries: vec![ClipboardEntry::Image(Image { format, bytes, id })],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn path_from_objc(path: id) -> PathBuf {
|
||||
let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
|
||||
let bytes = unsafe { path.UTF8String() as *const u8 };
|
||||
@@ -1377,3 +1605,120 @@ mod security {
|
||||
pub const errSecUserCanceled: OSStatus = -128;
|
||||
pub const errSecItemNotFound: OSStatus = -25300;
|
||||
}
|
||||
|
||||
impl From<ImageFormat> for UTType {
|
||||
fn from(value: ImageFormat) -> Self {
|
||||
match value {
|
||||
ImageFormat::Png => Self::png(),
|
||||
ImageFormat::Jpeg => Self::jpeg(),
|
||||
ImageFormat::Tiff => Self::tiff(),
|
||||
ImageFormat::Webp => Self::webp(),
|
||||
ImageFormat::Gif => Self::gif(),
|
||||
ImageFormat::Bmp => Self::bmp(),
|
||||
ImageFormat::Svg => Self::svg(),
|
||||
ImageFormat::Ico => Self::ico(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/
|
||||
struct UTType(id);
|
||||
|
||||
impl UTType {
|
||||
pub fn png() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/png
|
||||
Self(unsafe { NSPasteboardTypePNG }) // This is a rare case where there's a built-in NSPasteboardType
|
||||
}
|
||||
|
||||
pub fn jpeg() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/jpeg
|
||||
Self(unsafe { ns_string("public.jpeg") })
|
||||
}
|
||||
|
||||
pub fn gif() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/gif
|
||||
Self(unsafe { ns_string("com.compuserve.gif") })
|
||||
}
|
||||
|
||||
pub fn webp() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/webp
|
||||
Self(unsafe { ns_string("org.webmproject.webp") })
|
||||
}
|
||||
|
||||
pub fn bmp() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/bmp
|
||||
Self(unsafe { ns_string("com.microsoft.bmp") })
|
||||
}
|
||||
|
||||
pub fn svg() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/svg
|
||||
Self(unsafe { ns_string("public.svg-image") })
|
||||
}
|
||||
|
||||
pub fn ico() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ico
|
||||
Self(unsafe { ns_string("com.microsoft.ico") })
|
||||
}
|
||||
|
||||
pub fn tiff() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/tiff
|
||||
Self(unsafe { NSPasteboardTypeTIFF }) // This is a rare case where there's a built-in NSPasteboardType
|
||||
}
|
||||
|
||||
fn inner(&self) -> *const Object {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn inner_mut(&self) -> *mut Object {
|
||||
self.0 as *mut _
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ClipboardItem;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_clipboard() {
|
||||
let platform = build_platform();
|
||||
assert_eq!(platform.read_from_clipboard(), None);
|
||||
|
||||
let item = ClipboardItem::new_string("1".to_string());
|
||||
platform.write_to_clipboard(item.clone());
|
||||
assert_eq!(platform.read_from_clipboard(), Some(item));
|
||||
|
||||
let item = ClipboardItem {
|
||||
entries: vec![ClipboardEntry::String(
|
||||
ClipboardString::new("2".to_string()).with_json_metadata(vec![3, 4]),
|
||||
)],
|
||||
};
|
||||
platform.write_to_clipboard(item.clone());
|
||||
assert_eq!(platform.read_from_clipboard(), Some(item));
|
||||
|
||||
let text_from_other_app = "text from other app";
|
||||
unsafe {
|
||||
let bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
text_from_other_app.as_ptr() as *const c_void,
|
||||
text_from_other_app.len() as u64,
|
||||
);
|
||||
platform
|
||||
.0
|
||||
.lock()
|
||||
.pasteboard
|
||||
.setData_forType(bytes, NSPasteboardTypeString);
|
||||
}
|
||||
assert_eq!(
|
||||
platform.read_from_clipboard(),
|
||||
Some(ClipboardItem::new_string(text_from_other_app.to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
fn build_platform() -> MacPlatform {
|
||||
let platform = MacPlatform::new(false);
|
||||
platform.0.lock().pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
|
||||
platform
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,12 +62,9 @@ static mut BLURRED_VIEW_CLASS: *const Class = ptr::null();
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
|
||||
NSWindowStyleMask::from_bits_retain(1 << 7);
|
||||
// WindowLevel const value ref: https://docs.rs/core-graphics2/0.4.1/src/core_graphics2/window_level.rs.html
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSNormalWindowLevel: NSInteger = 0;
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSFloatingWindowLevel: NSInteger = 3;
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSPopUpWindowLevel: NSInteger = 101;
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01;
|
||||
@@ -426,8 +423,6 @@ struct MacWindowState {
|
||||
select_previous_tab_callback: Option<Box<dyn FnMut()>>,
|
||||
toggle_tab_bar_callback: Option<Box<dyn FnMut()>>,
|
||||
activated_least_once: bool,
|
||||
// The parent window if this window is a sheet (Dialog kind)
|
||||
sheet_parent: Option<id>,
|
||||
}
|
||||
|
||||
impl MacWindowState {
|
||||
@@ -627,16 +622,11 @@ impl MacWindow {
|
||||
}
|
||||
|
||||
let native_window: id = match kind {
|
||||
WindowKind::Normal => {
|
||||
msg_send![WINDOW_CLASS, alloc]
|
||||
}
|
||||
WindowKind::Normal | WindowKind::Floating => msg_send![WINDOW_CLASS, alloc],
|
||||
WindowKind::PopUp => {
|
||||
style_mask |= NSWindowStyleMaskNonactivatingPanel;
|
||||
msg_send![PANEL_CLASS, alloc]
|
||||
}
|
||||
WindowKind::Floating | WindowKind::Dialog => {
|
||||
msg_send![PANEL_CLASS, alloc]
|
||||
}
|
||||
};
|
||||
|
||||
let display = display_id
|
||||
@@ -739,7 +729,6 @@ impl MacWindow {
|
||||
select_previous_tab_callback: None,
|
||||
toggle_tab_bar_callback: None,
|
||||
activated_least_once: false,
|
||||
sheet_parent: None,
|
||||
})));
|
||||
|
||||
(*native_window).set_ivar(
|
||||
@@ -790,18 +779,9 @@ impl MacWindow {
|
||||
content_view.addSubview_(native_view.autorelease());
|
||||
native_window.makeFirstResponder_(native_view);
|
||||
|
||||
let app: id = NSApplication::sharedApplication(nil);
|
||||
let main_window: id = msg_send![app, mainWindow];
|
||||
let mut sheet_parent = None;
|
||||
|
||||
match kind {
|
||||
WindowKind::Normal | WindowKind::Floating => {
|
||||
if kind == WindowKind::Floating {
|
||||
// Let the window float keep above normal windows.
|
||||
native_window.setLevel_(NSFloatingWindowLevel);
|
||||
} else {
|
||||
native_window.setLevel_(NSNormalWindowLevel);
|
||||
}
|
||||
native_window.setLevel_(NSNormalWindowLevel);
|
||||
native_window.setAcceptsMouseMovedEvents_(YES);
|
||||
|
||||
if let Some(tabbing_identifier) = tabbing_identifier {
|
||||
@@ -836,23 +816,10 @@ impl MacWindow {
|
||||
NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary
|
||||
);
|
||||
}
|
||||
WindowKind::Dialog => {
|
||||
if !main_window.is_null() {
|
||||
let parent = {
|
||||
let active_sheet: id = msg_send![main_window, attachedSheet];
|
||||
if active_sheet.is_null() {
|
||||
main_window
|
||||
} else {
|
||||
active_sheet
|
||||
}
|
||||
};
|
||||
let _: () =
|
||||
msg_send![parent, beginSheet: native_window completionHandler: nil];
|
||||
sheet_parent = Some(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
let main_window: id = msg_send![app, mainWindow];
|
||||
if allows_automatic_window_tabbing
|
||||
&& !main_window.is_null()
|
||||
&& main_window != native_window
|
||||
@@ -894,11 +861,7 @@ impl MacWindow {
|
||||
// the window position might be incorrect if the main screen (the screen that contains the window that has focus)
|
||||
// is different from the primary screen.
|
||||
NSWindow::setFrameTopLeftPoint_(native_window, window_rect.origin);
|
||||
{
|
||||
let mut window_state = window.0.lock();
|
||||
window_state.move_traffic_light();
|
||||
window_state.sheet_parent = sheet_parent;
|
||||
}
|
||||
window.0.lock().move_traffic_light();
|
||||
|
||||
pool.drain();
|
||||
|
||||
@@ -975,7 +938,6 @@ impl Drop for MacWindow {
|
||||
let mut this = self.0.lock();
|
||||
this.renderer.destroy();
|
||||
let window = this.native_window;
|
||||
let sheet_parent = this.sheet_parent.take();
|
||||
this.display_link.take();
|
||||
unsafe {
|
||||
this.native_window.setDelegate_(nil);
|
||||
@@ -984,9 +946,6 @@ impl Drop for MacWindow {
|
||||
this.executor
|
||||
.spawn(async move {
|
||||
unsafe {
|
||||
if let Some(parent) = sheet_parent {
|
||||
let _: () = msg_send![parent, endSheet: window];
|
||||
}
|
||||
window.close();
|
||||
window.autorelease();
|
||||
}
|
||||
|
||||
@@ -32,8 +32,6 @@ pub(crate) struct TestPlatform {
|
||||
current_clipboard_item: Mutex<Option<ClipboardItem>>,
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
current_primary_item: Mutex<Option<ClipboardItem>>,
|
||||
#[cfg(target_os = "macos")]
|
||||
current_find_pasteboard_item: Mutex<Option<ClipboardItem>>,
|
||||
pub(crate) prompts: RefCell<TestPrompts>,
|
||||
screen_capture_sources: RefCell<Vec<TestScreenCaptureSource>>,
|
||||
pub opened_url: RefCell<Option<String>>,
|
||||
@@ -119,8 +117,6 @@ impl TestPlatform {
|
||||
current_clipboard_item: Mutex::new(None),
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
current_primary_item: Mutex::new(None),
|
||||
#[cfg(target_os = "macos")]
|
||||
current_find_pasteboard_item: Mutex::new(None),
|
||||
weak: weak.clone(),
|
||||
opened_url: Default::default(),
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -402,8 +398,9 @@ impl Platform for TestPlatform {
|
||||
false
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.current_clipboard_item.lock().clone()
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn write_to_primary(&self, item: ClipboardItem) {
|
||||
*self.current_primary_item.lock() = Some(item);
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
@@ -415,19 +412,8 @@ impl Platform for TestPlatform {
|
||||
self.current_primary_item.lock().clone()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn write_to_primary(&self, item: ClipboardItem) {
|
||||
*self.current_primary_item.lock() = Some(item);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
|
||||
self.current_find_pasteboard_item.lock().clone()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn write_to_find_pasteboard(&self, item: ClipboardItem) {
|
||||
*self.current_find_pasteboard_item.lock() = Some(item);
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.current_clipboard_item.lock().clone()
|
||||
}
|
||||
|
||||
fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
|
||||
|
||||
@@ -40,11 +40,6 @@ impl WindowsWindowInner {
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
let handled = match msg {
|
||||
// eagerly activate the window, so calls to `active_window` will work correctly
|
||||
WM_MOUSEACTIVATE => {
|
||||
unsafe { SetActiveWindow(handle).log_err() };
|
||||
None
|
||||
}
|
||||
WM_ACTIVATE => self.handle_activate_msg(wparam),
|
||||
WM_CREATE => self.handle_create_msg(handle),
|
||||
WM_MOVE => self.handle_move_msg(handle, lparam),
|
||||
@@ -270,14 +265,6 @@ impl WindowsWindowInner {
|
||||
|
||||
fn handle_destroy_msg(&self, handle: HWND) -> Option<isize> {
|
||||
let callback = { self.state.callbacks.close.take() };
|
||||
// Re-enable parent window if this was a modal dialog
|
||||
if let Some(parent_hwnd) = self.parent_hwnd {
|
||||
unsafe {
|
||||
let _ = EnableWindow(parent_hwnd, true);
|
||||
let _ = SetForegroundWindow(parent_hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(callback) = callback {
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -659,7 +659,7 @@ impl Platform for WindowsPlatform {
|
||||
if let Err(err) = result {
|
||||
// ERROR_NOT_FOUND means the credential doesn't exist.
|
||||
// Return Ok(None) to match macOS and Linux behavior.
|
||||
if err.code() == ERROR_NOT_FOUND.to_hresult() {
|
||||
if err.code().0 == ERROR_NOT_FOUND.0 as i32 {
|
||||
return Ok(None);
|
||||
}
|
||||
return Err(err.into());
|
||||
|
||||
@@ -83,7 +83,6 @@ pub(crate) struct WindowsWindowInner {
|
||||
pub(crate) validation_number: usize,
|
||||
pub(crate) main_receiver: flume::Receiver<RunnableVariant>,
|
||||
pub(crate) platform_window_handle: HWND,
|
||||
pub(crate) parent_hwnd: Option<HWND>,
|
||||
}
|
||||
|
||||
impl WindowsWindowState {
|
||||
@@ -242,7 +241,6 @@ impl WindowsWindowInner {
|
||||
main_receiver: context.main_receiver.clone(),
|
||||
platform_window_handle: context.platform_window_handle,
|
||||
system_settings: WindowsSystemSettings::new(context.display),
|
||||
parent_hwnd: context.parent_hwnd,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -370,7 +368,6 @@ struct WindowCreateContext {
|
||||
disable_direct_composition: bool,
|
||||
directx_devices: DirectXDevices,
|
||||
invalidate_devices: Arc<AtomicBool>,
|
||||
parent_hwnd: Option<HWND>,
|
||||
}
|
||||
|
||||
impl WindowsWindow {
|
||||
@@ -393,20 +390,6 @@ impl WindowsWindow {
|
||||
invalidate_devices,
|
||||
} = creation_info;
|
||||
register_window_class(icon);
|
||||
let parent_hwnd = if params.kind == WindowKind::Dialog {
|
||||
let parent_window = unsafe { GetActiveWindow() };
|
||||
if parent_window.is_invalid() {
|
||||
None
|
||||
} else {
|
||||
// Disable the parent window to make this dialog modal
|
||||
unsafe {
|
||||
EnableWindow(parent_window, false).as_bool();
|
||||
};
|
||||
Some(parent_window)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let hide_title_bar = params
|
||||
.titlebar
|
||||
.as_ref()
|
||||
@@ -433,14 +416,8 @@ impl WindowsWindow {
|
||||
if params.is_minimizable {
|
||||
dwstyle |= WS_MINIMIZEBOX;
|
||||
}
|
||||
let dwexstyle = if params.kind == WindowKind::Dialog {
|
||||
dwstyle |= WS_POPUP | WS_CAPTION;
|
||||
WS_EX_DLGMODALFRAME
|
||||
} else {
|
||||
WS_EX_APPWINDOW
|
||||
};
|
||||
|
||||
(dwexstyle, dwstyle)
|
||||
(WS_EX_APPWINDOW, dwstyle)
|
||||
};
|
||||
if !disable_direct_composition {
|
||||
dwexstyle |= WS_EX_NOREDIRECTIONBITMAP;
|
||||
@@ -472,7 +449,6 @@ impl WindowsWindow {
|
||||
disable_direct_composition,
|
||||
directx_devices,
|
||||
invalidate_devices,
|
||||
parent_hwnd,
|
||||
};
|
||||
let creation_result = unsafe {
|
||||
CreateWindowExW(
|
||||
@@ -484,7 +460,7 @@ impl WindowsWindow {
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
parent_hwnd,
|
||||
None,
|
||||
None,
|
||||
Some(hinstance.into()),
|
||||
Some(&context as *const _ as *const _),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
fmt,
|
||||
iter::FusedIterator,
|
||||
sync::{Arc, atomic::AtomicUsize},
|
||||
@@ -10,9 +9,9 @@ use rand::{Rng, SeedableRng, rngs::SmallRng};
|
||||
use crate::Priority;
|
||||
|
||||
struct PriorityQueues<T> {
|
||||
high_priority: VecDeque<T>,
|
||||
medium_priority: VecDeque<T>,
|
||||
low_priority: VecDeque<T>,
|
||||
high_priority: Vec<T>,
|
||||
medium_priority: Vec<T>,
|
||||
low_priority: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> PriorityQueues<T> {
|
||||
@@ -43,9 +42,9 @@ impl<T> PriorityQueueState<T> {
|
||||
let mut queues = self.queues.lock();
|
||||
match priority {
|
||||
Priority::Realtime(_) => unreachable!(),
|
||||
Priority::High => queues.high_priority.push_back(item),
|
||||
Priority::Medium => queues.medium_priority.push_back(item),
|
||||
Priority::Low => queues.low_priority.push_back(item),
|
||||
Priority::High => queues.high_priority.push(item),
|
||||
Priority::Medium => queues.medium_priority.push(item),
|
||||
Priority::Low => queues.low_priority.push(item),
|
||||
};
|
||||
self.condvar.notify_one();
|
||||
Ok(())
|
||||
@@ -142,9 +141,9 @@ impl<T> PriorityQueueReceiver<T> {
|
||||
pub(crate) fn new() -> (PriorityQueueSender<T>, Self) {
|
||||
let state = PriorityQueueState {
|
||||
queues: parking_lot::Mutex::new(PriorityQueues {
|
||||
high_priority: VecDeque::new(),
|
||||
medium_priority: VecDeque::new(),
|
||||
low_priority: VecDeque::new(),
|
||||
high_priority: Vec::new(),
|
||||
medium_priority: Vec::new(),
|
||||
low_priority: Vec::new(),
|
||||
}),
|
||||
condvar: parking_lot::Condvar::new(),
|
||||
receiver_count: AtomicUsize::new(1),
|
||||
@@ -227,7 +226,7 @@ impl<T> PriorityQueueReceiver<T> {
|
||||
if !queues.high_priority.is_empty() {
|
||||
let flip = self.rand.random_ratio(P::High.probability(), mass);
|
||||
if flip {
|
||||
return Ok(queues.high_priority.pop_front());
|
||||
return Ok(queues.high_priority.pop());
|
||||
}
|
||||
mass -= P::High.probability();
|
||||
}
|
||||
@@ -235,7 +234,7 @@ impl<T> PriorityQueueReceiver<T> {
|
||||
if !queues.medium_priority.is_empty() {
|
||||
let flip = self.rand.random_ratio(P::Medium.probability(), mass);
|
||||
if flip {
|
||||
return Ok(queues.medium_priority.pop_front());
|
||||
return Ok(queues.medium_priority.pop());
|
||||
}
|
||||
mass -= P::Medium.probability();
|
||||
}
|
||||
@@ -243,7 +242,7 @@ impl<T> PriorityQueueReceiver<T> {
|
||||
if !queues.low_priority.is_empty() {
|
||||
let flip = self.rand.random_ratio(P::Low.probability(), mass);
|
||||
if flip {
|
||||
return Ok(queues.low_priority.pop_front());
|
||||
return Ok(queues.low_priority.pop());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -334,13 +334,9 @@ 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 at the end when it doesn't fit, and represent this truncation by
|
||||
/// displaying the provided string (e.g., "very long te…").
|
||||
/// Truncate the text when it doesn't fit, and represent this truncation by displaying the
|
||||
/// provided string.
|
||||
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,21 +75,13 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the truncate overflowing text with an ellipsis (…) at the end if needed.
|
||||
/// Sets the truncate overflowing text with an ellipsis (…) 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,15 +2,6 @@ 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>,
|
||||
@@ -137,83 +128,40 @@ 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_affix: &str,
|
||||
truncate_from: TruncateFrom,
|
||||
) -> Option<usize> {
|
||||
let mut width = px(0.);
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
if width.floor() > truncate_width {
|
||||
return Some(truncate_ix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Truncate a line of text to the given width with this wrapper's font and font size.
|
||||
pub fn truncate_line<'a>(
|
||||
&mut self,
|
||||
line: SharedString,
|
||||
truncate_width: Pixels,
|
||||
truncation_affix: &str,
|
||||
truncation_suffix: &str,
|
||||
runs: &'a [TextRun],
|
||||
truncate_from: TruncateFrom,
|
||||
) -> (SharedString, Cow<'a, [TextRun]>) {
|
||||
if let Some(truncate_ix) =
|
||||
self.should_truncate_line(&line, truncate_width, truncation_affix, truncate_from)
|
||||
{
|
||||
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_affix, &mut runs, truncate_from);
|
||||
(result, Cow::Owned(runs))
|
||||
} else {
|
||||
(line, Cow::Borrowed(runs))
|
||||
let mut width = px(0.);
|
||||
let mut suffix_width = truncation_suffix
|
||||
.chars()
|
||||
.map(|c| self.width_for_char(c))
|
||||
.fold(px(0.0), |a, x| a + x);
|
||||
let mut char_indices = line.char_indices();
|
||||
let mut truncate_ix = 0;
|
||||
for (ix, c) in char_indices {
|
||||
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 {
|
||||
let result =
|
||||
SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix));
|
||||
let mut runs = runs.to_vec();
|
||||
update_runs_after_truncation(&result, truncation_suffix, &mut runs);
|
||||
|
||||
return (result, Cow::Owned(runs));
|
||||
}
|
||||
}
|
||||
|
||||
(line, Cow::Borrowed(runs))
|
||||
}
|
||||
|
||||
/// Any character in this list should be treated as a word character,
|
||||
@@ -234,11 +182,6 @@ impl LineWrapper {
|
||||
// Cyrillic for Russian, Ukrainian, etc.
|
||||
// https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode
|
||||
matches!(c, '\u{0400}'..='\u{04FF}') ||
|
||||
|
||||
// Vietnamese (https://vietunicode.sourceforge.net/charset/)
|
||||
matches!(c, '\u{1E00}'..='\u{1EFF}') || // Latin Extended Additional
|
||||
matches!(c, '\u{0300}'..='\u{036F}') || // Combining Diacritical Marks
|
||||
|
||||
// Some other known special characters that should be treated as word characters,
|
||||
// e.g. `a-b`, `var_name`, `I'm`, '@mention`, `#hashtag`, `100%`, `3.1415`,
|
||||
// `2^3`, `a~b`, `a=1`, `Self::new`, etc.
|
||||
@@ -282,35 +225,15 @@ impl LineWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_runs_after_truncation(
|
||||
result: &str,
|
||||
ellipsis: &str,
|
||||
runs: &mut Vec<TextRun>,
|
||||
truncate_from: TruncateFrom,
|
||||
) {
|
||||
fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<TextRun>) {
|
||||
let mut truncate_at = result.len() - ellipsis.len();
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -560,7 +483,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_line_end() {
|
||||
fn test_truncate_line() {
|
||||
let mut wrapper = build_wrapper();
|
||||
|
||||
fn perform_test(
|
||||
@@ -571,13 +494,8 @@ 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,
|
||||
TruncateFrom::End,
|
||||
);
|
||||
let (result, dummy_runs) =
|
||||
wrapper.truncate_line(text.into(), px(220.), ellipsis, &dummy_runs);
|
||||
assert_eq!(result, expected);
|
||||
assert_eq!(dummy_runs.first().unwrap().len, result.len());
|
||||
}
|
||||
@@ -603,50 +521,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
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() {
|
||||
fn test_truncate_multiple_runs() {
|
||||
let mut wrapper = build_wrapper();
|
||||
|
||||
fn perform_test(
|
||||
@@ -659,7 +534,7 @@ mod tests {
|
||||
) {
|
||||
let dummy_runs = generate_test_runs(run_lens);
|
||||
let (result, dummy_runs) =
|
||||
wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs, TruncateFrom::End);
|
||||
wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs);
|
||||
assert_eq!(result, expected);
|
||||
for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
|
||||
assert_eq!(run.len, *result_len);
|
||||
@@ -705,75 +580,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
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 test_update_run_after_truncation() {
|
||||
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, TruncateFrom::End);
|
||||
update_runs_after_truncation(result, "…", &mut dummy_runs);
|
||||
for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
|
||||
assert_eq!(run.len, *result_len);
|
||||
}
|
||||
@@ -808,12 +618,7 @@ mod tests {
|
||||
#[track_caller]
|
||||
fn assert_word(word: &str) {
|
||||
for c in word.chars() {
|
||||
assert!(
|
||||
LineWrapper::is_word_char(c),
|
||||
"assertion failed for '{}' (unicode 0x{:x})",
|
||||
c,
|
||||
c as u32
|
||||
);
|
||||
assert!(LineWrapper::is_word_char(c), "assertion failed for '{}'", c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -856,8 +661,6 @@ mod tests {
|
||||
assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ");
|
||||
// Cyrillic
|
||||
assert_word("АБВГДЕЖЗИЙКЛМНОП");
|
||||
// Vietnamese (https://github.com/zed-industries/zed/issues/23245)
|
||||
assert_word("ThậmchíđếnkhithuachạychúngcònnhẫntâmgiếtnốtsốđôngtùchínhtrịởYênBáivàCaoBằng");
|
||||
|
||||
// non-word characters
|
||||
assert_not_word("你好");
|
||||
|
||||
@@ -876,9 +876,7 @@ pub struct Window {
|
||||
active: Rc<Cell<bool>>,
|
||||
hovered: Rc<Cell<bool>>,
|
||||
pub(crate) needs_present: Rc<Cell<bool>>,
|
||||
/// Tracks recent input event timestamps to determine if input is arriving at a high rate.
|
||||
/// Used to selectively enable VRR optimization only when input rate exceeds 60fps.
|
||||
pub(crate) input_rate_tracker: Rc<RefCell<InputRateTracker>>,
|
||||
pub(crate) last_input_timestamp: Rc<Cell<Instant>>,
|
||||
last_input_modality: InputModality,
|
||||
pub(crate) refreshing: bool,
|
||||
pub(crate) activation_observers: SubscriberSet<(), AnyObserver>,
|
||||
@@ -899,51 +897,6 @@ struct ModifierState {
|
||||
saw_keystroke: bool,
|
||||
}
|
||||
|
||||
/// Tracks input event timestamps to determine if input is arriving at a high rate.
|
||||
/// Used for selective VRR (Variable Refresh Rate) optimization.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct InputRateTracker {
|
||||
timestamps: Vec<Instant>,
|
||||
window: Duration,
|
||||
inputs_per_second: u32,
|
||||
sustain_until: Instant,
|
||||
sustain_duration: Duration,
|
||||
}
|
||||
|
||||
impl Default for InputRateTracker {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timestamps: Vec::new(),
|
||||
window: Duration::from_millis(100),
|
||||
inputs_per_second: 60,
|
||||
sustain_until: Instant::now(),
|
||||
sustain_duration: Duration::from_secs(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InputRateTracker {
|
||||
pub fn record_input(&mut self) {
|
||||
let now = Instant::now();
|
||||
self.timestamps.push(now);
|
||||
self.prune_old_timestamps(now);
|
||||
|
||||
let min_events = self.inputs_per_second as u128 * self.window.as_millis() / 1000;
|
||||
if self.timestamps.len() as u128 >= min_events {
|
||||
self.sustain_until = now + self.sustain_duration;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_high_rate(&self) -> bool {
|
||||
Instant::now() < self.sustain_until
|
||||
}
|
||||
|
||||
fn prune_old_timestamps(&mut self, now: Instant) {
|
||||
self.timestamps
|
||||
.retain(|&t| now.duration_since(t) <= self.window);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum DrawPhase {
|
||||
None,
|
||||
@@ -1094,7 +1047,7 @@ impl Window {
|
||||
let hovered = Rc::new(Cell::new(platform_window.is_hovered()));
|
||||
let needs_present = Rc::new(Cell::new(false));
|
||||
let next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>> = Default::default();
|
||||
let input_rate_tracker = Rc::new(RefCell::new(InputRateTracker::default()));
|
||||
let last_input_timestamp = Rc::new(Cell::new(Instant::now()));
|
||||
|
||||
platform_window
|
||||
.request_decorations(window_decorations.unwrap_or(WindowDecorations::Server));
|
||||
@@ -1122,7 +1075,7 @@ impl Window {
|
||||
let active = active.clone();
|
||||
let needs_present = needs_present.clone();
|
||||
let next_frame_callbacks = next_frame_callbacks.clone();
|
||||
let input_rate_tracker = input_rate_tracker.clone();
|
||||
let last_input_timestamp = last_input_timestamp.clone();
|
||||
move |request_frame_options| {
|
||||
let next_frame_callbacks = next_frame_callbacks.take();
|
||||
if !next_frame_callbacks.is_empty() {
|
||||
@@ -1135,12 +1088,12 @@ impl Window {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
// Keep presenting if input was recently arriving at a high rate (>= 60fps).
|
||||
// Once high-rate input is detected, we sustain presentation for 1 second
|
||||
// to prevent display underclocking during active input.
|
||||
// Keep presenting the current scene for 1 extra second since the
|
||||
// last input to prevent the display from underclocking the refresh rate.
|
||||
let needs_present = request_frame_options.require_presentation
|
||||
|| needs_present.get()
|
||||
|| (active.get() && input_rate_tracker.borrow_mut().is_high_rate());
|
||||
|| (active.get()
|
||||
&& last_input_timestamp.get().elapsed() < Duration::from_secs(1));
|
||||
|
||||
if invalidator.is_dirty() || request_frame_options.force_render {
|
||||
measure("frame duration", || {
|
||||
@@ -1148,6 +1101,7 @@ impl Window {
|
||||
.update(&mut cx, |_, window, cx| {
|
||||
let arena_clear_needed = window.draw(cx);
|
||||
window.present();
|
||||
// drop the arena elements after present to reduce latency
|
||||
arena_clear_needed.clear();
|
||||
})
|
||||
.log_err();
|
||||
@@ -1345,7 +1299,7 @@ impl Window {
|
||||
active,
|
||||
hovered,
|
||||
needs_present,
|
||||
input_rate_tracker,
|
||||
last_input_timestamp,
|
||||
last_input_modality: InputModality::Mouse,
|
||||
refreshing: false,
|
||||
activation_observers: SubscriberSet::new(),
|
||||
@@ -3737,6 +3691,8 @@ impl Window {
|
||||
/// Dispatch a mouse or keyboard event on the window.
|
||||
#[profiling::function]
|
||||
pub fn dispatch_event(&mut self, event: PlatformInput, cx: &mut App) -> DispatchEventResult {
|
||||
self.last_input_timestamp.set(Instant::now());
|
||||
|
||||
// Track whether this input was keyboard-based for focus-visible styling
|
||||
self.last_input_modality = match &event {
|
||||
PlatformInput::KeyDown(_) | PlatformInput::ModifiersChanged(_) => {
|
||||
@@ -3837,10 +3793,6 @@ impl Window {
|
||||
self.dispatch_key_event(any_key_event, cx);
|
||||
}
|
||||
|
||||
if self.invalidator.is_dirty() {
|
||||
self.input_rate_tracker.borrow_mut().record_input();
|
||||
}
|
||||
|
||||
DispatchEventResult {
|
||||
propagate: cx.propagate_event,
|
||||
default_prevented: self.default_prevented,
|
||||
@@ -5014,7 +4966,7 @@ impl<V: 'static> From<WindowHandle<V>> for AnyWindowHandle {
|
||||
}
|
||||
|
||||
/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct AnyWindowHandle {
|
||||
pub(crate) id: WindowId,
|
||||
state_type: TypeId,
|
||||
|
||||
@@ -1801,7 +1801,9 @@ impl Buffer {
|
||||
self.syntax_map.lock().did_parse(syntax_snapshot);
|
||||
self.request_autoindent(cx);
|
||||
self.parse_status.0.send(ParseStatus::Idle).unwrap();
|
||||
self.invalidate_tree_sitter_data(self.text.snapshot());
|
||||
if self.text.version() != *self.tree_sitter_data.version() {
|
||||
self.invalidate_tree_sitter_data(self.text.snapshot());
|
||||
}
|
||||
cx.emit(BufferEvent::Reparsed);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1141,104 +1141,6 @@ 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]
|
||||
|
||||
@@ -827,15 +827,6 @@ pub struct LanguageConfig {
|
||||
/// Delimiters and configuration for recognizing and formatting documentation comments.
|
||||
#[serde(default, alias = "documentation")]
|
||||
pub documentation_comment: Option<BlockCommentConfig>,
|
||||
/// List markers that are inserted unchanged on newline (e.g., `- `, `* `, `+ `).
|
||||
#[serde(default)]
|
||||
pub unordered_list: Vec<Arc<str>>,
|
||||
/// Configuration for ordered lists with auto-incrementing numbers on newline (e.g., `1. ` becomes `2. `).
|
||||
#[serde(default)]
|
||||
pub ordered_list: Vec<OrderedListConfig>,
|
||||
/// Configuration for task lists where multiple markers map to a single continuation prefix (e.g., `- [x] ` continues as `- [ ] `).
|
||||
#[serde(default)]
|
||||
pub task_list: Option<TaskListConfig>,
|
||||
/// A list of additional regex patterns that should be treated as prefixes
|
||||
/// for creating boundaries during rewrapping, ensuring content from one
|
||||
/// prefixed section doesn't merge with another (e.g., markdown list items).
|
||||
@@ -907,24 +898,6 @@ pub struct DecreaseIndentConfig {
|
||||
pub valid_after: Vec<String>,
|
||||
}
|
||||
|
||||
/// Configuration for continuing ordered lists with auto-incrementing numbers.
|
||||
#[derive(Clone, Debug, Deserialize, JsonSchema)]
|
||||
pub struct OrderedListConfig {
|
||||
/// A regex pattern with a capture group for the number portion (e.g., `(\\d+)\\. `).
|
||||
pub pattern: String,
|
||||
/// A format string where `{1}` is replaced with the incremented number (e.g., `{1}. `).
|
||||
pub format: String,
|
||||
}
|
||||
|
||||
/// Configuration for continuing task lists on newline.
|
||||
#[derive(Clone, Debug, Deserialize, JsonSchema)]
|
||||
pub struct TaskListConfig {
|
||||
/// The list markers to match (e.g., `- [ ] `, `- [x] `).
|
||||
pub prefixes: Vec<Arc<str>>,
|
||||
/// The marker to insert when continuing the list on a new line (e.g., `- [ ] `).
|
||||
pub continuation: Arc<str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
|
||||
pub struct LanguageMatcher {
|
||||
/// Given a list of `LanguageConfig`'s, the language of a file can be determined based on the path extension matching any of the `path_suffixes`.
|
||||
@@ -1095,9 +1068,6 @@ impl Default for LanguageConfig {
|
||||
line_comments: Default::default(),
|
||||
block_comment: Default::default(),
|
||||
documentation_comment: Default::default(),
|
||||
unordered_list: Default::default(),
|
||||
ordered_list: Default::default(),
|
||||
task_list: Default::default(),
|
||||
rewrap_prefixes: Default::default(),
|
||||
scope_opt_in_language_servers: Default::default(),
|
||||
overrides: Default::default(),
|
||||
@@ -2183,21 +2153,6 @@ impl LanguageScope {
|
||||
self.language.config.documentation_comment.as_ref()
|
||||
}
|
||||
|
||||
/// Returns list markers that are inserted unchanged on newline (e.g., `- `, `* `, `+ `).
|
||||
pub fn unordered_list(&self) -> &[Arc<str>] {
|
||||
&self.language.config.unordered_list
|
||||
}
|
||||
|
||||
/// Returns configuration for ordered lists with auto-incrementing numbers (e.g., `1. ` becomes `2. `).
|
||||
pub fn ordered_list(&self) -> &[OrderedListConfig] {
|
||||
&self.language.config.ordered_list
|
||||
}
|
||||
|
||||
/// Returns configuration for task list continuation, if any (e.g., `- [x] ` continues as `- [ ] `).
|
||||
pub fn task_list(&self) -> Option<&TaskListConfig> {
|
||||
self.language.config.task_list.as_ref()
|
||||
}
|
||||
|
||||
/// Returns additional regex patterns that act as prefix markers for creating
|
||||
/// boundaries during rewrapping.
|
||||
///
|
||||
|
||||
@@ -122,10 +122,6 @@ pub struct LanguageSettings {
|
||||
pub whitespace_map: WhitespaceMap,
|
||||
/// Whether to start a new line with a comment when a previous line is a comment as well.
|
||||
pub extend_comment_on_newline: bool,
|
||||
/// Whether to continue markdown lists when pressing enter.
|
||||
pub extend_list_on_newline: bool,
|
||||
/// Whether to indent list items when pressing tab after a list marker.
|
||||
pub indent_list_on_tab: bool,
|
||||
/// Inlay hint related settings.
|
||||
pub inlay_hints: InlayHintSettings,
|
||||
/// Whether to automatically close brackets.
|
||||
@@ -571,8 +567,6 @@ impl settings::Settings for AllLanguageSettings {
|
||||
tab: SharedString::new(whitespace_map.tab.unwrap().to_string()),
|
||||
},
|
||||
extend_comment_on_newline: settings.extend_comment_on_newline.unwrap(),
|
||||
extend_list_on_newline: settings.extend_list_on_newline.unwrap(),
|
||||
indent_list_on_tab: settings.indent_list_on_tab.unwrap(),
|
||||
inlay_hints: InlayHintSettings {
|
||||
enabled: inlay_hints.enabled.unwrap(),
|
||||
show_value_hints: inlay_hints.show_value_hints.unwrap(),
|
||||
|
||||
@@ -19,10 +19,7 @@ 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, QueryMatch, QueryMatches,
|
||||
QueryPredicateArg, Tree,
|
||||
};
|
||||
use tree_sitter::{Node, Query, QueryCapture, QueryCaptures, QueryCursor, QueryMatches, Tree};
|
||||
|
||||
pub const MAX_BYTES_TO_QUERY: usize = 16 * 1024;
|
||||
|
||||
@@ -85,7 +82,6 @@ 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,
|
||||
}
|
||||
@@ -1167,7 +1163,6 @@ impl<'a> SyntaxMapMatches<'a> {
|
||||
depth: layer.depth,
|
||||
grammar_index,
|
||||
matches,
|
||||
query,
|
||||
next_pattern_index: 0,
|
||||
next_captures: Vec::new(),
|
||||
has_next: false,
|
||||
@@ -1265,20 +1260,13 @@ impl SyntaxMapCapturesLayer<'_> {
|
||||
|
||||
impl SyntaxMapMatchesLayer<'_> {
|
||||
fn advance(&mut self) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1307,39 +1295,6 @@ 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,10 +4,7 @@
|
||||
//! 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::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
@@ -39,7 +36,7 @@ pub struct Toolchain {
|
||||
/// - Only in the subproject they're currently in.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum ToolchainScope {
|
||||
Subproject(Arc<Path>, Arc<RelPath>),
|
||||
Subproject(WorktreeId, Arc<RelPath>),
|
||||
Project,
|
||||
/// Available in all projects on this box. It wouldn't make sense to show suggestions across machines.
|
||||
Global,
|
||||
|
||||
@@ -797,26 +797,11 @@ 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) -> IconOrSvg {
|
||||
IconOrSvg::default()
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::ZedAssistant
|
||||
}
|
||||
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>>;
|
||||
fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>>;
|
||||
@@ -835,7 +820,7 @@ pub trait LanguageModelProvider: 'static {
|
||||
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>>;
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, PartialEq, Eq)]
|
||||
#[derive(Default, Clone)]
|
||||
pub enum ConfigurationViewTargetAgent {
|
||||
#[default]
|
||||
ZedAgent,
|
||||
|
||||
@@ -2,16 +2,12 @@ use crate::{
|
||||
LanguageModel, LanguageModelId, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderState,
|
||||
};
|
||||
use collections::{BTreeMap, HashSet};
|
||||
use collections::BTreeMap;
|
||||
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));
|
||||
@@ -52,11 +48,6 @@ 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)]
|
||||
@@ -113,8 +104,6 @@ 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 {}
|
||||
@@ -194,60 +183,6 @@ 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>,
|
||||
@@ -481,132 +416,4 @@ 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,8 +28,6 @@ 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"] }
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
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,12 +7,9 @@ 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;
|
||||
@@ -34,56 +31,6 @@ 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()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user