Compare commits

..

1 Commits

Author SHA1 Message Date
Ben Brandt
a6a5f55a05 Add GitHub token authentication to HTTP client
Automatically adds GitHub authentication headers when GITHUB_TOKEN
environment variable is set.
2025-06-05 20:22:38 +02:00
195 changed files with 2496 additions and 8757 deletions

View File

@@ -19,12 +19,6 @@ runs:
shell: bash -euxo pipefail {0}
run: ./script/linux
- name: Check for broken links
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
with:
args: --no-progress './docs/src/**/*'
fail: true
- name: Build book
shell: bash -euxo pipefail {0}
run: |

View File

@@ -683,7 +683,7 @@ jobs:
timeout-minutes: 60
name: Linux arm64 release bundle
runs-on:
- hosted-linux-arm-1
- buildjet-16vcpu-ubuntu-2204-arm
if: |
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
@@ -736,64 +736,6 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
freebsd:
timeout-minutes: 60
runs-on: github-8vcpu-ubuntu-2404
if: |
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
needs: [linux_tests]
name: Build Zed on FreeBSD
# env:
# MYTOKEN : ${{ secrets.MYTOKEN }}
# MYTOKEN2: "value2"
steps:
- uses: actions/checkout@v4
- name: Build FreeBSD remote-server
id: freebsd-build
uses: vmactions/freebsd-vm@c3ae29a132c8ef1924775414107a97cac042aad5 # v1.2.0
with:
# envs: "MYTOKEN MYTOKEN2"
usesh: true
release: 13.5
copyback: true
prepare: |
pkg install -y \
bash curl jq git \
rustup-init cmake-core llvm-devel-lite pkgconf protobuf # ibx11 alsa-lib rust-bindgen-cli
run: |
freebsd-version
sysctl hw.model
sysctl hw.ncpu
sysctl hw.physmem
sysctl hw.usermem
git config --global --add safe.directory /home/runner/work/zed/zed
rustup-init --profile minimal --default-toolchain none -y
. "$HOME/.cargo/env"
./script/bundle-freebsd
mkdir -p out/
mv "target/zed-remote-server-freebsd-x86_64.gz" out/
rm -rf target/
cargo clean
- name: Upload Artifact to Workflow - zed-remote-server (run-bundling)
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-freebsd.gz
path: out/zed-remote-server-freebsd-x86_64.gz
- name: Upload Artifacts to release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
files: |
out/zed-remote-server-freebsd-x86_64.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
nix-build:
name: Build with Nix
uses: ./.github/workflows/nix.yml
@@ -808,12 +750,12 @@ jobs:
if: |
startsWith(github.ref, 'refs/tags/v')
&& endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, freebsd]
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64]
runs-on:
- self-hosted
- bundle
steps:
- name: gh release
run: gh release edit $GITHUB_REF_NAME --draft=false
run: gh release edit $GITHUB_REF_NAME --draft=true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -167,50 +167,6 @@ jobs:
- name: Upload Zed Nightly
run: script/upload-nightly linux-targz
freebsd:
timeout-minutes: 60
if: github.repository_owner == 'zed-industries'
runs-on: github-8vcpu-ubuntu-2404
needs: tests
env:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
name: Build Zed on FreeBSD
# env:
# MYTOKEN : ${{ secrets.MYTOKEN }}
# MYTOKEN2: "value2"
steps:
- uses: actions/checkout@v4
- name: Build FreeBSD remote-server
id: freebsd-build
uses: vmactions/freebsd-vm@c3ae29a132c8ef1924775414107a97cac042aad5 # v1.2.0
with:
# envs: "MYTOKEN MYTOKEN2"
usesh: true
release: 13.5
copyback: true
prepare: |
pkg install -y \
bash curl jq git \
rustup-init cmake-core llvm-devel-lite pkgconf protobuf # ibx11 alsa-lib rust-bindgen-cli
run: |
freebsd-version
sysctl hw.model
sysctl hw.ncpu
sysctl hw.physmem
sysctl hw.usermem
git config --global --add safe.directory /home/runner/work/zed/zed
rustup-init --profile minimal --default-toolchain none -y
. "$HOME/.cargo/env"
./script/bundle-freebsd
mkdir -p out/
mv "target/zed-remote-server-freebsd-x86_64.gz" out/
rm -rf target/
cargo clean
- name: Upload Zed Nightly
run: script/upload-nightly freebsd
bundle-nix:
name: Build and cache Nix package
needs: tests

View File

@@ -66,7 +66,7 @@ jobs:
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Send failure message to Slack channel if needed
- name: Send the pull request link into the Slack channel
if: ${{ failure() }}
uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52
with:

View File

@@ -47,7 +47,6 @@
"remove_trailing_whitespace_on_save": true,
"ensure_final_newline_on_save": true,
"file_scan_exclusions": [
"crates/assistant_tools/src/evals/fixtures",
"crates/eval/worktrees/",
"crates/eval/repos/",
"**/.git",

44
Cargo.lock generated
View File

@@ -59,7 +59,7 @@ dependencies = [
"assistant_slash_command",
"assistant_slash_commands",
"assistant_tool",
"assistant_tools",
"async-watch",
"audio",
"buffer_diff",
"chrono",
@@ -130,7 +130,6 @@ dependencies = [
"urlencoding",
"util",
"uuid",
"watch",
"workspace",
"workspace-hack",
"zed_actions",
@@ -148,6 +147,7 @@ dependencies = [
"deepseek",
"fs",
"gpui",
"indexmap",
"language_model",
"lmstudio",
"log",
@@ -631,6 +631,7 @@ name = "assistant_tool"
version = "0.1.0"
dependencies = [
"anyhow",
"async-watch",
"buffer_diff",
"clock",
"collections",
@@ -652,7 +653,6 @@ dependencies = [
"settings",
"text",
"util",
"watch",
"workspace",
"workspace-hack",
"zlog",
@@ -665,6 +665,7 @@ dependencies = [
"agent_settings",
"anyhow",
"assistant_tool",
"async-watch",
"buffer_diff",
"chrono",
"client",
@@ -715,7 +716,6 @@ dependencies = [
"ui",
"unindent",
"util",
"watch",
"web_search",
"which 6.0.3",
"workspace",
@@ -1074,6 +1074,15 @@ dependencies = [
"tungstenite 0.26.2",
]
[[package]]
name = "async-watch"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a078faf4e27c0c6cc0efb20e5da59dcccc04968ebf2801d8e0b2195124cdcdb2"
dependencies = [
"event-listener 2.5.3",
]
[[package]]
name = "async_zip"
version = "0.0.17"
@@ -2978,6 +2987,7 @@ dependencies = [
"anyhow",
"assistant_context_editor",
"assistant_slash_command",
"assistant_tool",
"async-stripe",
"async-trait",
"async-tungstenite",
@@ -4224,7 +4234,6 @@ dependencies = [
"futures 0.3.31",
"fuzzy",
"gpui",
"itertools 0.14.0",
"language",
"log",
"menu",
@@ -5004,6 +5013,7 @@ dependencies = [
"assistant_tool",
"assistant_tools",
"async-trait",
"async-watch",
"buffer_diff",
"chrono",
"clap",
@@ -5045,7 +5055,6 @@ dependencies = [
"unindent",
"util",
"uuid",
"watch",
"workspace-hack",
"zed_llm_client",
]
@@ -8730,6 +8739,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"async-watch",
"clock",
"collections",
"ctor",
@@ -8779,7 +8789,6 @@ dependencies = [
"unicase",
"unindent",
"util",
"watch",
"workspace-hack",
"zlog",
]
@@ -10138,6 +10147,7 @@ dependencies = [
"async-std",
"async-tar",
"async-trait",
"async-watch",
"futures 0.3.31",
"http_client",
"log",
@@ -10147,7 +10157,6 @@ dependencies = [
"serde_json",
"smol",
"util",
"watch",
"which 6.0.3",
"workspace-hack",
]
@@ -10196,7 +10205,6 @@ dependencies = [
"util",
"workspace",
"workspace-hack",
"zed_actions",
]
[[package]]
@@ -12998,6 +13006,7 @@ dependencies = [
"askpass",
"assistant_tool",
"assistant_tools",
"async-watch",
"backtrace",
"cargo_toml",
"chrono",
@@ -13044,7 +13053,6 @@ dependencies = [
"toml 0.8.20",
"unindent",
"util",
"watch",
"worktree",
"zlog",
]
@@ -15724,7 +15732,6 @@ dependencies = [
"task",
"theme",
"thiserror 2.0.12",
"url",
"util",
"windows 0.61.1",
"workspace-hack",
@@ -17906,19 +17913,6 @@ dependencies = [
"leb128",
]
[[package]]
name = "watch"
version = "0.1.0"
dependencies = [
"ctor",
"futures 0.3.31",
"gpui",
"parking_lot",
"rand 0.8.5",
"workspace-hack",
"zlog",
]
[[package]]
name = "wayland-backend"
version = "0.3.8"
@@ -19730,6 +19724,7 @@ dependencies = [
"assistant_context_editor",
"assistant_tool",
"assistant_tools",
"async-watch",
"audio",
"auto_update",
"auto_update_ui",
@@ -19846,7 +19841,6 @@ dependencies = [
"uuid",
"vim",
"vim_mode_setting",
"watch",
"web_search",
"web_search_providers",
"welcome",

View File

@@ -165,7 +165,6 @@ members = [
"crates/util_macros",
"crates/vim",
"crates/vim_mode_setting",
"crates/watch",
"crates/web_search",
"crates/web_search_providers",
"crates/welcome",
@@ -374,7 +373,6 @@ util = { path = "crates/util" }
util_macros = { path = "crates/util_macros" }
vim = { path = "crates/vim" }
vim_mode_setting = { path = "crates/vim_mode_setting" }
watch = { path = "crates/watch" }
web_search = { path = "crates/web_search" }
web_search_providers = { path = "crates/web_search_providers" }
welcome = { path = "crates/welcome" }
@@ -405,6 +403,7 @@ async-recursion = "1.0.0"
async-tar = "0.5.0"
async-trait = "0.1"
async-tungstenite = "0.29.1"
async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
aws-credential-types = { version = "1.2.2", features = [

View File

@@ -153,7 +153,8 @@
"context": "Editor && mode == full && edit_prediction",
"bindings": {
"alt-]": "editor::NextEditPrediction",
"alt-[": "editor::PreviousEditPrediction"
"alt-[": "editor::PreviousEditPrediction",
"alt-right": "editor::AcceptPartialEditPrediction"
}
},
{
@@ -218,6 +219,7 @@
"ctrl-enter": "assistant::Assist",
"ctrl-s": "workspace::Save",
"save": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
@@ -243,7 +245,6 @@
"ctrl-shift-j": "agent::ToggleNavigationMenu",
"ctrl-shift-i": "agent::ToggleOptionsMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl->": "assistant::QuoteSelection",
"ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
@@ -661,16 +662,14 @@
"bindings": {
"alt-tab": "editor::AcceptEditPrediction",
"alt-l": "editor::AcceptEditPrediction",
"tab": "editor::AcceptEditPrediction",
"alt-right": "editor::AcceptPartialEditPrediction"
"tab": "editor::AcceptEditPrediction"
}
},
{
"context": "Editor && edit_prediction_conflict",
"bindings": {
"alt-tab": "editor::AcceptEditPrediction",
"alt-l": "editor::AcceptEditPrediction",
"alt-right": "editor::AcceptPartialEditPrediction"
"alt-l": "editor::AcceptEditPrediction"
}
},
{

View File

@@ -181,7 +181,8 @@
"use_key_equivalents": true,
"bindings": {
"alt-tab": "editor::NextEditPrediction",
"alt-shift-tab": "editor::PreviousEditPrediction"
"alt-shift-tab": "editor::PreviousEditPrediction",
"ctrl-cmd-right": "editor::AcceptPartialEditPrediction"
}
},
{
@@ -252,6 +253,7 @@
"bindings": {
"cmd-enter": "assistant::Assist",
"cmd-s": "workspace::Save",
"cmd->": "assistant::QuoteSelection",
"cmd-<": "assistant::InsertIntoEditor",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
@@ -278,7 +280,6 @@
"cmd-shift-j": "agent::ToggleNavigationMenu",
"cmd-shift-i": "agent::ToggleOptionsMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"cmd->": "assistant::QuoteSelection",
"cmd-alt-e": "agent::RemoveAllContext",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-shift-enter": "agent::ContinueThread",
@@ -718,16 +719,14 @@
"context": "Editor && edit_prediction",
"bindings": {
"alt-tab": "editor::AcceptEditPrediction",
"tab": "editor::AcceptEditPrediction",
"ctrl-cmd-right": "editor::AcceptPartialEditPrediction"
"tab": "editor::AcceptEditPrediction"
}
},
{
"context": "Editor && edit_prediction_conflict",
"use_key_equivalents": true,
"bindings": {
"alt-tab": "editor::AcceptEditPrediction",
"ctrl-cmd-right": "editor::AcceptPartialEditPrediction"
"alt-tab": "editor::AcceptEditPrediction"
}
},
{

View File

@@ -13,9 +13,9 @@
}
},
{
"context": "Editor && vim_mode == insert && !menu",
"context": "Editor",
"bindings": {
// "j k": "vim::SwitchToNormalMode"
// "j k": ["workspace::SendKeystrokes", "escape"]
}
}
]

View File

@@ -711,7 +711,7 @@
}
},
{
"context": "AgentPanel || GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || DebugPanel",
"context": "GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || DebugPanel",
"bindings": {
// window related commands (ctrl-w X)
"ctrl-w": null,

View File

@@ -771,6 +771,7 @@
"tools": {
"copy_path": true,
"create_directory": true,
"create_file": true,
"delete_path": true,
"diagnostics": true,
"edit_file": true,
@@ -1033,14 +1034,6 @@
"button": true,
// Whether to show warnings or not by default.
"include_warnings": true,
// Settings for using LSP pull diagnostics mechanism in Zed.
"lsp_pull_diagnostics": {
// Whether to pull for diagnostics or not.
"enabled": true,
// Minimum time to wait before pulling diagnostics from the language server(s).
// 0 turns the debounce off.
"debounce_ms": 50
},
// Settings for inline diagnostics
"inline": {
// Whether to show diagnostics inline or not
@@ -1464,9 +1457,7 @@
"language_servers": ["erlang-ls", "!elp", "..."]
},
"Git Commit": {
"allow_rewrap": "anywhere",
"soft_wrap": "editor_width",
"preferred_line_length": 72
"allow_rewrap": "anywhere"
},
"Go": {
"code_actions_on_format": {
@@ -1544,6 +1535,12 @@
"allowed": true
}
},
"SQL": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-sql"]
}
},
"Starlark": {
"language_servers": ["starpls", "!buck2-lsp", "..."]
},

View File

@@ -261,11 +261,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#bfbdb6ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#d2a6ffff",
"font_style": null,
@@ -321,16 +316,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#d2a6ffff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#5ac1feff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#a9d94bff",
"font_style": null,
@@ -457,9 +442,9 @@
"terminal.foreground": "#5c6166ff",
"terminal.bright_foreground": "#5c6166ff",
"terminal.dim_foreground": "#fcfcfcff",
"terminal.ansi.black": "#5c6166ff",
"terminal.ansi.bright_black": "#3b9ee5ff",
"terminal.ansi.dim_black": "#9c9fa2ff",
"terminal.ansi.black": "#fcfcfcff",
"terminal.ansi.bright_black": "#bcbec0ff",
"terminal.ansi.dim_black": "#5c6166ff",
"terminal.ansi.red": "#ef7271ff",
"terminal.ansi.bright_red": "#febab6ff",
"terminal.ansi.dim_red": "#833538ff",
@@ -478,9 +463,9 @@
"terminal.ansi.cyan": "#4dbf99ff",
"terminal.ansi.bright_cyan": "#ace0cbff",
"terminal.ansi.dim_cyan": "#2a5f4aff",
"terminal.ansi.white": "#fcfcfcff",
"terminal.ansi.bright_white": "#fcfcfcff",
"terminal.ansi.dim_white": "#bcbec0ff",
"terminal.ansi.white": "#5c6166ff",
"terminal.ansi.bright_white": "#5c6166ff",
"terminal.ansi.dim_white": "#9c9fa2ff",
"link_text.hover": "#3b9ee5ff",
"conflict": "#f1ad49ff",
"conflict.background": "#ffeedaff",
@@ -647,11 +632,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#5c6166ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#a37accff",
"font_style": null,
@@ -707,16 +687,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#a37accff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#3b9ee5ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#86b300ff",
"font_style": null,
@@ -1033,11 +1003,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#cccac2ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#dfbfffff",
"font_style": null,
@@ -1093,16 +1058,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#dfbfffff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#72cffeff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#d4fe7fff",
"font_style": null,

View File

@@ -270,11 +270,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#d3869bff",
"font_style": null,
@@ -330,16 +325,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#fabd2eff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#b8bb25ff",
"font_style": null,
@@ -670,11 +655,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#d3869bff",
"font_style": null,
@@ -730,16 +710,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#fabd2eff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#b8bb25ff",
"font_style": null,
@@ -1070,11 +1040,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#d3869bff",
"font_style": null,
@@ -1130,16 +1095,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#fabd2eff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#b8bb25ff",
"font_style": null,
@@ -1272,9 +1227,9 @@
"terminal.foreground": "#282828ff",
"terminal.bright_foreground": "#282828ff",
"terminal.dim_foreground": "#fbf1c7ff",
"terminal.ansi.black": "#282828ff",
"terminal.ansi.bright_black": "#0b6678ff",
"terminal.ansi.dim_black": "#5f5650ff",
"terminal.ansi.black": "#fbf1c7ff",
"terminal.ansi.bright_black": "#b0a189ff",
"terminal.ansi.dim_black": "#282828ff",
"terminal.ansi.red": "#9d0308ff",
"terminal.ansi.bright_red": "#db8b7aff",
"terminal.ansi.dim_red": "#4e1207ff",
@@ -1293,9 +1248,9 @@
"terminal.ansi.cyan": "#437b59ff",
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#fbf1c7ff",
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"terminal.ansi.white": "#282828ff",
"terminal.ansi.bright_white": "#282828ff",
"terminal.ansi.dim_white": "#73675eff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",
"version_control.modified": "#b57615ff",
@@ -1470,11 +1425,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#066578ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#8f3e71ff",
"font_style": null,
@@ -1530,16 +1480,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#b57613ff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#0b6678ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#79740eff",
"font_style": null,
@@ -1672,9 +1612,9 @@
"terminal.foreground": "#282828ff",
"terminal.bright_foreground": "#282828ff",
"terminal.dim_foreground": "#f9f5d7ff",
"terminal.ansi.black": "#282828ff",
"terminal.ansi.bright_black": "#73675eff",
"terminal.ansi.dim_black": "#f9f5d7ff",
"terminal.ansi.black": "#f9f5d7ff",
"terminal.ansi.bright_black": "#b0a189ff",
"terminal.ansi.dim_black": "#282828ff",
"terminal.ansi.red": "#9d0308ff",
"terminal.ansi.bright_red": "#db8b7aff",
"terminal.ansi.dim_red": "#4e1207ff",
@@ -1693,9 +1633,9 @@
"terminal.ansi.cyan": "#437b59ff",
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#f9f5d7ff",
"terminal.ansi.bright_white": "#f9f5d7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"terminal.ansi.white": "#282828ff",
"terminal.ansi.bright_white": "#282828ff",
"terminal.ansi.dim_white": "#73675eff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",
"version_control.modified": "#b57615ff",
@@ -1870,11 +1810,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#066578ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#8f3e71ff",
"font_style": null,
@@ -1930,16 +1865,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#b57613ff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#0b6678ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#79740eff",
"font_style": null,
@@ -2072,9 +1997,9 @@
"terminal.foreground": "#282828ff",
"terminal.bright_foreground": "#282828ff",
"terminal.dim_foreground": "#f2e5bcff",
"terminal.ansi.black": "#282828ff",
"terminal.ansi.bright_black": "#73675eff",
"terminal.ansi.dim_black": "#f2e5bcff",
"terminal.ansi.black": "#f2e5bcff",
"terminal.ansi.bright_black": "#b0a189ff",
"terminal.ansi.dim_black": "#282828ff",
"terminal.ansi.red": "#9d0308ff",
"terminal.ansi.bright_red": "#db8b7aff",
"terminal.ansi.dim_red": "#4e1207ff",
@@ -2093,9 +2018,9 @@
"terminal.ansi.cyan": "#437b59ff",
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#f2e5bcff",
"terminal.ansi.bright_white": "#f2e5bcff",
"terminal.ansi.dim_white": "#b0a189ff",
"terminal.ansi.white": "#282828ff",
"terminal.ansi.bright_white": "#282828ff",
"terminal.ansi.dim_white": "#73675eff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",
"version_control.modified": "#b57615ff",
@@ -2270,11 +2195,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#066578ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#8f3e71ff",
"font_style": null,
@@ -2330,16 +2250,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#b57613ff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#0b6678ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#79740eff",
"font_style": null,

View File

@@ -264,11 +264,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#dce0e5ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#bf956aff",
"font_style": null,
@@ -324,16 +319,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#dfc184ff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#74ade8ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#a1c181ff",
"font_style": null,
@@ -465,9 +450,9 @@
"terminal.foreground": "#242529ff",
"terminal.bright_foreground": "#242529ff",
"terminal.dim_foreground": "#fafafaff",
"terminal.ansi.black": "#242529ff",
"terminal.ansi.bright_black": "#242529ff",
"terminal.ansi.dim_black": "#97979aff",
"terminal.ansi.black": "#fafafaff",
"terminal.ansi.bright_black": "#aaaaaaff",
"terminal.ansi.dim_black": "#242529ff",
"terminal.ansi.red": "#d36151ff",
"terminal.ansi.bright_red": "#f0b0a4ff",
"terminal.ansi.dim_red": "#6f312aff",
@@ -486,9 +471,9 @@
"terminal.ansi.cyan": "#3a82b7ff",
"terminal.ansi.bright_cyan": "#a3bedaff",
"terminal.ansi.dim_cyan": "#254058ff",
"terminal.ansi.white": "#fafafaff",
"terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.dim_white": "#aaaaaaff",
"terminal.ansi.white": "#242529ff",
"terminal.ansi.bright_white": "#242529ff",
"terminal.ansi.dim_white": "#97979aff",
"link_text.hover": "#5c78e2ff",
"version_control.added": "#27a657ff",
"version_control.modified": "#d3b020ff",
@@ -658,11 +643,6 @@
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#242529ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#ad6e25ff",
"font_style": null,
@@ -718,16 +698,6 @@
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#669f59ff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#5c78e2ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#649f57ff",
"font_style": null,

View File

@@ -25,6 +25,7 @@ assistant_context_editor.workspace = true
assistant_slash_command.workspace = true
assistant_slash_commands.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
audio.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
@@ -94,7 +95,6 @@ ui_input.workspace = true
urlencoding.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
@@ -102,7 +102,6 @@ zed_llm_client.workspace = true
zstd.workspace = true
[dev-dependencies]
assistant_tools.workspace = true
buffer_diff = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] }

View File

@@ -1144,10 +1144,6 @@ impl ActiveThread {
cx,
);
}
ThreadEvent::ProfileChanged => {
self.save_thread(cx);
cx.notify();
}
}
}

View File

@@ -3,7 +3,6 @@ mod agent_configuration;
mod agent_diff;
mod agent_model_selector;
mod agent_panel;
mod agent_profile;
mod buffer_codegen;
mod context;
mod context_picker;

View File

@@ -2,21 +2,25 @@ mod profile_modal_header;
use std::sync::Arc;
use agent_settings::{AgentProfileId, AgentSettings, builtin_profiles};
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles};
use assistant_tool::ToolWorkingSet;
use convert_case::{Case, Casing as _};
use editor::Editor;
use fs::Fs;
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*};
use settings::Settings as _;
use gpui::{
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity,
prelude::*,
};
use settings::{Settings as _, update_settings_file};
use ui::{
KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*,
};
use util::ResultExt as _;
use workspace::{ModalView, Workspace};
use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::agent_profile::AgentProfile;
use crate::{AgentPanel, ManageProfiles};
use crate::{AgentPanel, ManageProfiles, ThreadStore};
use super::tool_picker::ToolPickerMode;
@@ -99,6 +103,7 @@ pub struct NewProfileMode {
pub struct ManageProfilesModal {
fs: Arc<dyn Fs>,
tools: Entity<ToolWorkingSet>,
thread_store: WeakEntity<ThreadStore>,
focus_handle: FocusHandle,
mode: Mode,
}
@@ -114,8 +119,9 @@ impl ManageProfilesModal {
let fs = workspace.app_state().fs.clone();
let thread_store = panel.read(cx).thread_store();
let tools = thread_store.read(cx).tools();
let thread_store = thread_store.downgrade();
workspace.toggle_modal(window, cx, |window, cx| {
let mut this = Self::new(fs, tools, window, cx);
let mut this = Self::new(fs, tools, thread_store, window, cx);
if let Some(profile_id) = action.customize_tools.clone() {
this.configure_builtin_tools(profile_id, window, cx);
@@ -130,6 +136,7 @@ impl ManageProfilesModal {
pub fn new(
fs: Arc<dyn Fs>,
tools: Entity<ToolWorkingSet>,
thread_store: WeakEntity<ThreadStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -138,6 +145,7 @@ impl ManageProfilesModal {
Self {
fs,
tools,
thread_store,
focus_handle,
mode: Mode::choose_profile(window, cx),
}
@@ -198,6 +206,7 @@ impl ManageProfilesModal {
ToolPickerMode::McpTools,
self.fs.clone(),
self.tools.clone(),
self.thread_store.clone(),
profile_id.clone(),
profile,
cx,
@@ -235,6 +244,7 @@ impl ManageProfilesModal {
ToolPickerMode::BuiltinTools,
self.fs.clone(),
self.tools.clone(),
self.thread_store.clone(),
profile_id.clone(),
profile,
cx,
@@ -260,10 +270,32 @@ impl ManageProfilesModal {
match &self.mode {
Mode::ChooseProfile { .. } => {}
Mode::NewProfile(mode) => {
let name = mode.name_editor.read(cx).text(cx);
let settings = AgentSettings::get_global(cx);
let profile_id =
AgentProfile::create(name, mode.base_profile_id.clone(), self.fs.clone(), cx);
let base_profile = mode
.base_profile_id
.as_ref()
.and_then(|profile_id| settings.profiles.get(profile_id).cloned());
let name = mode.name_editor.read(cx).text(cx);
let profile_id = AgentProfileId(name.to_case(Case::Kebab).into());
let profile = AgentProfile {
name: name.into(),
tools: base_profile
.as_ref()
.map(|profile| profile.tools.clone())
.unwrap_or_default(),
enable_all_context_servers: base_profile
.as_ref()
.map(|profile| profile.enable_all_context_servers)
.unwrap_or_default(),
context_servers: base_profile
.map(|profile| profile.context_servers)
.unwrap_or_default(),
};
self.create_profile(profile_id.clone(), profile, cx);
self.view_profile(profile_id, window, cx);
}
Mode::ViewProfile(_) => {}
@@ -293,6 +325,19 @@ impl ManageProfilesModal {
}
}
}
fn create_profile(
&self,
profile_id: AgentProfileId,
profile: AgentProfile,
cx: &mut Context<Self>,
) {
update_settings_file::<AgentSettings>(self.fs.clone(), cx, {
move |settings, _cx| {
settings.create_profile(profile_id, profile).log_err();
}
});
}
}
impl ModalView for ManageProfilesModal {}
@@ -475,13 +520,14 @@ impl ManageProfilesModal {
) -> impl IntoElement {
let settings = AgentSettings::get_global(cx);
let profile_id = &settings.default_profile;
let profile_name = settings
.profiles
.get(&mode.profile_id)
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
let icon = match mode.profile_id.as_str() {
let icon = match profile_id.as_str() {
"write" => IconName::Pencil,
"ask" => IconName::MessageBubbles,
_ => IconName::UserRoundPen,

View File

@@ -1,17 +1,19 @@
use std::{collections::BTreeMap, sync::Arc};
use agent_settings::{
AgentProfileContent, AgentProfileId, AgentProfileSettings, AgentSettings, AgentSettingsContent,
AgentProfile, AgentProfileContent, AgentProfileId, AgentSettings, AgentSettingsContent,
ContextServerPresetContent,
};
use assistant_tool::{ToolSource, ToolWorkingSet};
use fs::Fs;
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
use picker::{Picker, PickerDelegate};
use settings::update_settings_file;
use settings::{Settings as _, update_settings_file};
use ui::{ListItem, ListItemSpacing, prelude::*};
use util::ResultExt as _;
use crate::ThreadStore;
pub struct ToolPicker {
picker: Entity<Picker<ToolPickerDelegate>>,
}
@@ -69,10 +71,11 @@ pub enum PickerItem {
pub struct ToolPickerDelegate {
tool_picker: WeakEntity<ToolPicker>,
thread_store: WeakEntity<ThreadStore>,
fs: Arc<dyn Fs>,
items: Arc<Vec<PickerItem>>,
profile_id: AgentProfileId,
profile_settings: AgentProfileSettings,
profile: AgentProfile,
filtered_items: Vec<PickerItem>,
selected_index: usize,
mode: ToolPickerMode,
@@ -83,18 +86,20 @@ impl ToolPickerDelegate {
mode: ToolPickerMode,
fs: Arc<dyn Fs>,
tool_set: Entity<ToolWorkingSet>,
thread_store: WeakEntity<ThreadStore>,
profile_id: AgentProfileId,
profile_settings: AgentProfileSettings,
profile: AgentProfile,
cx: &mut Context<ToolPicker>,
) -> Self {
let items = Arc::new(Self::resolve_items(mode, &tool_set, cx));
Self {
tool_picker: cx.entity().downgrade(),
thread_store,
fs,
items,
profile_id,
profile_settings,
profile,
filtered_items: Vec::new(),
selected_index: 0,
mode,
@@ -244,31 +249,28 @@ impl PickerDelegate for ToolPickerDelegate {
};
let is_currently_enabled = if let Some(server_id) = server_id.clone() {
let preset = self
.profile_settings
.context_servers
.entry(server_id)
.or_default();
let preset = self.profile.context_servers.entry(server_id).or_default();
let is_enabled = *preset.tools.entry(tool_name.clone()).or_default();
*preset.tools.entry(tool_name.clone()).or_default() = !is_enabled;
is_enabled
} else {
let is_enabled = *self
.profile_settings
.tools
.entry(tool_name.clone())
.or_default();
*self
.profile_settings
.tools
.entry(tool_name.clone())
.or_default() = !is_enabled;
let is_enabled = *self.profile.tools.entry(tool_name.clone()).or_default();
*self.profile.tools.entry(tool_name.clone()).or_default() = !is_enabled;
is_enabled
};
let active_profile_id = &AgentSettings::get_global(cx).default_profile;
if active_profile_id == &self.profile_id {
self.thread_store
.update(cx, |this, cx| {
this.load_profile(self.profile.clone(), cx);
})
.log_err();
}
update_settings_file::<AgentSettings>(self.fs.clone(), cx, {
let profile_id = self.profile_id.clone();
let default_profile = self.profile_settings.clone();
let default_profile = self.profile.clone();
let server_id = server_id.clone();
let tool_name = tool_name.clone();
move |settings: &mut AgentSettingsContent, _cx| {
@@ -346,18 +348,14 @@ impl PickerDelegate for ToolPickerDelegate {
),
PickerItem::Tool { name, server_id } => {
let is_enabled = if let Some(server_id) = server_id {
self.profile_settings
self.profile
.context_servers
.get(server_id.as_ref())
.and_then(|preset| preset.tools.get(name))
.copied()
.unwrap_or(self.profile_settings.enable_all_context_servers)
.unwrap_or(self.profile.enable_all_context_servers)
} else {
self.profile_settings
.tools
.get(name)
.copied()
.unwrap_or(false)
self.profile.tools.get(name).copied().unwrap_or(false)
};
Some(

View File

@@ -1378,8 +1378,7 @@ impl AgentDiff {
| ThreadEvent::CheckpointChanged
| ThreadEvent::ToolConfirmationNeeded
| ThreadEvent::ToolUseLimitReached
| ThreadEvent::CancelEditing
| ThreadEvent::ProfileChanged => {}
| ThreadEvent::CancelEditing => {}
}
}

View File

@@ -1,334 +0,0 @@
use std::sync::Arc;
use agent_settings::{AgentProfileId, AgentProfileSettings, AgentSettings};
use assistant_tool::{Tool, ToolSource, ToolWorkingSet};
use collections::IndexMap;
use convert_case::{Case, Casing};
use fs::Fs;
use gpui::{App, Entity};
use settings::{Settings, update_settings_file};
use ui::SharedString;
use util::ResultExt;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AgentProfile {
id: AgentProfileId,
tool_set: Entity<ToolWorkingSet>,
}
pub type AvailableProfiles = IndexMap<AgentProfileId, SharedString>;
impl AgentProfile {
pub fn new(id: AgentProfileId, tool_set: Entity<ToolWorkingSet>) -> Self {
Self { id, tool_set }
}
/// Saves a new profile to the settings.
pub fn create(
name: String,
base_profile_id: Option<AgentProfileId>,
fs: Arc<dyn Fs>,
cx: &App,
) -> AgentProfileId {
let id = AgentProfileId(name.to_case(Case::Kebab).into());
let base_profile =
base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned());
let profile_settings = AgentProfileSettings {
name: name.into(),
tools: base_profile
.as_ref()
.map(|profile| profile.tools.clone())
.unwrap_or_default(),
enable_all_context_servers: base_profile
.as_ref()
.map(|profile| profile.enable_all_context_servers)
.unwrap_or_default(),
context_servers: base_profile
.map(|profile| profile.context_servers)
.unwrap_or_default(),
};
update_settings_file::<AgentSettings>(fs, cx, {
let id = id.clone();
move |settings, _cx| {
settings.create_profile(id, profile_settings).log_err();
}
});
id
}
/// Returns a map of AgentProfileIds to their names
pub fn available_profiles(cx: &App) -> AvailableProfiles {
let mut profiles = AvailableProfiles::default();
for (id, profile) in AgentSettings::get_global(cx).profiles.iter() {
profiles.insert(id.clone(), profile.name.clone());
}
profiles
}
pub fn id(&self) -> &AgentProfileId {
&self.id
}
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
let Some(settings) = AgentSettings::get_global(cx).profiles.get(&self.id) else {
return Vec::new();
};
self.tool_set
.read(cx)
.tools(cx)
.into_iter()
.filter(|tool| Self::is_enabled(settings, tool.source(), tool.name()))
.collect()
}
fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool {
match source {
ToolSource::Native => *settings.tools.get(name.as_str()).unwrap_or(&false),
ToolSource::ContextServer { id } => {
if settings.enable_all_context_servers {
return true;
}
let Some(preset) = settings.context_servers.get(id.as_ref()) else {
return false;
};
*preset.tools.get(name.as_str()).unwrap_or(&false)
}
}
}
}
#[cfg(test)]
mod tests {
use agent_settings::ContextServerPreset;
use assistant_tool::ToolRegistry;
use collections::IndexMap;
use gpui::{AppContext, TestAppContext};
use http_client::FakeHttpClient;
use project::Project;
use settings::{Settings, SettingsStore};
use ui::SharedString;
use super::*;
#[gpui::test]
async fn test_enabled_built_in_tools_for_profile(cx: &mut TestAppContext) {
init_test_settings(cx);
let id = AgentProfileId::default();
let profile_settings = cx.read(|cx| {
AgentSettings::get_global(cx)
.profiles
.get(&id)
.unwrap()
.clone()
});
let tool_set = default_tool_set(cx);
let profile = AgentProfile::new(id.clone(), tool_set);
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
.into_iter()
.map(|tool| tool.name())
.collect::<Vec<_>>();
enabled_tools.sort();
let mut expected_tools = profile_settings
.tools
.into_iter()
.filter_map(|(tool, enabled)| enabled.then_some(tool.to_string()))
// Provider dependent
.filter(|tool| tool != "web_search")
.collect::<Vec<_>>();
// Plus all registered MCP tools
expected_tools.extend(["enabled_mcp_tool".into(), "disabled_mcp_tool".into()]);
expected_tools.sort();
assert_eq!(enabled_tools, expected_tools);
}
#[gpui::test]
async fn test_custom_mcp_settings(cx: &mut TestAppContext) {
init_test_settings(cx);
let id = AgentProfileId("custom_mcp".into());
let profile_settings = cx.read(|cx| {
AgentSettings::get_global(cx)
.profiles
.get(&id)
.unwrap()
.clone()
});
let tool_set = default_tool_set(cx);
let profile = AgentProfile::new(id.clone(), tool_set);
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
.into_iter()
.map(|tool| tool.name())
.collect::<Vec<_>>();
enabled_tools.sort();
let mut expected_tools = profile_settings.context_servers["mcp"]
.tools
.iter()
.filter_map(|(key, enabled)| enabled.then(|| key.to_string()))
.collect::<Vec<_>>();
expected_tools.sort();
assert_eq!(enabled_tools, expected_tools);
}
#[gpui::test]
async fn test_only_built_in(cx: &mut TestAppContext) {
init_test_settings(cx);
let id = AgentProfileId("write_minus_mcp".into());
let profile_settings = cx.read(|cx| {
AgentSettings::get_global(cx)
.profiles
.get(&id)
.unwrap()
.clone()
});
let tool_set = default_tool_set(cx);
let profile = AgentProfile::new(id.clone(), tool_set);
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
.into_iter()
.map(|tool| tool.name())
.collect::<Vec<_>>();
enabled_tools.sort();
let mut expected_tools = profile_settings
.tools
.into_iter()
.filter_map(|(tool, enabled)| enabled.then_some(tool.to_string()))
// Provider dependent
.filter(|tool| tool != "web_search")
.collect::<Vec<_>>();
expected_tools.sort();
assert_eq!(enabled_tools, expected_tools);
}
fn init_test_settings(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
Project::init_settings(cx);
AgentSettings::register(cx);
language_model::init_settings(cx);
ToolRegistry::default_global(cx);
assistant_tools::init(FakeHttpClient::with_404_response(), cx);
});
cx.update(|cx| {
let mut agent_settings = AgentSettings::get_global(cx).clone();
agent_settings.profiles.insert(
AgentProfileId("write_minus_mcp".into()),
AgentProfileSettings {
name: "write_minus_mcp".into(),
enable_all_context_servers: false,
..agent_settings.profiles[&AgentProfileId::default()].clone()
},
);
agent_settings.profiles.insert(
AgentProfileId("custom_mcp".into()),
AgentProfileSettings {
name: "mcp".into(),
tools: IndexMap::default(),
enable_all_context_servers: false,
context_servers: IndexMap::from_iter([("mcp".into(), context_server_preset())]),
},
);
AgentSettings::override_global(agent_settings, cx);
})
}
fn context_server_preset() -> ContextServerPreset {
ContextServerPreset {
tools: IndexMap::from_iter([
("enabled_mcp_tool".into(), true),
("disabled_mcp_tool".into(), false),
]),
}
}
fn default_tool_set(cx: &mut TestAppContext) -> Entity<ToolWorkingSet> {
cx.new(|_| {
let mut tool_set = ToolWorkingSet::default();
tool_set.insert(Arc::new(FakeTool::new("enabled_mcp_tool", "mcp")));
tool_set.insert(Arc::new(FakeTool::new("disabled_mcp_tool", "mcp")));
tool_set
})
}
struct FakeTool {
name: String,
source: SharedString,
}
impl FakeTool {
fn new(name: impl Into<String>, source: impl Into<SharedString>) -> Self {
Self {
name: name.into(),
source: source.into(),
}
}
}
impl Tool for FakeTool {
fn name(&self) -> String {
self.name.clone()
}
fn source(&self) -> ToolSource {
ToolSource::ContextServer {
id: self.source.clone(),
}
}
fn description(&self) -> String {
unimplemented!()
}
fn icon(&self) -> ui::IconName {
unimplemented!()
}
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
unimplemented!()
}
fn ui_text(&self, _input: &serde_json::Value) -> String {
unimplemented!()
}
fn run(
self: Arc<Self>,
_input: serde_json::Value,
_request: Arc<language_model::LanguageModelRequest>,
_project: Entity<Project>,
_action_log: Entity<assistant_tool::ActionLog>,
_model: Arc<dyn language_model::LanguageModel>,
_window: Option<gpui::AnyWindowHandle>,
_cx: &mut App,
) -> assistant_tool::ToolResult {
unimplemented!()
}
fn may_perform_edits(&self) -> bool {
unimplemented!()
}
}
}

View File

@@ -1,5 +1,7 @@
use std::cell::RefCell;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@@ -765,7 +767,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let snapshot = buffer.read(cx).snapshot();
let source_range = snapshot.anchor_before(state.source_range.start)
..snapshot.anchor_after(state.source_range.end);
..snapshot.anchor_before(state.source_range.end);
let thread_store = self.thread_store.clone();
let text_thread_store = self.text_thread_store.clone();
@@ -910,6 +912,16 @@ impl CompletionProvider for ContextPickerCompletionProvider {
})
}
fn resolve_completions(
&self,
_buffer: Entity<Buffer>,
_completion_indices: Vec<usize>,
_completions: Rc<RefCell<Box<[Completion]>>>,
_cx: &mut Context<Editor>,
) -> Task<Result<bool>> {
Task::ready(Ok(true))
}
fn is_completion_trigger(
&self,
buffer: &Entity<language::Buffer>,
@@ -1065,7 +1077,7 @@ mod tests {
use project::{Project, ProjectPath};
use serde_json::json;
use settings::SettingsStore;
use std::{ops::Deref, rc::Rc};
use std::ops::Deref;
use util::{path, separator};
use workspace::{AppState, Item};

View File

@@ -104,15 +104,7 @@ impl Tool for ContextServerTool {
tool_name,
arguments
);
let response = protocol
.request::<context_server::types::request::CallTool>(
context_server::types::CallToolParams {
name: tool_name,
arguments,
meta: None,
},
)
.await?;
let response = protocol.run_tool(tool_name, arguments).await?;
let mut result = String::new();
for content in response.content {

View File

@@ -1011,7 +1011,7 @@ impl InlineAssistant {
self.update_editor_highlights(&editor, cx);
}
} else {
entry.get_mut().highlight_updates.send(()).ok();
entry.get().highlight_updates.send(()).ok();
}
}
@@ -1519,7 +1519,7 @@ impl InlineAssistant {
struct EditorInlineAssists {
assist_ids: Vec<InlineAssistId>,
scroll_lock: Option<InlineAssistScrollLock>,
highlight_updates: watch::Sender<()>,
highlight_updates: async_watch::Sender<()>,
_update_highlights: Task<Result<()>>,
_subscriptions: Vec<gpui::Subscription>,
}
@@ -1531,7 +1531,7 @@ struct InlineAssistScrollLock {
impl EditorInlineAssists {
fn new(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) -> Self {
let (highlight_updates_tx, mut highlight_updates_rx) = watch::channel(());
let (highlight_updates_tx, mut highlight_updates_rx) = async_watch::channel(());
Self {
assist_ids: Vec::new(),
scroll_lock: None,
@@ -1689,7 +1689,7 @@ impl InlineAssist {
if let Some(editor) = editor.upgrade() {
InlineAssistant::update_global(cx, |this, cx| {
if let Some(editor_assists) =
this.assists_by_editor.get_mut(&editor.downgrade())
this.assists_by_editor.get(&editor.downgrade())
{
editor_assists.highlight_updates.send(()).ok();
}

View File

@@ -175,7 +175,8 @@ impl MessageEditor {
)
});
let incompatible_tools = cx.new(|cx| IncompatibleToolsState::new(thread.clone(), cx));
let incompatible_tools =
cx.new(|cx| IncompatibleToolsState::new(thread.read(cx).tools().clone(), cx));
let subscriptions = vec![
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
@@ -203,8 +204,15 @@ impl MessageEditor {
)
});
let profile_selector =
cx.new(|cx| ProfileSelector::new(fs, thread.clone(), editor.focus_handle(cx), cx));
let profile_selector = cx.new(|cx| {
ProfileSelector::new(
fs,
thread.clone(),
thread_store,
editor.focus_handle(cx),
cx,
)
});
Self {
editor: editor.clone(),

View File

@@ -1,24 +1,26 @@
use std::sync::Arc;
use agent_settings::{AgentDockPosition, AgentProfileId, AgentSettings, builtin_profiles};
use agent_settings::{
AgentDockPosition, AgentProfile, AgentProfileId, AgentSettings, GroupedAgentProfiles,
builtin_profiles,
};
use fs::Fs;
use gpui::{Action, Empty, Entity, FocusHandle, Subscription, prelude::*};
use gpui::{Action, Empty, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
use language_model::LanguageModelRegistry;
use settings::{Settings as _, SettingsStore, update_settings_file};
use ui::{
ContextMenu, ContextMenuEntry, DocumentationSide, PopoverMenu, PopoverMenuHandle, Tooltip,
prelude::*,
};
use util::ResultExt as _;
use crate::{
ManageProfiles, Thread, ToggleProfileSelector,
agent_profile::{AgentProfile, AvailableProfiles},
};
use crate::{ManageProfiles, Thread, ThreadStore, ToggleProfileSelector};
pub struct ProfileSelector {
profiles: AvailableProfiles,
profiles: GroupedAgentProfiles,
fs: Arc<dyn Fs>,
thread: Entity<Thread>,
thread_store: WeakEntity<ThreadStore>,
menu_handle: PopoverMenuHandle<ContextMenu>,
focus_handle: FocusHandle,
_subscriptions: Vec<Subscription>,
@@ -28,6 +30,7 @@ impl ProfileSelector {
pub fn new(
fs: Arc<dyn Fs>,
thread: Entity<Thread>,
thread_store: WeakEntity<ThreadStore>,
focus_handle: FocusHandle,
cx: &mut Context<Self>,
) -> Self {
@@ -36,9 +39,10 @@ impl ProfileSelector {
});
Self {
profiles: AgentProfile::available_profiles(cx),
profiles: GroupedAgentProfiles::from_settings(AgentSettings::get_global(cx)),
fs,
thread,
thread_store,
menu_handle: PopoverMenuHandle::default(),
focus_handle,
_subscriptions: vec![settings_subscription],
@@ -50,7 +54,7 @@ impl ProfileSelector {
}
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
self.profiles = AgentProfile::available_profiles(cx);
self.profiles = GroupedAgentProfiles::from_settings(AgentSettings::get_global(cx));
}
fn build_context_menu(
@@ -60,30 +64,21 @@ impl ProfileSelector {
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |mut menu, _window, cx| {
let settings = AgentSettings::get_global(cx);
let mut found_non_builtin = false;
for (profile_id, profile_name) in self.profiles.iter() {
if !builtin_profiles::is_builtin(profile_id) {
found_non_builtin = true;
continue;
}
for (profile_id, profile) in self.profiles.builtin.iter() {
menu = menu.item(self.menu_entry_for_profile(
profile_id.clone(),
profile_name,
profile,
settings,
cx,
));
}
if found_non_builtin {
if !self.profiles.custom.is_empty() {
menu = menu.separator().header("Custom Profiles");
for (profile_id, profile_name) in self.profiles.iter() {
if builtin_profiles::is_builtin(profile_id) {
continue;
}
for (profile_id, profile) in self.profiles.custom.iter() {
menu = menu.item(self.menu_entry_for_profile(
profile_id.clone(),
profile_name,
profile,
settings,
cx,
));
@@ -104,20 +99,19 @@ impl ProfileSelector {
fn menu_entry_for_profile(
&self,
profile_id: AgentProfileId,
profile_name: &SharedString,
profile: &AgentProfile,
settings: &AgentSettings,
cx: &App,
_cx: &App,
) -> ContextMenuEntry {
let documentation = match profile_name.to_lowercase().as_str() {
let documentation = match profile.name.to_lowercase().as_str() {
builtin_profiles::WRITE => Some("Get help to write anything."),
builtin_profiles::ASK => Some("Chat about your codebase."),
builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
_ => None,
};
let thread_profile_id = self.thread.read(cx).profile().id();
let entry = ContextMenuEntry::new(profile_name.clone())
.toggleable(IconPosition::End, &profile_id == thread_profile_id);
let entry = ContextMenuEntry::new(profile.name.clone())
.toggleable(IconPosition::End, profile_id == settings.default_profile);
let entry = if let Some(doc_text) = documentation {
entry.documentation_aside(documentation_side(settings.dock), move |_| {
@@ -129,7 +123,7 @@ impl ProfileSelector {
entry.handler({
let fs = self.fs.clone();
let thread = self.thread.clone();
let thread_store = self.thread_store.clone();
let profile_id = profile_id.clone();
move |_window, cx| {
update_settings_file::<AgentSettings>(fs.clone(), cx, {
@@ -139,9 +133,11 @@ impl ProfileSelector {
}
});
thread.update(cx, |this, cx| {
this.set_profile(profile_id.clone(), cx);
});
thread_store
.update(cx, |this, cx| {
this.load_profile_by_id(profile_id.clone(), cx);
})
.log_err();
}
})
}
@@ -150,7 +146,7 @@ impl ProfileSelector {
impl Render for ProfileSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AgentSettings::get_global(cx);
let profile_id = self.thread.read(cx).profile().id();
let profile_id = &settings.default_profile;
let profile = settings.profiles.get(profile_id);
let selected_profile = profile

View File

@@ -4,7 +4,7 @@ use std::ops::Range;
use std::sync::Arc;
use std::time::Instant;
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
use agent_settings::{AgentSettings, CompletionMode};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
@@ -41,7 +41,6 @@ use uuid::Uuid;
use zed_llm_client::{CompletionIntent, CompletionRequestStatus};
use crate::ThreadStore;
use crate::agent_profile::AgentProfile;
use crate::context::{AgentContext, AgentContextHandle, ContextLoadResult, LoadedContext};
use crate::thread_store::{
SerializedCrease, SerializedLanguageModel, SerializedMessage, SerializedMessageSegment,
@@ -361,7 +360,6 @@ pub struct Thread {
>,
remaining_turns: u32,
configured_model: Option<ConfiguredModel>,
profile: AgentProfile,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -409,7 +407,6 @@ impl Thread {
) -> Self {
let (detailed_summary_tx, detailed_summary_rx) = postage::watch::channel();
let configured_model = LanguageModelRegistry::read_global(cx).default_model();
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
Self {
id: ThreadId::new(),
@@ -452,7 +449,6 @@ impl Thread {
request_callback: None,
remaining_turns: u32::MAX,
configured_model,
profile: AgentProfile::new(profile_id, tools),
}
}
@@ -499,9 +495,6 @@ impl Thread {
let completion_mode = serialized
.completion_mode
.unwrap_or_else(|| AgentSettings::get_global(cx).preferred_completion_mode);
let profile_id = serialized
.profile
.unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone());
Self {
id,
@@ -561,7 +554,7 @@ impl Thread {
pending_checkpoint: None,
project: project.clone(),
prompt_builder,
tools: tools.clone(),
tools,
tool_use,
action_log: cx.new(|_| ActionLog::new(project)),
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
@@ -577,7 +570,6 @@ impl Thread {
request_callback: None,
remaining_turns: u32::MAX,
configured_model,
profile: AgentProfile::new(profile_id, tools),
}
}
@@ -593,17 +585,6 @@ impl Thread {
&self.id
}
pub fn profile(&self) -> &AgentProfile {
&self.profile
}
pub fn set_profile(&mut self, id: AgentProfileId, cx: &mut Context<Self>) {
if &id != self.profile.id() {
self.profile = AgentProfile::new(id, self.tools.clone());
cx.emit(ThreadEvent::ProfileChanged);
}
}
pub fn is_empty(&self) -> bool {
self.messages.is_empty()
}
@@ -938,7 +919,8 @@ impl Thread {
model: Arc<dyn LanguageModel>,
) -> Vec<LanguageModelRequestTool> {
if model.supports_tools() {
self.profile
self.tools()
.read(cx)
.enabled_tools(cx)
.into_iter()
.filter_map(|tool| {
@@ -1198,7 +1180,6 @@ impl Thread {
}),
completion_mode: Some(this.completion_mode),
tool_use_limit_reached: this.tool_use_limit_reached,
profile: Some(this.profile.id().clone()),
})
})
}
@@ -2140,7 +2121,7 @@ impl Thread {
window: Option<AnyWindowHandle>,
cx: &mut Context<Thread>,
) {
let available_tools = self.profile.enabled_tools(cx);
let available_tools = self.tools.read(cx).enabled_tools(cx);
let tool_list = available_tools
.iter()
@@ -2232,15 +2213,19 @@ impl Thread {
) -> Task<()> {
let tool_name: Arc<str> = tool.name().into();
let tool_result = tool.run(
input,
request,
self.project.clone(),
self.action_log.clone(),
model,
window,
cx,
);
let tool_result = if self.tools.read(cx).is_disabled(&tool.source(), &tool_name) {
Task::ready(Err(anyhow!("tool is disabled: {tool_name}"))).into()
} else {
tool.run(
input,
request,
self.project.clone(),
self.action_log.clone(),
model,
window,
cx,
)
};
// Store the card separately if it exists
if let Some(card) = tool_result.card.clone() {
@@ -2359,7 +2344,8 @@ impl Thread {
let client = self.project.read(cx).client();
let enabled_tool_names: Vec<String> = self
.profile
.tools()
.read(cx)
.enabled_tools(cx)
.iter()
.map(|tool| tool.name())
@@ -2872,7 +2858,6 @@ pub enum ThreadEvent {
ToolUseLimitReached,
CancelEditing,
CompletionCanceled,
ProfileChanged,
}
impl EventEmitter<ThreadEvent> for Thread {}
@@ -2887,7 +2872,7 @@ struct PendingCompletion {
mod tests {
use super::*;
use crate::{ThreadStore, context::load_context, context_store::ContextStore, thread_store};
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelParameters};
use agent_settings::{AgentSettings, LanguageModelParameters};
use assistant_tool::ToolRegistry;
use editor::EditorSettings;
use gpui::TestAppContext;
@@ -3300,71 +3285,6 @@ fn main() {{
);
}
#[gpui::test]
async fn test_storing_profile_setting_per_thread(cx: &mut TestAppContext) {
init_test_settings(cx);
let project = create_test_project(
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
.await;
let (_workspace, thread_store, thread, _context_store, _model) =
setup_test_environment(cx, project.clone()).await;
// Check that we are starting with the default profile
let profile = cx.read(|cx| thread.read(cx).profile.clone());
let tool_set = cx.read(|cx| thread_store.read(cx).tools());
assert_eq!(
profile,
AgentProfile::new(AgentProfileId::default(), tool_set)
);
}
#[gpui::test]
async fn test_serializing_thread_profile(cx: &mut TestAppContext) {
init_test_settings(cx);
let project = create_test_project(
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
.await;
let (_workspace, thread_store, thread, _context_store, _model) =
setup_test_environment(cx, project.clone()).await;
// Profile gets serialized with default values
let serialized = thread
.update(cx, |thread, cx| thread.serialize(cx))
.await
.unwrap();
assert_eq!(serialized.profile, Some(AgentProfileId::default()));
let deserialized = cx.update(|cx| {
thread.update(cx, |thread, cx| {
Thread::deserialize(
thread.id.clone(),
serialized,
thread.project.clone(),
thread.tools.clone(),
thread.prompt_builder.clone(),
thread.project_context.clone(),
None,
cx,
)
})
});
let tool_set = cx.read(|cx| thread_store.read(cx).tools());
assert_eq!(
deserialized.profile,
AgentProfile::new(AgentProfileId::default(), tool_set)
);
}
#[gpui::test]
async fn test_temperature_setting(cx: &mut TestAppContext) {
init_test_settings(cx);

View File

@@ -3,9 +3,9 @@ use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use agent_settings::{AgentProfileId, CompletionMode};
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, CompletionMode};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ToolId, ToolWorkingSet};
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
use context_server::ContextServerId;
@@ -25,6 +25,7 @@ use prompt_store::{
UserRulesContext, WorktreeContext,
};
use serde::{Deserialize, Serialize};
use settings::{Settings as _, SettingsStore};
use ui::Window;
use util::ResultExt as _;
@@ -146,7 +147,12 @@ impl ThreadStore {
prompt_store: Option<Entity<PromptStore>>,
cx: &mut Context<Self>,
) -> (Self, oneshot::Receiver<()>) {
let mut subscriptions = vec![cx.subscribe(&project, Self::handle_project_event)];
let mut subscriptions = vec![
cx.observe_global::<SettingsStore>(move |this: &mut Self, cx| {
this.load_default_profile(cx);
}),
cx.subscribe(&project, Self::handle_project_event),
];
if let Some(prompt_store) = prompt_store.as_ref() {
subscriptions.push(cx.subscribe(
@@ -194,6 +200,7 @@ impl ThreadStore {
_reload_system_prompt_task: reload_system_prompt_task,
_subscriptions: subscriptions,
};
this.load_default_profile(cx);
this.register_context_server_handlers(cx);
this.reload(cx).detach_and_log_err(cx);
(this, ready_rx)
@@ -513,17 +520,94 @@ impl ThreadStore {
})
}
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
let context_server_store = self.project.read(cx).context_server_store();
cx.subscribe(&context_server_store, Self::handle_context_server_event)
.detach();
fn load_default_profile(&self, cx: &mut Context<Self>) {
let assistant_settings = AgentSettings::get_global(cx);
// Check for any servers that were already running before the handler was registered
for server in context_server_store.read(cx).running_servers() {
self.load_context_server_tools(server.id(), context_server_store.clone(), cx);
self.load_profile_by_id(assistant_settings.default_profile.clone(), cx);
}
pub fn load_profile_by_id(&self, profile_id: AgentProfileId, cx: &mut Context<Self>) {
let assistant_settings = AgentSettings::get_global(cx);
if let Some(profile) = assistant_settings.profiles.get(&profile_id) {
self.load_profile(profile.clone(), cx);
}
}
pub fn load_profile(&self, profile: AgentProfile, cx: &mut Context<Self>) {
self.tools.update(cx, |tools, cx| {
tools.disable_all_tools(cx);
tools.enable(
ToolSource::Native,
&profile
.tools
.into_iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool))
.collect::<Vec<_>>(),
cx,
);
});
if profile.enable_all_context_servers {
for context_server_id in self
.project
.read(cx)
.context_server_store()
.read(cx)
.all_server_ids()
{
self.tools.update(cx, |tools, cx| {
tools.enable_source(
ToolSource::ContextServer {
id: context_server_id.0.into(),
},
cx,
);
});
}
// Enable all the tools from all context servers, but disable the ones that are explicitly disabled
for (context_server_id, preset) in profile.context_servers {
self.tools.update(cx, |tools, cx| {
tools.disable(
ToolSource::ContextServer {
id: context_server_id.into(),
},
&preset
.tools
.into_iter()
.filter_map(|(tool, enabled)| (!enabled).then(|| tool))
.collect::<Vec<_>>(),
cx,
)
})
}
} else {
for (context_server_id, preset) in profile.context_servers {
self.tools.update(cx, |tools, cx| {
tools.enable(
ToolSource::ContextServer {
id: context_server_id.into(),
},
&preset
.tools
.into_iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool))
.collect::<Vec<_>>(),
cx,
)
})
}
}
}
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
cx.subscribe(
&self.project.read(cx).context_server_store(),
Self::handle_context_server_event,
)
.detach();
}
fn handle_context_server_event(
&mut self,
context_server_store: Entity<ContextServerStore>,
@@ -534,71 +618,71 @@ impl ThreadStore {
match event {
project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
match status {
ContextServerStatus::Starting => {}
ContextServerStatus::Running => {
self.load_context_server_tools(server_id.clone(), context_server_store, cx);
if let Some(server) =
context_server_store.read(cx).get_running_server(server_id)
{
let context_server_manager = context_server_store.clone();
cx.spawn({
let server = server.clone();
let server_id = server_id.clone();
async move |this, cx| {
let Some(protocol) = server.client() else {
return;
};
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
if let Some(tools) = protocol.list_tools().await.log_err() {
let tool_ids = tool_working_set
.update(cx, |tool_working_set, _| {
tools
.tools
.into_iter()
.map(|tool| {
log::info!(
"registering context server tool: {:?}",
tool.name
);
tool_working_set.insert(Arc::new(
ContextServerTool::new(
context_server_manager.clone(),
server.id(),
tool,
),
))
})
.collect::<Vec<_>>()
})
.log_err();
if let Some(tool_ids) = tool_ids {
this.update(cx, |this, cx| {
this.context_server_tool_ids
.insert(server_id, tool_ids);
this.load_default_profile(cx);
})
.log_err();
}
}
}
}
})
.detach();
}
}
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
tool_working_set.update(cx, |tool_working_set, _| {
tool_working_set.remove(&tool_ids);
});
self.load_default_profile(cx);
}
}
_ => {}
}
}
}
}
fn load_context_server_tools(
&self,
server_id: ContextServerId,
context_server_store: Entity<ContextServerStore>,
cx: &mut Context<Self>,
) {
let Some(server) = context_server_store.read(cx).get_running_server(&server_id) else {
return;
};
let tool_working_set = self.tools.clone();
cx.spawn(async move |this, cx| {
let Some(protocol) = server.client() else {
return;
};
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
if let Some(response) = protocol
.request::<context_server::types::request::ListTools>(())
.await
.log_err()
{
let tool_ids = tool_working_set
.update(cx, |tool_working_set, _| {
response
.tools
.into_iter()
.map(|tool| {
log::info!("registering context server tool: {:?}", tool.name);
tool_working_set.insert(Arc::new(ContextServerTool::new(
context_server_store.clone(),
server.id(),
tool,
)))
})
.collect::<Vec<_>>()
})
.log_err();
if let Some(tool_ids) = tool_ids {
this.update(cx, |this, _| {
this.context_server_tool_ids.insert(server_id, tool_ids);
})
.log_err();
}
}
}
})
.detach();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -630,8 +714,6 @@ pub struct SerializedThread {
pub completion_mode: Option<CompletionMode>,
#[serde(default)]
pub tool_use_limit_reached: bool,
#[serde(default)]
pub profile: Option<AgentProfileId>,
}
#[derive(Serialize, Deserialize, Debug)]
@@ -774,7 +856,6 @@ impl LegacySerializedThread {
model: None,
completion_mode: None,
tool_use_limit_reached: false,
profile: None,
}
}
}

View File

@@ -1,33 +1,30 @@
use std::sync::Arc;
use assistant_tool::{Tool, ToolSource};
use assistant_tool::{Tool, ToolSource, ToolWorkingSet, ToolWorkingSetEvent};
use collections::HashMap;
use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window};
use language_model::{LanguageModel, LanguageModelToolSchemaFormat};
use ui::prelude::*;
use crate::{Thread, ThreadEvent};
pub struct IncompatibleToolsState {
cache: HashMap<LanguageModelToolSchemaFormat, Vec<Arc<dyn Tool>>>,
thread: Entity<Thread>,
_thread_subscription: Subscription,
tool_working_set: Entity<ToolWorkingSet>,
_tool_working_set_subscription: Subscription,
}
impl IncompatibleToolsState {
pub fn new(thread: Entity<Thread>, cx: &mut Context<Self>) -> Self {
pub fn new(tool_working_set: Entity<ToolWorkingSet>, cx: &mut Context<Self>) -> Self {
let _tool_working_set_subscription =
cx.subscribe(&thread, |this, _, event, _| match event {
ThreadEvent::ProfileChanged => {
cx.subscribe(&tool_working_set, |this, _, event, _| match event {
ToolWorkingSetEvent::EnabledToolsChanged => {
this.cache.clear();
}
_ => {}
});
Self {
cache: HashMap::default(),
thread,
_thread_subscription: _tool_working_set_subscription,
tool_working_set,
_tool_working_set_subscription,
}
}
@@ -39,9 +36,8 @@ impl IncompatibleToolsState {
self.cache
.entry(model.tool_input_format())
.or_insert_with(|| {
self.thread
self.tool_working_set
.read(cx)
.profile()
.enabled_tools(cx)
.iter()
.filter(|tool| tool.input_schema(model.tool_input_format()).is_err())

View File

@@ -16,6 +16,7 @@ anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
collections.workspace = true
gpui.workspace = true
indexmap.workspace = true
language_model.workspace = true
lmstudio = { workspace = true, features = ["schemars"] }
log.workspace = true

View File

@@ -17,6 +17,29 @@ pub mod builtin_profiles {
}
}
#[derive(Default)]
pub struct GroupedAgentProfiles {
pub builtin: IndexMap<AgentProfileId, AgentProfile>,
pub custom: IndexMap<AgentProfileId, AgentProfile>,
}
impl GroupedAgentProfiles {
pub fn from_settings(settings: &crate::AgentSettings) -> Self {
let mut builtin = IndexMap::default();
let mut custom = IndexMap::default();
for (profile_id, profile) in settings.profiles.clone() {
if builtin_profiles::is_builtin(&profile_id) {
builtin.insert(profile_id, profile);
} else {
custom.insert(profile_id, profile);
}
}
Self { builtin, custom }
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AgentProfileId(pub Arc<str>);
@@ -40,7 +63,7 @@ impl Default for AgentProfileId {
/// A profile for the Zed Agent that controls its behavior.
#[derive(Debug, Clone)]
pub struct AgentProfileSettings {
pub struct AgentProfile {
/// The name of the profile.
pub name: SharedString,
pub tools: IndexMap<Arc<str>, bool>,

View File

@@ -102,7 +102,7 @@ pub struct AgentSettings {
pub using_outdated_settings_version: bool,
pub default_profile: AgentProfileId,
pub default_view: DefaultView,
pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,
pub profiles: IndexMap<AgentProfileId, AgentProfile>,
pub always_allow_tool_actions: bool,
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
pub play_sound_when_agent_done: bool,
@@ -531,7 +531,7 @@ impl AgentSettingsContent {
pub fn create_profile(
&mut self,
profile_id: AgentProfileId,
profile_settings: AgentProfileSettings,
profile: AgentProfile,
) -> Result<()> {
self.v2_setting(|settings| {
let profiles = settings.profiles.get_or_insert_default();
@@ -542,10 +542,10 @@ impl AgentSettingsContent {
profiles.insert(
profile_id,
AgentProfileContent {
name: profile_settings.name.into(),
tools: profile_settings.tools,
enable_all_context_servers: Some(profile_settings.enable_all_context_servers),
context_servers: profile_settings
name: profile.name.into(),
tools: profile.tools,
enable_all_context_servers: Some(profile.enable_all_context_servers),
context_servers: profile
.context_servers
.into_iter()
.map(|(server_id, preset)| {
@@ -910,7 +910,7 @@ impl Settings for AgentSettings {
.extend(profiles.into_iter().map(|(id, profile)| {
(
id,
AgentProfileSettings {
AgentProfile {
name: profile.name.into(),
tools: profile.tools,
enable_all_context_servers: profile

View File

@@ -809,37 +809,74 @@ impl ContextStore {
}
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
let context_server_store = self.project.read(cx).context_server_store();
cx.subscribe(&context_server_store, Self::handle_context_server_event)
.detach();
// Check for any servers that were already running before the handler was registered
for server in context_server_store.read(cx).running_servers() {
self.load_context_server_slash_commands(server.id(), context_server_store.clone(), cx);
}
cx.subscribe(
&self.project.read(cx).context_server_store(),
Self::handle_context_server_event,
)
.detach();
}
fn handle_context_server_event(
&mut self,
context_server_store: Entity<ContextServerStore>,
context_server_manager: Entity<ContextServerStore>,
event: &project::context_server_store::Event,
cx: &mut Context<Self>,
) {
let slash_command_working_set = self.slash_commands.clone();
match event {
project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
match status {
ContextServerStatus::Running => {
self.load_context_server_slash_commands(
server_id.clone(),
context_server_store.clone(),
cx,
);
if let Some(server) = context_server_manager
.read(cx)
.get_running_server(server_id)
{
let context_server_manager = context_server_manager.clone();
cx.spawn({
let server = server.clone();
let server_id = server_id.clone();
async move |this, cx| {
let Some(protocol) = server.client() else {
return;
};
if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
if let Some(prompts) = protocol.list_prompts().await.log_err() {
let slash_command_ids = prompts
.into_iter()
.filter(assistant_slash_commands::acceptable_prompt)
.map(|prompt| {
log::info!(
"registering context server command: {:?}",
prompt.name
);
slash_command_working_set.insert(Arc::new(
assistant_slash_commands::ContextServerSlashCommand::new(
context_server_manager.clone(),
server.id(),
prompt,
),
))
})
.collect::<Vec<_>>();
this.update( cx, |this, _cx| {
this.context_server_slash_command_ids
.insert(server_id.clone(), slash_command_ids);
})
.log_err();
}
}
}
})
.detach();
}
}
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
if let Some(slash_command_ids) =
self.context_server_slash_command_ids.remove(server_id)
{
self.slash_commands.remove(&slash_command_ids);
slash_command_working_set.remove(&slash_command_ids);
}
}
_ => {}
@@ -847,52 +884,4 @@ impl ContextStore {
}
}
}
fn load_context_server_slash_commands(
&self,
server_id: ContextServerId,
context_server_store: Entity<ContextServerStore>,
cx: &mut Context<Self>,
) {
let Some(server) = context_server_store.read(cx).get_running_server(&server_id) else {
return;
};
let slash_command_working_set = self.slash_commands.clone();
cx.spawn(async move |this, cx| {
let Some(protocol) = server.client() else {
return;
};
if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
if let Some(response) = protocol
.request::<context_server::types::request::PromptsList>(())
.await
.log_err()
{
let slash_command_ids = response
.prompts
.into_iter()
.filter(assistant_slash_commands::acceptable_prompt)
.map(|prompt| {
log::info!("registering context server command: {:?}", prompt.name);
slash_command_working_set.insert(Arc::new(
assistant_slash_commands::ContextServerSlashCommand::new(
context_server_store.clone(),
server.id(),
prompt,
),
))
})
.collect::<Vec<_>>();
this.update(cx, |this, _cx| {
this.context_server_slash_command_ids
.insert(server_id.clone(), slash_command_ids);
})
.log_err();
}
}
})
.detach();
}
}

View File

@@ -10,7 +10,9 @@ use parking_lot::Mutex;
use project::{CompletionIntent, CompletionSource, lsp_store::CompletionDocumentation};
use rope::Point;
use std::{
cell::RefCell,
ops::Range,
rc::Rc,
sync::{
Arc,
atomic::{AtomicBool, Ordering::SeqCst},
@@ -238,14 +240,13 @@ impl SlashCommandCompletionProvider {
Ok(vec![project::CompletionResponse {
completions,
// TODO: Could have slash commands indicate whether their completions are incomplete.
is_incomplete: true,
is_incomplete: false,
}])
})
} else {
Task::ready(Ok(vec![project::CompletionResponse {
completions: Vec::new(),
is_incomplete: true,
is_incomplete: false,
}]))
}
}
@@ -274,17 +275,17 @@ impl CompletionProvider for SlashCommandCompletionProvider {
position.row,
call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
);
let command_range = buffer.anchor_before(command_range_start)
let command_range = buffer.anchor_after(command_range_start)
..buffer.anchor_after(command_range_end);
let name = line[call.name.clone()].to_string();
let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
{
let last_arg_start =
buffer.anchor_before(Point::new(position.row, argument.start as u32));
buffer.anchor_after(Point::new(position.row, argument.start as u32));
let first_arg_start = call.arguments.first().expect("we have the last element");
let first_arg_start = buffer
.anchor_before(Point::new(position.row, first_arg_start.start as u32));
let first_arg_start =
buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
let arguments = call
.arguments
.into_iter()
@@ -297,7 +298,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
)
} else {
let start =
buffer.anchor_before(Point::new(position.row, call.name.start as u32));
buffer.anchor_after(Point::new(position.row, call.name.start as u32));
(None, start..buffer_position)
};
@@ -325,6 +326,16 @@ impl CompletionProvider for SlashCommandCompletionProvider {
}
}
fn resolve_completions(
&self,
_: Entity<Buffer>,
_: Vec<usize>,
_: Rc<RefCell<Box<[project::Completion]>>>,
_: &mut Context<Editor>,
) -> Task<Result<bool>> {
Task::ready(Ok(true))
}
fn is_completion_trigger(
&self,
buffer: &Entity<Buffer>,

View File

@@ -86,26 +86,20 @@ impl SlashCommand for ContextServerSlashCommand {
cx.foreground_executor().spawn(async move {
let protocol = server.client().context("Context server not initialized")?;
let response = protocol
.request::<context_server::types::request::CompletionComplete>(
context_server::types::CompletionCompleteParams {
reference: context_server::types::CompletionReference::Prompt(
context_server::types::PromptReference {
ty: context_server::types::PromptReferenceType::Prompt,
name: prompt_name,
},
),
argument: context_server::types::CompletionArgument {
name: arg_name,
value: arg_value,
let completion_result = protocol
.completion(
context_server::types::CompletionReference::Prompt(
context_server::types::PromptReference {
r#type: context_server::types::PromptReferenceType::Prompt,
name: prompt_name,
},
meta: None,
},
),
arg_name,
arg_value,
)
.await?;
let completions = response
.completion
let completions = completion_result
.values
.into_iter()
.map(|value| ArgumentCompletion {
@@ -144,18 +138,10 @@ impl SlashCommand for ContextServerSlashCommand {
if let Some(server) = store.get_running_server(&server_id) {
cx.foreground_executor().spawn(async move {
let protocol = server.client().context("Context server not initialized")?;
let response = protocol
.request::<context_server::types::request::PromptsGet>(
context_server::types::PromptsGetParams {
name: prompt_name.clone(),
arguments: Some(prompt_args),
meta: None,
},
)
.await?;
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
anyhow::ensure!(
response
result
.messages
.iter()
.all(|msg| matches!(msg.role, context_server::types::Role::User)),
@@ -163,7 +149,7 @@ impl SlashCommand for ContextServerSlashCommand {
);
// Extract text from user messages into a single prompt string
let mut prompt = response
let mut prompt = result
.messages
.into_iter()
.filter_map(|msg| match msg.content {
@@ -181,7 +167,7 @@ impl SlashCommand for ContextServerSlashCommand {
range: 0..(prompt.len()),
icon: IconName::ZedAssistant,
label: SharedString::from(
response
result
.description
.unwrap_or(format!("Result from {}", prompt_name)),
),

View File

@@ -13,6 +13,7 @@ path = "src/assistant_tool.rs"
[dependencies]
anyhow.workspace = true
async-watch.workspace = true
buffer_diff.workspace = true
clock.workspace = true
collections.workspace = true
@@ -29,7 +30,6 @@ serde.workspace = true
serde_json.workspace = true
text.workspace = true
util.workspace = true
watch.workspace = true
workspace.workspace = true
workspace-hack.workspace = true

View File

@@ -204,7 +204,7 @@ impl ActionLog {
git_store.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
})?;
let (mut git_diff_updates_tx, mut git_diff_updates_rx) = watch::channel(());
let (git_diff_updates_tx, mut git_diff_updates_rx) = async_watch::channel(());
let _repo_subscription =
if let Some((git_diff, (buffer_repo, _))) = git_diff.as_ref().zip(buffer_repo) {
cx.update(|cx| {

View File

@@ -214,7 +214,7 @@ pub trait Tool: 'static + Send + Sync {
ToolSource::Native
}
/// Returns true if the tool needs the users's confirmation
/// Returns true iff the tool needs the users's confirmation
/// before having permission to run.
fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool;

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use collections::{HashMap, IndexMap};
use gpui::App;
use collections::{HashMap, HashSet, IndexMap};
use gpui::{App, Context, EventEmitter};
use crate::{Tool, ToolRegistry, ToolSource};
@@ -13,9 +13,17 @@ pub struct ToolId(usize);
pub struct ToolWorkingSet {
context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
enabled_sources: HashSet<ToolSource>,
enabled_tools_by_source: HashMap<ToolSource, HashSet<Arc<str>>>,
next_tool_id: ToolId,
}
pub enum ToolWorkingSetEvent {
EnabledToolsChanged,
}
impl EventEmitter<ToolWorkingSetEvent> for ToolWorkingSet {}
impl ToolWorkingSet {
pub fn tool(&self, name: &str, cx: &App) -> Option<Arc<dyn Tool>> {
self.context_server_tools_by_name
@@ -49,6 +57,42 @@ impl ToolWorkingSet {
tools_by_source
}
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
let all_tools = self.tools(cx);
all_tools
.into_iter()
.filter(|tool| self.is_enabled(&tool.source(), &tool.name().into()))
.collect()
}
pub fn disable_all_tools(&mut self, cx: &mut Context<Self>) {
self.enabled_tools_by_source.clear();
cx.emit(ToolWorkingSetEvent::EnabledToolsChanged);
}
pub fn enable_source(&mut self, source: ToolSource, cx: &mut Context<Self>) {
self.enabled_sources.insert(source.clone());
let tools_by_source = self.tools_by_source(cx);
if let Some(tools) = tools_by_source.get(&source) {
self.enabled_tools_by_source.insert(
source,
tools
.into_iter()
.map(|tool| tool.name().into())
.collect::<HashSet<_>>(),
);
}
cx.emit(ToolWorkingSetEvent::EnabledToolsChanged);
}
pub fn disable_source(&mut self, source: &ToolSource, cx: &mut Context<Self>) {
self.enabled_sources.remove(source);
self.enabled_tools_by_source.remove(source);
cx.emit(ToolWorkingSetEvent::EnabledToolsChanged);
}
pub fn insert(&mut self, tool: Arc<dyn Tool>) -> ToolId {
let tool_id = self.next_tool_id;
self.next_tool_id.0 += 1;
@@ -58,6 +102,42 @@ impl ToolWorkingSet {
tool_id
}
pub fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
self.enabled_tools_by_source
.get(source)
.map_or(false, |enabled_tools| enabled_tools.contains(name))
}
pub fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
!self.is_enabled(source, name)
}
pub fn enable(
&mut self,
source: ToolSource,
tools_to_enable: &[Arc<str>],
cx: &mut Context<Self>,
) {
self.enabled_tools_by_source
.entry(source)
.or_default()
.extend(tools_to_enable.into_iter().cloned());
cx.emit(ToolWorkingSetEvent::EnabledToolsChanged);
}
pub fn disable(
&mut self,
source: ToolSource,
tools_to_disable: &[Arc<str>],
cx: &mut Context<Self>,
) {
self.enabled_tools_by_source
.entry(source)
.or_default()
.retain(|name| !tools_to_disable.contains(name));
cx.emit(ToolWorkingSetEvent::EnabledToolsChanged);
}
pub fn remove(&mut self, tool_ids_to_remove: &[ToolId]) {
self.context_server_tools_by_id
.retain(|id, _| !tool_ids_to_remove.contains(id));

View File

@@ -18,6 +18,7 @@ eval = []
agent_settings.workspace = true
anyhow.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
collections.workspace = true
@@ -57,7 +58,6 @@ terminal_view.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
watch.workspace = true
web_search.workspace = true
which.workspace = true
workspace-hack.workspace = true

View File

@@ -420,12 +420,12 @@ impl EditAgent {
cx: &mut AsyncApp,
) -> (
Task<Result<(T, Vec<ResolvedOldText>)>>,
watch::Receiver<Option<Range<usize>>>,
async_watch::Receiver<Option<Range<usize>>>,
)
where
T: 'static + Send + Unpin + Stream<Item = Result<EditParserEvent>>,
{
let (mut old_range_tx, old_range_rx) = watch::channel(None);
let (old_range_tx, old_range_rx) = async_watch::channel(None);
let task = cx.background_spawn(async move {
let mut matcher = StreamingFuzzyMatcher::new(snapshot);
while let Some(edit_event) = edit_events.next().await {

View File

@@ -39,7 +39,7 @@ fn eval_extract_handle_command_output() {
// Model | Pass rate
// ----------------------------|----------
// claude-3.7-sonnet | 0.98
// gemini-2.5-pro-06-05 | 0.77
// gemini-2.5-pro | 0.86
// gemini-2.5-flash | 0.11
// gpt-4.1 | 1.00
@@ -58,7 +58,6 @@ fn eval_extract_handle_command_output() {
eval(
100,
0.7, // Taking the lower bar for Gemini
0.05,
EvalInput::from_conversation(
vec![
message(
@@ -117,7 +116,6 @@ fn eval_delete_run_git_blame() {
eval(
100,
0.95,
0.05,
EvalInput::from_conversation(
vec![
message(
@@ -180,7 +178,6 @@ fn eval_translate_doc_comments() {
eval(
200,
1.,
0.05,
EvalInput::from_conversation(
vec![
message(
@@ -244,7 +241,6 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
eval(
100,
0.95,
0.05,
EvalInput::from_conversation(
vec![
message(
@@ -369,7 +365,6 @@ fn eval_disable_cursor_blinking() {
eval(
100,
0.95,
0.05,
EvalInput::from_conversation(
vec![
message(User, [text("Let's research how to cursor blinking works.")]),
@@ -453,9 +448,6 @@ fn eval_from_pixels_constructor() {
eval(
100,
0.95,
// For whatever reason, this eval produces more mismatched tags.
// Increasing for now, let's see if we can bring this down.
0.2,
EvalInput::from_conversation(
vec![
message(
@@ -656,7 +648,6 @@ fn eval_zode() {
eval(
50,
1.,
0.05,
EvalInput::from_conversation(
vec![
message(User, [text(include_str!("evals/fixtures/zode/prompt.md"))]),
@@ -763,7 +754,6 @@ fn eval_add_overwrite_test() {
eval(
200,
0.5, // TODO: make this eval better
0.05,
EvalInput::from_conversation(
vec![
message(
@@ -1003,7 +993,6 @@ fn eval_create_empty_file() {
eval(
100,
0.99,
0.05,
EvalInput::from_conversation(
vec![
message(User, [text("Create a second empty todo file ")]),
@@ -1290,12 +1279,7 @@ impl EvalAssertion {
}
}
fn eval(
iterations: usize,
expected_pass_ratio: f32,
mismatched_tag_threshold: f32,
mut eval: EvalInput,
) {
fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) {
let mut evaluated_count = 0;
let mut failed_count = 0;
report_progress(evaluated_count, failed_count, iterations);
@@ -1367,7 +1351,7 @@ fn eval(
let mismatched_tag_ratio =
cumulative_parser_metrics.mismatched_tags as f32 / cumulative_parser_metrics.tags as f32;
if mismatched_tag_ratio > mismatched_tag_threshold {
if mismatched_tag_ratio > 0.10 {
for eval_output in eval_outputs {
println!("{}", eval_output);
}

View File

@@ -498,7 +498,7 @@ client.with_options(max_retries=5).messages.create(
### Timeouts
By default requests time out after 10 minutes. You can configure this with a `timeout` option,
which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object:
which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object:
```python
from anthropic import Anthropic

View File

@@ -80,6 +80,7 @@ zed_llm_client.workspace = true
agent_settings.workspace = true
assistant_context_editor.workspace = true
assistant_slash_command.workspace = true
assistant_tool.workspace = true
async-trait.workspace = true
audio.workspace = true
buffer_diff.workspace = true

View File

@@ -312,7 +312,6 @@ impl Server {
.add_request_handler(
forward_read_only_project_request::<proto::LanguageServerIdForName>,
)
.add_request_handler(forward_read_only_project_request::<proto::GetDocumentDiagnostics>)
.add_request_handler(
forward_mutating_project_request::<proto::RegisterBufferWithLanguageServers>,
)
@@ -355,9 +354,6 @@ impl Server {
.add_message_handler(broadcast_project_message_from_host::<proto::BufferReloaded>)
.add_message_handler(broadcast_project_message_from_host::<proto::BufferSaved>)
.add_message_handler(broadcast_project_message_from_host::<proto::UpdateDiffBases>)
.add_message_handler(
broadcast_project_message_from_host::<proto::PullWorkspaceDiagnostics>,
)
.add_request_handler(get_users)
.add_request_handler(fuzzy_search_users)
.add_request_handler(request_contact)

View File

@@ -7,7 +7,7 @@ use editor::{
Editor, RowInfo,
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
ExpandMacroRecursively, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
ExpandMacroRecursively, Redo, Rename, ToggleCodeActions, Undo,
},
test::{
editor_test_context::{AssertionContextManager, EditorTestContext},
@@ -2712,7 +2712,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(params.position, lsp::Position::new(0, 0));
assert_eq!(params.position, lsp::Position::new(0, 0),);
Ok(Some(ExpandedMacro {
name: "test_macro_name".to_string(),
expansion: "test_macro_expansion on the host".to_string(),
@@ -2747,11 +2747,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.position,
lsp::Position::new(0, 12),
"editor_b has selected the entire text and should query for a different position"
);
assert_eq!(params.position, lsp::Position::new(0, 0),);
Ok(Some(ExpandedMacro {
name: "test_macro_name".to_string(),
expansion: "test_macro_expansion on the client".to_string(),
@@ -2760,7 +2756,6 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
);
editor_b.update_in(cx_b, |editor, window, cx| {
editor.select_all(&SelectAll, window, cx);
expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
});
expand_request_b.next().await.unwrap();

View File

@@ -20,8 +20,8 @@ use gpui::{
UpdateGlobal, px, size,
};
use language::{
Diagnostic, DiagnosticEntry, DiagnosticSourceKind, FakeLspAdapter, Language, LanguageConfig,
LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher,
LineEnding, OffsetRangeExt, Point, Rope,
language_settings::{
AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
},
@@ -4237,8 +4237,7 @@ async fn test_collaborating_with_diagnostics(
message: "message 1".to_string(),
severity: lsp::DiagnosticSeverity::ERROR,
is_primary: true,
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
..Default::default()
}
},
DiagnosticEntry {
@@ -4248,8 +4247,7 @@ async fn test_collaborating_with_diagnostics(
severity: lsp::DiagnosticSeverity::WARNING,
message: "message 2".to_string(),
is_primary: true,
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
..Default::default()
}
}
]
@@ -4261,7 +4259,7 @@ async fn test_collaborating_with_diagnostics(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: Vec::new(),
diagnostics: vec![],
},
);
executor.run_until_parked();

View File

@@ -15,6 +15,7 @@ use language::{
use project::{Completion, CompletionResponse, CompletionSource, search::SearchQuery};
use settings::Settings;
use std::{
cell::RefCell,
ops::Range,
rc::Rc,
sync::{Arc, LazyLock},
@@ -72,6 +73,16 @@ impl CompletionProvider for MessageEditorCompletionProvider {
})
}
fn resolve_completions(
&self,
_buffer: Entity<Buffer>,
_completion_indices: Vec<usize>,
_completions: Rc<RefCell<Box<[Completion]>>>,
_cx: &mut Context<Editor>,
) -> Task<anyhow::Result<bool>> {
Task::ready(Ok(false))
}
fn is_completion_trigger(
&self,
_buffer: &Entity<Buffer>,
@@ -244,7 +255,7 @@ impl MessageEditor {
{
if !candidates.is_empty() {
return cx.spawn(async move |_, cx| {
let completion_response = Self::completions_for_candidates(
let completion_response = Self::resolve_completions_for_candidates(
&cx,
query.as_str(),
&candidates,
@@ -262,7 +273,7 @@ impl MessageEditor {
{
if !candidates.is_empty() {
return cx.spawn(async move |_, cx| {
let completion_response = Self::completions_for_candidates(
let completion_response = Self::resolve_completions_for_candidates(
&cx,
query.as_str(),
candidates,
@@ -281,7 +292,7 @@ impl MessageEditor {
}]))
}
async fn completions_for_candidates(
async fn resolve_completions_for_candidates(
cx: &AsyncApp,
query: &str,
candidates: &[StringMatchCandidate],

View File

@@ -11,9 +11,6 @@ workspace = true
[lib]
path = "src/context_server.rs"
[features]
test-support = []
[dependencies]
anyhow.workspace = true
async-trait.workspace = true

View File

@@ -1,7 +1,5 @@
pub mod client;
pub mod protocol;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
pub mod transport;
pub mod types;

View File

@@ -6,9 +6,10 @@
//! of messages.
use anyhow::Result;
use collections::HashMap;
use crate::client::Client;
use crate::types::{self, Request};
use crate::types;
pub struct ModelContextProtocol {
inner: Client,
@@ -42,7 +43,7 @@ impl ModelContextProtocol {
let response: types::InitializeResponse = self
.inner
.request(types::request::Initialize::METHOD, params)
.request(types::RequestType::Initialize.as_str(), params)
.await?;
anyhow::ensure!(
@@ -93,7 +94,137 @@ impl InitializedContextServerProtocol {
}
}
pub async fn request<T: Request>(&self, params: T::Params) -> Result<T::Response> {
self.inner.request(T::METHOD, params).await
fn check_capability(&self, capability: ServerCapability) -> Result<()> {
anyhow::ensure!(
self.capable(capability),
"Server does not support {capability:?} capability"
);
Ok(())
}
/// List the MCP prompts.
pub async fn list_prompts(&self) -> Result<Vec<types::Prompt>> {
self.check_capability(ServerCapability::Prompts)?;
let response: types::PromptsListResponse = self
.inner
.request(
types::RequestType::PromptsList.as_str(),
serde_json::json!({}),
)
.await?;
Ok(response.prompts)
}
/// List the MCP resources.
pub async fn list_resources(&self) -> Result<types::ResourcesListResponse> {
self.check_capability(ServerCapability::Resources)?;
let response: types::ResourcesListResponse = self
.inner
.request(
types::RequestType::ResourcesList.as_str(),
serde_json::json!({}),
)
.await?;
Ok(response)
}
/// Executes a prompt with the given arguments and returns the result.
pub async fn run_prompt<P: AsRef<str>>(
&self,
prompt: P,
arguments: HashMap<String, String>,
) -> Result<types::PromptsGetResponse> {
self.check_capability(ServerCapability::Prompts)?;
let params = types::PromptsGetParams {
name: prompt.as_ref().to_string(),
arguments: Some(arguments),
meta: None,
};
let response: types::PromptsGetResponse = self
.inner
.request(types::RequestType::PromptsGet.as_str(), params)
.await?;
Ok(response)
}
pub async fn completion<P: Into<String>>(
&self,
reference: types::CompletionReference,
argument: P,
value: P,
) -> Result<types::Completion> {
let params = types::CompletionCompleteParams {
r#ref: reference,
argument: types::CompletionArgument {
name: argument.into(),
value: value.into(),
},
meta: None,
};
let result: types::CompletionCompleteResponse = self
.inner
.request(types::RequestType::CompletionComplete.as_str(), params)
.await?;
let completion = types::Completion {
values: result.completion.values,
total: types::CompletionTotal::from_options(
result.completion.has_more,
result.completion.total,
),
};
Ok(completion)
}
/// List MCP tools.
pub async fn list_tools(&self) -> Result<types::ListToolsResponse> {
self.check_capability(ServerCapability::Tools)?;
let response = self
.inner
.request::<types::ListToolsResponse>(types::RequestType::ListTools.as_str(), ())
.await?;
Ok(response)
}
/// Executes a tool with the given arguments
pub async fn run_tool<P: AsRef<str>>(
&self,
tool: P,
arguments: Option<HashMap<String, serde_json::Value>>,
) -> Result<types::CallToolResponse> {
self.check_capability(ServerCapability::Tools)?;
let params = types::CallToolParams {
name: tool.as_ref().to_string(),
arguments,
meta: None,
};
let response: types::CallToolResponse = self
.inner
.request(types::RequestType::CallTool.as_str(), params)
.await?;
Ok(response)
}
}
impl InitializedContextServerProtocol {
pub async fn request<R: serde::de::DeserializeOwned>(
&self,
method: &str,
params: impl serde::Serialize,
) -> Result<R> {
self.inner.request(method, params).await
}
}

View File

@@ -1,118 +0,0 @@
use anyhow::Context as _;
use collections::HashMap;
use futures::{Stream, StreamExt as _, lock::Mutex};
use gpui::BackgroundExecutor;
use std::{pin::Pin, sync::Arc};
use crate::{
transport::Transport,
types::{Implementation, InitializeResponse, ProtocolVersion, ServerCapabilities},
};
pub fn create_fake_transport(
name: impl Into<String>,
executor: BackgroundExecutor,
) -> FakeTransport {
let name = name.into();
FakeTransport::new(executor).on_request::<crate::types::request::Initialize>(move |_params| {
create_initialize_response(name.clone())
})
}
fn create_initialize_response(server_name: String) -> InitializeResponse {
InitializeResponse {
protocol_version: ProtocolVersion(crate::types::LATEST_PROTOCOL_VERSION.to_string()),
server_info: Implementation {
name: server_name,
version: "1.0.0".to_string(),
},
capabilities: ServerCapabilities::default(),
meta: None,
}
}
pub struct FakeTransport {
request_handlers:
HashMap<&'static str, Arc<dyn Fn(serde_json::Value) -> serde_json::Value + Send + Sync>>,
tx: futures::channel::mpsc::UnboundedSender<String>,
rx: Arc<Mutex<futures::channel::mpsc::UnboundedReceiver<String>>>,
executor: BackgroundExecutor,
}
impl FakeTransport {
pub fn new(executor: BackgroundExecutor) -> Self {
let (tx, rx) = futures::channel::mpsc::unbounded();
Self {
request_handlers: Default::default(),
tx,
rx: Arc::new(Mutex::new(rx)),
executor,
}
}
pub fn on_request<T: crate::types::Request>(
mut self,
handler: impl Fn(T::Params) -> T::Response + Send + Sync + 'static,
) -> Self {
self.request_handlers.insert(
T::METHOD,
Arc::new(move |value| {
let params = value.get("params").expect("Missing parameters").clone();
let params: T::Params =
serde_json::from_value(params).expect("Invalid parameters received");
let response = handler(params);
serde_json::to_value(response).unwrap()
}),
);
self
}
}
#[async_trait::async_trait]
impl Transport for FakeTransport {
async fn send(&self, message: String) -> anyhow::Result<()> {
if let Ok(msg) = serde_json::from_str::<serde_json::Value>(&message) {
let id = msg.get("id").and_then(|id| id.as_u64()).unwrap_or(0);
if let Some(method) = msg.get("method") {
let method = method.as_str().expect("Invalid method received");
if let Some(handler) = self.request_handlers.get(method) {
let payload = handler(msg);
let response = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"result": payload
});
self.tx
.unbounded_send(response.to_string())
.context("sending a message")?;
} else {
log::debug!("No handler registered for MCP request '{method}'");
}
}
}
Ok(())
}
fn receive(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
let rx = self.rx.clone();
let executor = self.executor.clone();
Box::pin(futures::stream::unfold(rx, move |rx| {
let executor = executor.clone();
async move {
let mut rx_guard = rx.lock().await;
executor.simulate_random_delay().await;
if let Some(message) = rx_guard.next().await {
drop(rx_guard);
Some((message, rx))
} else {
None
}
}
}))
}
fn receive_err(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
Box::pin(futures::stream::empty())
}
}

View File

@@ -1,92 +1,76 @@
use collections::HashMap;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use url::Url;
pub const LATEST_PROTOCOL_VERSION: &str = "2024-11-05";
pub mod request {
use super::*;
macro_rules! request {
($method:expr, $name:ident, $params:ty, $response:ty) => {
pub struct $name;
impl Request for $name {
type Params = $params;
type Response = $response;
const METHOD: &'static str = $method;
}
};
}
request!(
"initialize",
Initialize,
InitializeParams,
InitializeResponse
);
request!("tools/call", CallTool, CallToolParams, CallToolResponse);
request!(
"resources/unsubscribe",
ResourcesUnsubscribe,
ResourcesUnsubscribeParams,
()
);
request!(
"resources/subscribe",
ResourcesSubscribe,
ResourcesSubscribeParams,
()
);
request!(
"resources/read",
ResourcesRead,
ResourcesReadParams,
ResourcesReadResponse
);
request!("resources/list", ResourcesList, (), ResourcesListResponse);
request!(
"logging/setLevel",
LoggingSetLevel,
LoggingSetLevelParams,
()
);
request!(
"prompts/get",
PromptsGet,
PromptsGetParams,
PromptsGetResponse
);
request!("prompts/list", PromptsList, (), PromptsListResponse);
request!(
"completion/complete",
CompletionComplete,
CompletionCompleteParams,
CompletionCompleteResponse
);
request!("ping", Ping, (), ());
request!("tools/list", ListTools, (), ListToolsResponse);
request!(
"resources/templates/list",
ListResourceTemplates,
(),
ListResourceTemplatesResponse
);
request!("roots/list", ListRoots, (), ListRootsResponse);
pub enum RequestType {
Initialize,
CallTool,
ResourcesUnsubscribe,
ResourcesSubscribe,
ResourcesRead,
ResourcesList,
LoggingSetLevel,
PromptsGet,
PromptsList,
CompletionComplete,
Ping,
ListTools,
ListResourceTemplates,
ListRoots,
}
pub trait Request {
type Params: DeserializeOwned + Serialize + Send + Sync + 'static;
type Response: DeserializeOwned + Serialize + Send + Sync + 'static;
const METHOD: &'static str;
impl RequestType {
pub fn as_str(&self) -> &'static str {
match self {
RequestType::Initialize => "initialize",
RequestType::CallTool => "tools/call",
RequestType::ResourcesUnsubscribe => "resources/unsubscribe",
RequestType::ResourcesSubscribe => "resources/subscribe",
RequestType::ResourcesRead => "resources/read",
RequestType::ResourcesList => "resources/list",
RequestType::LoggingSetLevel => "logging/setLevel",
RequestType::PromptsGet => "prompts/get",
RequestType::PromptsList => "prompts/list",
RequestType::CompletionComplete => "completion/complete",
RequestType::Ping => "ping",
RequestType::ListTools => "tools/list",
RequestType::ListResourceTemplates => "resources/templates/list",
RequestType::ListRoots => "roots/list",
}
}
}
impl TryFrom<&str> for RequestType {
type Error = ();
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"initialize" => Ok(RequestType::Initialize),
"tools/call" => Ok(RequestType::CallTool),
"resources/unsubscribe" => Ok(RequestType::ResourcesUnsubscribe),
"resources/subscribe" => Ok(RequestType::ResourcesSubscribe),
"resources/read" => Ok(RequestType::ResourcesRead),
"resources/list" => Ok(RequestType::ResourcesList),
"logging/setLevel" => Ok(RequestType::LoggingSetLevel),
"prompts/get" => Ok(RequestType::PromptsGet),
"prompts/list" => Ok(RequestType::PromptsList),
"completion/complete" => Ok(RequestType::CompletionComplete),
"ping" => Ok(RequestType::Ping),
"tools/list" => Ok(RequestType::ListTools),
"resources/templates/list" => Ok(RequestType::ListResourceTemplates),
"roots/list" => Ok(RequestType::ListRoots),
_ => Err(()),
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ProtocolVersion(pub String);
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeParams {
pub protocol_version: ProtocolVersion,
@@ -96,7 +80,7 @@ pub struct InitializeParams {
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CallToolParams {
pub name: String,
@@ -106,7 +90,7 @@ pub struct CallToolParams {
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesUnsubscribeParams {
pub uri: Url,
@@ -114,7 +98,7 @@ pub struct ResourcesUnsubscribeParams {
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesSubscribeParams {
pub uri: Url,
@@ -122,7 +106,7 @@ pub struct ResourcesSubscribeParams {
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesReadParams {
pub uri: Url,
@@ -130,7 +114,7 @@ pub struct ResourcesReadParams {
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LoggingSetLevelParams {
pub level: LoggingLevel,
@@ -138,7 +122,7 @@ pub struct LoggingSetLevelParams {
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptsGetParams {
pub name: String,
@@ -148,40 +132,37 @@ pub struct PromptsGetParams {
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionCompleteParams {
#[serde(rename = "ref")]
pub reference: CompletionReference,
pub r#ref: CompletionReference,
pub argument: CompletionArgument,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum CompletionReference {
Prompt(PromptReference),
Resource(ResourceReference),
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptReference {
#[serde(rename = "type")]
pub ty: PromptReferenceType,
pub r#type: PromptReferenceType,
pub name: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceReference {
#[serde(rename = "type")]
pub ty: PromptReferenceType,
pub r#type: PromptReferenceType,
pub uri: Url,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PromptReferenceType {
#[serde(rename = "ref/prompt")]
@@ -190,7 +171,7 @@ pub enum PromptReferenceType {
Resource,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionArgument {
pub name: String,
@@ -207,7 +188,7 @@ pub struct InitializeResponse {
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesReadResponse {
pub contents: Vec<ResourceContentsType>,
@@ -215,14 +196,14 @@ pub struct ResourcesReadResponse {
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum ResourceContentsType {
Text(TextResourceContents),
Blob(BlobResourceContents),
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesListResponse {
pub resources: Vec<Resource>,
@@ -239,7 +220,7 @@ pub struct SamplingMessage {
pub content: MessageContent,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateMessageRequest {
pub messages: Vec<SamplingMessage>,
@@ -315,7 +296,7 @@ pub struct MessageAnnotations {
pub priority: Option<f64>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptsGetResponse {
#[serde(skip_serializing_if = "Option::is_none")]
@@ -325,7 +306,7 @@ pub struct PromptsGetResponse {
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptsListResponse {
pub prompts: Vec<Prompt>,
@@ -335,7 +316,7 @@ pub struct PromptsListResponse {
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionCompleteResponse {
pub completion: CompletionResult,
@@ -343,7 +324,7 @@ pub struct CompletionCompleteResponse {
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionResult {
pub values: Vec<String>,
@@ -355,7 +336,7 @@ pub struct CompletionResult {
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Prompt {
pub name: String,
@@ -365,7 +346,7 @@ pub struct Prompt {
pub arguments: Option<Vec<PromptArgument>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptArgument {
pub name: String,
@@ -528,7 +509,7 @@ pub struct ModelHint {
pub name: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum NotificationType {
Initialized,
@@ -608,7 +589,7 @@ pub struct Completion {
pub total: CompletionTotal,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallToolResponse {
pub content: Vec<ToolResponseContent>,
@@ -639,7 +620,7 @@ pub struct ListToolsResponse {
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListResourceTemplatesResponse {
pub resource_templates: Vec<ResourceTemplate>,
@@ -649,7 +630,7 @@ pub struct ListResourceTemplatesResponse {
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListRootsResponse {
pub roots: Vec<Root>,

View File

@@ -520,7 +520,7 @@ impl Copilot {
let server = cx
.update(|cx| {
let mut params = server.default_initialize_params(false, cx);
let mut params = server.default_initialize_params(cx);
params.initialization_options = Some(editor_info_json);
server.initialize(params, configuration.into(), cx)
})?

View File

@@ -8,7 +8,6 @@ use chrono::DateTime;
use collections::HashSet;
use fs::Fs;
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
use gpui::WeakEntity;
use gpui::{App, AsyncApp, Global, prelude::*};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use itertools::Itertools;
@@ -16,12 +15,9 @@ use paths::home_dir;
use serde::{Deserialize, Serialize};
use settings::watch_config_dir;
#[derive(Default, Clone, Debug, PartialEq)]
pub struct CopilotChatSettings {
pub api_url: Arc<str>,
pub auth_url: Arc<str>,
pub models_url: Arc<str>,
}
pub const COPILOT_CHAT_COMPLETION_URL: &str = "https://api.githubcopilot.com/chat/completions";
pub const COPILOT_CHAT_AUTH_URL: &str = "https://api.github.com/copilot_internal/v2/token";
pub const COPILOT_CHAT_MODELS_URL: &str = "https://api.githubcopilot.com/models";
// Copilot's base model; defined by Microsoft in premium requests table
// This will be moved to the front of the Copilot model list, and will be used for
@@ -344,7 +340,6 @@ impl Global for GlobalCopilotChat {}
pub struct CopilotChat {
oauth_token: Option<String>,
api_token: Option<ApiToken>,
settings: CopilotChatSettings,
models: Option<Vec<Model>>,
client: Arc<dyn HttpClient>,
}
@@ -378,30 +373,53 @@ impl CopilotChat {
.map(|model| model.0.clone())
}
fn new(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &mut Context<Self>) -> Self {
pub fn new(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &App) -> Self {
let config_paths: HashSet<PathBuf> = copilot_chat_config_paths().into_iter().collect();
let dir_path = copilot_chat_config_dir();
let settings = CopilotChatSettings::default();
cx.spawn(async move |this, cx| {
let mut parent_watch_rx = watch_config_dir(
cx.background_executor(),
fs.clone(),
dir_path.clone(),
config_paths,
);
while let Some(contents) = parent_watch_rx.next().await {
let oauth_token = extract_oauth_token(contents);
this.update(cx, |this, cx| {
this.oauth_token = oauth_token.clone();
cx.notify();
})?;
cx.spawn({
let client = client.clone();
async move |cx| {
let mut parent_watch_rx = watch_config_dir(
cx.background_executor(),
fs.clone(),
dir_path.clone(),
config_paths,
);
while let Some(contents) = parent_watch_rx.next().await {
let oauth_token = extract_oauth_token(contents);
cx.update(|cx| {
if let Some(this) = Self::global(cx).as_ref() {
this.update(cx, |this, cx| {
this.oauth_token = oauth_token.clone();
cx.notify();
});
}
})?;
if oauth_token.is_some() {
Self::update_models(&this, cx).await?;
if let Some(ref oauth_token) = oauth_token {
let api_token = request_api_token(oauth_token, client.clone()).await?;
cx.update(|cx| {
if let Some(this) = Self::global(cx).as_ref() {
this.update(cx, |this, cx| {
this.api_token = Some(api_token.clone());
cx.notify();
});
}
})?;
let models = get_models(api_token.api_key, client.clone()).await?;
cx.update(|cx| {
if let Some(this) = Self::global(cx).as_ref() {
this.update(cx, |this, cx| {
this.models = Some(models);
cx.notify();
});
}
})?;
}
}
anyhow::Ok(())
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
@@ -409,42 +427,10 @@ impl CopilotChat {
oauth_token: None,
api_token: None,
models: None,
settings,
client,
}
}
async fn update_models(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
let (oauth_token, client, auth_url) = this.read_with(cx, |this, _| {
(
this.oauth_token.clone(),
this.client.clone(),
this.settings.auth_url.clone(),
)
})?;
let api_token = request_api_token(
&oauth_token.ok_or_else(|| {
anyhow!("OAuth token is missing while updating Copilot Chat models")
})?,
auth_url,
client.clone(),
)
.await?;
let models_url = this.update(cx, |this, cx| {
this.api_token = Some(api_token.clone());
cx.notify();
this.settings.models_url.clone()
})?;
let models = get_models(models_url, api_token.api_key, client.clone()).await?;
this.update(cx, |this, cx| {
this.models = Some(models);
cx.notify();
})?;
anyhow::Ok(())
}
pub fn is_authenticated(&self) -> bool {
self.oauth_token.is_some()
}
@@ -463,23 +449,20 @@ impl CopilotChat {
.flatten()
.context("Copilot chat is not enabled")?;
let (oauth_token, api_token, client, api_url, auth_url) =
this.read_with(&cx, |this, _| {
(
this.oauth_token.clone(),
this.api_token.clone(),
this.client.clone(),
this.settings.api_url.clone(),
this.settings.auth_url.clone(),
)
})?;
let (oauth_token, api_token, client) = this.read_with(&cx, |this, _| {
(
this.oauth_token.clone(),
this.api_token.clone(),
this.client.clone(),
)
})?;
let oauth_token = oauth_token.context("No OAuth token available")?;
let token = match api_token {
Some(api_token) if api_token.remaining_seconds() > 5 * 60 => api_token.clone(),
_ => {
let token = request_api_token(&oauth_token, auth_url, client.clone()).await?;
let token = request_api_token(&oauth_token, client.clone()).await?;
this.update(&mut cx, |this, cx| {
this.api_token = Some(token.clone());
cx.notify();
@@ -488,28 +471,12 @@ impl CopilotChat {
}
};
stream_completion(client.clone(), token.api_key, api_url, request).await
}
pub fn set_settings(&mut self, settings: CopilotChatSettings, cx: &mut Context<Self>) {
let same_settings = self.settings == settings;
self.settings = settings;
if !same_settings {
cx.spawn(async move |this, cx| {
Self::update_models(&this, cx).await?;
Ok::<_, anyhow::Error>(())
})
.detach();
}
stream_completion(client.clone(), token.api_key, request).await
}
}
async fn get_models(
models_url: Arc<str>,
api_token: String,
client: Arc<dyn HttpClient>,
) -> Result<Vec<Model>> {
let all_models = request_models(models_url, api_token, client).await?;
async fn get_models(api_token: String, client: Arc<dyn HttpClient>) -> Result<Vec<Model>> {
let all_models = request_models(api_token, client).await?;
let mut models: Vec<Model> = all_models
.into_iter()
@@ -537,14 +504,10 @@ async fn get_models(
Ok(models)
}
async fn request_models(
models_url: Arc<str>,
api_token: String,
client: Arc<dyn HttpClient>,
) -> Result<Vec<Model>> {
async fn request_models(api_token: String, client: Arc<dyn HttpClient>) -> Result<Vec<Model>> {
let request_builder = HttpRequest::builder()
.method(Method::GET)
.uri(models_url.as_ref())
.uri(COPILOT_CHAT_MODELS_URL)
.header("Authorization", format!("Bearer {}", api_token))
.header("Content-Type", "application/json")
.header("Copilot-Integration-Id", "vscode-chat");
@@ -568,14 +531,10 @@ async fn request_models(
Ok(models)
}
async fn request_api_token(
oauth_token: &str,
auth_url: Arc<str>,
client: Arc<dyn HttpClient>,
) -> Result<ApiToken> {
async fn request_api_token(oauth_token: &str, client: Arc<dyn HttpClient>) -> Result<ApiToken> {
let request_builder = HttpRequest::builder()
.method(Method::GET)
.uri(auth_url.as_ref())
.uri(COPILOT_CHAT_AUTH_URL)
.header("Authorization", format!("token {}", oauth_token))
.header("Accept", "application/json");
@@ -620,7 +579,6 @@ fn extract_oauth_token(contents: String) -> Option<String> {
async fn stream_completion(
client: Arc<dyn HttpClient>,
api_key: String,
completion_url: Arc<str>,
request: Request,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let is_vision_request = request.messages.last().map_or(false, |message| match message {
@@ -634,7 +592,7 @@ async fn stream_completion(
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(completion_url.as_ref())
.uri(COPILOT_CHAT_COMPLETION_URL)
.header(
"Editor-Version",
format!(

View File

@@ -39,7 +39,6 @@ file_icons.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
itertools.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true

View File

@@ -342,7 +342,7 @@ impl DebugPanel {
window.defer(cx, move |window, cx| {
workspace
.update(cx, |workspace, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
NewProcessModal::show(workspace, window, NewProcessMode::Launch, None, cx);
})
.ok();
});

View File

@@ -19,7 +19,6 @@ use gpui::{
InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, Subscription,
TextStyle, UnderlineStyle, WeakEntity,
};
use itertools::Itertools as _;
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
use settings::{Settings, initial_local_debug_tasks_content};
@@ -50,7 +49,7 @@ pub(super) struct NewProcessModal {
mode: NewProcessMode,
debug_picker: Entity<Picker<DebugDelegate>>,
attach_mode: Entity<AttachMode>,
launch_mode: Entity<ConfigureMode>,
launch_mode: Entity<LaunchMode>,
task_mode: TaskMode,
debugger: Option<DebugAdapterName>,
// save_scenario_state: Option<SaveScenarioState>,
@@ -98,13 +97,13 @@ impl NewProcessModal {
workspace.toggle_modal(window, cx, |window, cx| {
let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx);
let debug_picker = cx.new(|cx| {
let launch_picker = cx.new(|cx| {
let delegate =
DebugDelegate::new(debug_panel.downgrade(), task_store.clone());
Picker::uniform_list(delegate, window, cx).modal(false)
});
let configure_mode = ConfigureMode::new(window, cx);
let configure_mode = LaunchMode::new(window, cx);
let task_overrides = Some(TaskOverrides { reveal_target });
@@ -123,7 +122,7 @@ impl NewProcessModal {
};
let _subscriptions = [
cx.subscribe(&debug_picker, |_, _, _, cx| {
cx.subscribe(&launch_picker, |_, _, _, cx| {
cx.emit(DismissEvent);
}),
cx.subscribe(
@@ -138,76 +137,19 @@ impl NewProcessModal {
];
cx.spawn_in(window, {
let debug_picker = debug_picker.downgrade();
let launch_picker = launch_picker.downgrade();
let configure_mode = configure_mode.downgrade();
let task_modal = task_mode.task_modal.downgrade();
let workspace = workspace_handle.clone();
async move |this, cx| {
let task_contexts = task_contexts.await;
let task_contexts = Arc::new(task_contexts);
let lsp_task_sources = task_contexts.lsp_task_sources.clone();
let task_position = task_contexts.latest_selection;
// Get LSP tasks and filter out based on language vs lsp preference
let (lsp_tasks, prefer_lsp) =
workspace.update(cx, |workspace, cx| {
let lsp_tasks = editor::lsp_tasks(
workspace.project().clone(),
&lsp_task_sources,
task_position,
cx,
);
let prefer_lsp = workspace
.active_item(cx)
.and_then(|item| item.downcast::<Editor>())
.map(|editor| {
editor
.read(cx)
.buffer()
.read(cx)
.language_settings(cx)
.tasks
.prefer_lsp
})
.unwrap_or(false);
(lsp_tasks, prefer_lsp)
})?;
let lsp_tasks = lsp_tasks.await;
let add_current_language_tasks = !prefer_lsp || lsp_tasks.is_empty();
let lsp_tasks = lsp_tasks
.into_iter()
.flat_map(|(kind, tasks_with_locations)| {
tasks_with_locations
.into_iter()
.sorted_by_key(|(location, task)| {
(location.is_none(), task.resolved_label.clone())
})
.map(move |(_, task)| (kind.clone(), task))
})
.collect::<Vec<_>>();
let Some(task_inventory) = task_store
.update(cx, |task_store, _| task_store.task_inventory().cloned())?
else {
return Ok(());
};
let (used_tasks, current_resolved_tasks) =
task_inventory.update(cx, |task_inventory, cx| {
task_inventory
.used_and_current_resolved_tasks(&task_contexts, cx)
})?;
debug_picker
launch_picker
.update_in(cx, |picker, window, cx| {
picker.delegate.tasks_loaded(
picker.delegate.task_contexts_loaded(
task_contexts.clone(),
languages,
lsp_tasks.clone(),
current_resolved_tasks.clone(),
add_current_language_tasks,
window,
cx,
);
picker.refresh(window, cx);
@@ -228,15 +170,7 @@ impl NewProcessModal {
task_modal
.update_in(cx, |task_modal, window, cx| {
task_modal.tasks_loaded(
task_contexts,
lsp_tasks,
used_tasks,
current_resolved_tasks,
add_current_language_tasks,
window,
cx,
);
task_modal.task_contexts_loaded(task_contexts, window, cx);
})
.ok();
@@ -244,14 +178,12 @@ impl NewProcessModal {
cx.notify();
})
.ok();
anyhow::Ok(())
}
})
.detach();
Self {
debug_picker,
debug_picker: launch_picker,
attach_mode,
launch_mode: configure_mode,
task_mode,
@@ -888,18 +820,18 @@ impl RenderOnce for AttachMode {
}
#[derive(Clone)]
pub(super) struct ConfigureMode {
pub(super) struct LaunchMode {
program: Entity<Editor>,
cwd: Entity<Editor>,
stop_on_entry: ToggleState,
// save_to_debug_json: ToggleState,
}
impl ConfigureMode {
impl LaunchMode {
pub(super) fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
let program = cx.new(|cx| Editor::single_line(window, cx));
program.update(cx, |this, cx| {
this.set_placeholder_text("ENV=Zed ~/bin/program --option", cx);
this.set_placeholder_text("ENV=Zed ~/bin/debugger --launch", cx);
});
let cwd = cx.new(|cx| Editor::single_line(window, cx));
@@ -987,7 +919,7 @@ impl ConfigureMode {
.child(adapter_menu),
)
.child(
Label::new("Program")
Label::new("Debugger Program")
.size(ui::LabelSize::Small)
.color(Color::Muted),
)
@@ -1135,29 +1067,21 @@ impl DebugDelegate {
(language, scenario)
}
pub fn tasks_loaded(
pub fn task_contexts_loaded(
&mut self,
task_contexts: Arc<TaskContexts>,
languages: Arc<LanguageRegistry>,
lsp_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
add_current_language_tasks: bool,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
self.task_contexts = Some(task_contexts.clone());
self.task_contexts = Some(task_contexts);
let (recent, scenarios) = self
.task_store
.update(cx, |task_store, cx| {
task_store.task_inventory().map(|inventory| {
inventory.update(cx, |inventory, cx| {
inventory.list_debug_scenarios(
&task_contexts,
lsp_tasks,
current_resolved_tasks,
add_current_language_tasks,
cx,
)
inventory.list_debug_scenarios(self.task_contexts.as_ref().unwrap(), cx)
})
})
})
@@ -1333,17 +1257,12 @@ impl PickerDelegate for DebugDelegate {
.map(|icon| icon.color(Color::Muted).size(IconSize::Small));
let indicator = if matches!(task_kind, Some(TaskSourceKind::Lsp { .. })) {
Some(Indicator::icon(
Icon::new(IconName::BoltFilled)
.color(Color::Muted)
.size(IconSize::Small),
Icon::new(IconName::BoltFilled).color(Color::Muted),
))
} else {
None
};
let icon = icon.map(|icon| {
IconWithIndicator::new(icon, indicator)
.indicator_border_color(Some(cx.theme().colors().border_transparent))
});
let icon = icon.map(|icon| IconWithIndicator::new(icon, indicator));
Some(
ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))

View File

@@ -282,6 +282,16 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
}
}
fn resolve_completions(
&self,
_buffer: Entity<Buffer>,
_completion_indices: Vec<usize>,
_completions: Rc<RefCell<Box<[Completion]>>>,
_cx: &mut Context<Editor>,
) -> gpui::Task<anyhow::Result<bool>> {
Task::ready(Ok(false))
}
fn apply_additional_edits_for_completion(
&self,
_buffer: Entity<Buffer>,

View File

@@ -11,7 +11,7 @@ use editor::{
};
use gpui::{TestAppContext, VisualTestContext};
use indoc::indoc;
use language::{DiagnosticSourceKind, Rope};
use language::Rope;
use lsp::LanguageServerId;
use pretty_assertions::assert_eq;
use project::FakeFs;
@@ -105,7 +105,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
}
],
version: None
}, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
}, &[], cx).unwrap();
});
// Open the project diagnostics view while there are already diagnostics.
@@ -176,8 +176,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
}],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -263,8 +261,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -372,8 +368,6 @@ async fn test_diagnostics_with_folds(cx: &mut TestAppContext) {
}],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -471,8 +465,6 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
}],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -515,8 +507,6 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
}],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -558,8 +548,6 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
}],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -572,8 +560,6 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
diagnostics: vec![],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -614,8 +600,6 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
}],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -748,8 +732,6 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
diagnostics: diagnostics.clone(),
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -937,8 +919,6 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
diagnostics: diagnostics.clone(),
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -994,8 +974,6 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext)
..Default::default()
}],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -1029,8 +1007,6 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext)
version: None,
diagnostics: Vec::new(),
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -1112,8 +1088,6 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
},
],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -1252,8 +1226,6 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
..Default::default()
}],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -1305,8 +1277,6 @@ async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext)
..Default::default()
}],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -1408,8 +1378,6 @@ async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)

View File

@@ -74,9 +74,8 @@ pub use element::{
};
use feature_flags::{DebuggerFeatureFlag, FeatureFlagAppExt};
use futures::{
FutureExt, StreamExt as _,
FutureExt,
future::{self, Shared, join},
stream::FuturesUnordered,
};
use fuzzy::{StringMatch, StringMatchCandidate};
@@ -109,10 +108,9 @@ pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
use language::{
AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel,
CursorShape, DiagnosticEntry, DiagnosticSourceKind, DiffOptions, DocumentationConfig,
EditPredictionsMode, EditPreview, HighlightedText, IndentKind, IndentSize, Language,
OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
WordsQuery,
CursorShape, DiagnosticEntry, DiffOptions, DocumentationConfig, EditPredictionsMode,
EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point,
Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
language_settings::{
self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
all_language_settings, language_settings,
@@ -125,7 +123,7 @@ use markdown::Markdown;
use mouse_context_menu::MouseContextMenu;
use persistence::DB;
use project::{
BreakpointWithPosition, CompletionResponse, LspPullDiagnostics, ProjectPath, PulledDiagnostics,
BreakpointWithPosition, CompletionResponse, ProjectPath,
debugger::{
breakpoint_store::{
BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
@@ -299,7 +297,6 @@ pub enum DebugStackFrameLine {}
enum DocumentHighlightRead {}
enum DocumentHighlightWrite {}
enum InputComposition {}
pub enum PendingInput {}
enum SelectedTextHighlight {}
pub enum ConflictsOuter {}
@@ -1075,7 +1072,6 @@ pub struct Editor {
tasks_update_task: Option<Task<()>>,
breakpoint_store: Option<Entity<BreakpointStore>>,
gutter_breakpoint_indicator: (Option<PhantomBreakpointIndicator>, Option<Task<()>>),
pull_diagnostics_task: Task<()>,
in_project_search: bool,
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
breadcrumb_header: Option<String>,
@@ -1314,11 +1310,6 @@ struct RowHighlight {
#[derive(Clone, Debug)]
struct AddSelectionsState {
groups: Vec<AddSelectionsGroup>,
}
#[derive(Clone, Debug)]
struct AddSelectionsGroup {
above: bool,
stack: Vec<usize>,
}
@@ -1699,7 +1690,6 @@ impl Editor {
editor.tasks_update_task =
Some(editor.refresh_runnables(window, cx));
}
editor.pull_diagnostics(None, window, cx);
}
project::Event::SnippetEdit(id, snippet_edits) => {
if let Some(buffer) = editor.buffer.read(cx).buffer(*id) {
@@ -1774,8 +1764,6 @@ impl Editor {
.detach();
cx.on_blur(&focus_handle, window, Self::handle_blur)
.detach();
cx.observe_pending_input(window, Self::observe_pending_input)
.detach();
let show_indent_guides = if matches!(mode, EditorMode::SingleLine { .. }) {
Some(false)
@@ -1804,7 +1792,7 @@ impl Editor {
code_action_providers.push(Rc::new(project) as Rc<_>);
}
let mut editor = Self {
let mut this = Self {
focus_handle,
show_cursor_when_unfocused: false,
last_focused_descendant: None,
@@ -1966,7 +1954,6 @@ impl Editor {
}),
],
tasks_update_task: None,
pull_diagnostics_task: Task::ready(()),
linked_edit_ranges: Default::default(),
in_project_search: false,
previous_search_ranges: None,
@@ -1991,17 +1978,16 @@ impl Editor {
change_list: ChangeList::new(),
mode,
};
if let Some(breakpoints) = editor.breakpoint_store.as_ref() {
editor
._subscriptions
if let Some(breakpoints) = this.breakpoint_store.as_ref() {
this._subscriptions
.push(cx.observe(breakpoints, |_, _, cx| {
cx.notify();
}));
}
editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
editor._subscriptions.extend(project_subscriptions);
this.tasks_update_task = Some(this.refresh_runnables(window, cx));
this._subscriptions.extend(project_subscriptions);
editor._subscriptions.push(cx.subscribe_in(
this._subscriptions.push(cx.subscribe_in(
&cx.entity(),
window,
|editor, _, e: &EditorEvent, window, cx| match e {
@@ -2046,15 +2032,14 @@ impl Editor {
},
));
if let Some(dap_store) = editor
if let Some(dap_store) = this
.project
.as_ref()
.map(|project| project.read(cx).dap_store())
{
let weak_editor = cx.weak_entity();
editor
._subscriptions
this._subscriptions
.push(
cx.observe_new::<project::debugger::session::Session>(move |_, _, cx| {
let session_entity = cx.entity();
@@ -2069,44 +2054,40 @@ impl Editor {
);
for session in dap_store.read(cx).sessions().cloned().collect::<Vec<_>>() {
editor
._subscriptions
this._subscriptions
.push(cx.subscribe(&session, Self::on_debug_session_event));
}
}
editor.end_selection(window, cx);
editor.scroll_manager.show_scrollbars(window, cx);
jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &buffer, cx);
this.end_selection(window, cx);
this.scroll_manager.show_scrollbars(window, cx);
jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx);
if full_mode {
let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars();
cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
if editor.git_blame_inline_enabled {
editor.start_git_blame_inline(false, window, cx);
if this.git_blame_inline_enabled {
this.start_git_blame_inline(false, window, cx);
}
editor.go_to_active_debug_line(window, cx);
this.go_to_active_debug_line(window, cx);
if let Some(buffer) = buffer.read(cx).as_singleton() {
if let Some(project) = editor.project.as_ref() {
if let Some(project) = this.project.as_ref() {
let handle = project.update(cx, |project, cx| {
project.register_buffer_with_language_servers(&buffer, cx)
});
editor
.registered_buffers
this.registered_buffers
.insert(buffer.read(cx).remote_id(), handle);
}
}
editor.minimap =
editor.create_minimap(EditorSettings::get_global(cx).minimap, window, cx);
editor.pull_diagnostics(None, window, cx);
this.minimap = this.create_minimap(EditorSettings::get_global(cx).minimap, window, cx);
}
editor.report_editor_event("Editor Opened", None, cx);
editor
this.report_editor_event("Editor Opened", None, cx);
this
}
pub fn deploy_mouse_context_menu(
@@ -2248,28 +2229,31 @@ impl Editor {
pub fn accept_edit_prediction_keybind(
&self,
accept_partial: bool,
window: &Window,
cx: &App,
) -> AcceptEditPredictionBinding {
let key_context = self.key_context_internal(true, window, cx);
let in_conflict = self.edit_prediction_in_conflict();
let bindings = if accept_partial {
window.bindings_for_action_in_context(&AcceptPartialEditPrediction, key_context)
} else {
window.bindings_for_action_in_context(&AcceptEditPrediction, key_context)
};
// TODO: if the binding contains multiple keystrokes, display all of them, not
// just the first one.
AcceptEditPredictionBinding(bindings.into_iter().rev().find(|binding| {
!in_conflict
|| binding
.keystrokes()
.first()
.map_or(false, |keystroke| keystroke.modifiers.modified())
}))
AcceptEditPredictionBinding(
window
.bindings_for_action_in_context(&AcceptEditPrediction, key_context)
.into_iter()
.filter(|binding| {
!in_conflict
|| binding
.keystrokes()
.first()
.map_or(false, |keystroke| keystroke.modifiers.modified())
})
.rev()
.min_by_key(|binding| {
binding
.keystrokes()
.first()
.map_or(u8::MAX, |k| k.modifiers.number_of_modifiers())
}),
)
}
pub fn new_file(
@@ -2719,9 +2703,7 @@ impl Editor {
.display_map
.update(cx, |display_map, cx| display_map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
if self.selections.count() == 1 {
self.add_selections_state = None;
}
self.add_selections_state = None;
self.select_next_state = None;
self.select_prev_state = None;
self.select_syntax_node_history.try_clear();
@@ -2730,9 +2712,7 @@ impl Editor {
.invalidate(&self.selections.disjoint_anchors(), buffer);
self.take_rename(false, window, cx);
let newest_selection = self.selections.newest_anchor();
let new_cursor_position = newest_selection.head();
let selection_start = newest_selection.start;
let new_cursor_position = self.selections.newest_anchor().head();
self.push_to_nav_history(
*old_cursor_position,
@@ -2742,6 +2722,8 @@ impl Editor {
);
if local {
let new_cursor_position = self.selections.newest_anchor().head();
if let Some(buffer_id) = new_cursor_position.buffer_id {
if !self.registered_buffers.contains_key(&buffer_id) {
if let Some(project) = self.project.as_ref() {
@@ -2772,15 +2754,15 @@ impl Editor {
if should_update_completions {
if let Some(completion_position) = completion_position {
let start_offset = selection_start.to_offset(buffer);
let position_matches = start_offset == completion_position.to_offset(buffer);
let new_cursor_offset = new_cursor_position.to_offset(buffer);
let position_matches =
new_cursor_offset == completion_position.to_offset(buffer);
let continue_showing = if position_matches {
if self.snippet_stack.is_empty() {
buffer.char_kind_before(start_offset, true) == Some(CharKind::Word)
let (word_range, kind) = buffer.surrounding_word(new_cursor_offset, true);
if let Some(CharKind::Word) = kind {
word_range.start < new_cursor_offset
} else {
// Snippet choices can be shown even when the cursor is in whitespace.
// Dismissing the menu when actions like backspace
true
false
}
} else {
false
@@ -5064,16 +5046,7 @@ impl Editor {
return;
}
let multibuffer_snapshot = self.buffer.read(cx).read(cx);
// Typically `start` == `end`, but with snippet tabstop choices the default choice is
// inserted and selected. To handle that case, the start of the selection is used so that
// the menu starts with all choices.
let position = self
.selections
.newest_anchor()
.start
.bias_right(&multibuffer_snapshot);
let position = self.selections.newest_anchor().head();
if position.diff_base_anchor.is_some() {
return;
}
@@ -5086,9 +5059,8 @@ impl Editor {
let buffer_snapshot = buffer.read(cx).snapshot();
let query: Option<Arc<String>> =
Self::completion_query(&multibuffer_snapshot, position).map(|query| query.into());
drop(multibuffer_snapshot);
Self::completion_query(&self.buffer.read(cx).read(cx), position)
.map(|query| query.into());
let provider = match requested_source {
Some(CompletionsMenuSource::Normal) | None => self.completion_provider.clone(),
@@ -7123,25 +7095,12 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
let mut modifiers_held = false;
if let Some(accept_keystroke) = self
.accept_edit_prediction_keybind(false, window, cx)
.keystroke()
{
modifiers_held = modifiers_held
|| (&accept_keystroke.modifiers == modifiers
&& accept_keystroke.modifiers.modified());
let accept_keybind = self.accept_edit_prediction_keybind(window, cx);
let Some(accept_keystroke) = accept_keybind.keystroke() else {
return;
};
if let Some(accept_partial_keystroke) = self
.accept_edit_prediction_keybind(true, window, cx)
.keystroke()
{
modifiers_held = modifiers_held
|| (&accept_partial_keystroke.modifiers == modifiers
&& accept_partial_keystroke.modifiers.modified());
}
if modifiers_held {
if &accept_keystroke.modifiers == modifiers && accept_keystroke.modifiers.modified() {
if matches!(
self.edit_prediction_preview,
EditPredictionPreview::Inactive { .. }
@@ -8458,7 +8417,7 @@ impl Editor {
window: &mut Window,
cx: &App,
) -> Option<AnyElement> {
let accept_binding = self.accept_edit_prediction_keybind(false, window, cx);
let accept_binding = self.accept_edit_prediction_keybind(window, cx);
let accept_keystroke = accept_binding.keystroke()?;
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
@@ -8955,30 +8914,26 @@ impl Editor {
selection: Range<Anchor>,
cx: &mut Context<Self>,
) {
let buffer_id = match (&selection.start.buffer_id, &selection.end.buffer_id) {
(Some(a), Some(b)) if a == b => a,
_ => {
log::error!("expected anchor range to have matching buffer IDs");
return;
}
};
let multi_buffer = self.buffer().read(cx);
let Some(buffer) = multi_buffer.buffer(*buffer_id) else {
if selection.start.buffer_id.is_none() {
return;
};
}
let buffer_id = selection.start.buffer_id.unwrap();
let buffer = self.buffer().read(cx).buffer(buffer_id);
let id = post_inc(&mut self.next_completion_id);
let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order;
*self.context_menu.borrow_mut() = Some(CodeContextMenu::Completions(
CompletionsMenu::new_snippet_choices(
id,
true,
choices,
selection,
buffer,
snippet_sort_order,
),
));
if let Some(buffer) = buffer {
*self.context_menu.borrow_mut() = Some(CodeContextMenu::Completions(
CompletionsMenu::new_snippet_choices(
id,
true,
choices,
selection,
buffer,
snippet_sort_order,
),
));
}
}
pub fn insert_snippet(
@@ -9032,7 +8987,9 @@ impl Editor {
})
})
.collect::<Vec<_>>();
tabstop_ranges.sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot));
// Sort in reverse order so that the first range is the newest created
// selection. Completions will use it and autoscroll will prioritize it.
tabstop_ranges.sort_unstable_by(|a, b| b.start.cmp(&a.start, snapshot));
Tabstop {
is_end_tabstop,
@@ -9044,9 +9001,7 @@ impl Editor {
});
if let Some(tabstop) = tabstops.first() {
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
// Reverse order so that the first range is the newest created selection.
// Completions will use it and autoscroll will prioritize it.
s.select_ranges(tabstop.ranges.iter().rev().cloned());
s.select_ranges(tabstop.ranges.iter().cloned());
});
if let Some(choices) = &tabstop.choices {
@@ -9162,9 +9117,7 @@ impl Editor {
}
if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) {
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
// Reverse order so that the first range is the newest created selection.
// Completions will use it and autoscroll will prioritize it.
s.select_ranges(current_ranges.iter().rev().cloned())
s.select_ranges(current_ranges.iter().cloned())
});
if let Some(choices) = &snippet.choices[snippet.active_index] {
@@ -12723,74 +12676,49 @@ impl Editor {
self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let all_selections = self.selections.all::<Point>(cx);
let mut selections = self.selections.all::<Point>(cx);
let text_layout_details = self.text_layout_details(window);
let mut state = self.add_selections_state.take().unwrap_or_else(|| {
let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
let range = oldest_selection.display_range(&display_map).sorted();
let (mut columnar_selections, new_selections_to_columnarize) = {
if let Some(state) = self.add_selections_state.as_ref() {
let columnar_selection_ids: HashSet<_> = state
.groups
.iter()
.flat_map(|group| group.stack.iter())
.copied()
.collect();
all_selections
.into_iter()
.partition(|s| columnar_selection_ids.contains(&s.id))
} else {
(Vec::new(), all_selections)
}
};
let mut state = self
.add_selections_state
.take()
.unwrap_or_else(|| AddSelectionsState { groups: Vec::new() });
for selection in new_selections_to_columnarize {
let range = selection.display_range(&display_map).sorted();
let start_x = display_map.x_for_display_point(range.start, &text_layout_details);
let end_x = display_map.x_for_display_point(range.end, &text_layout_details);
let positions = start_x.min(end_x)..start_x.max(end_x);
selections.clear();
let mut stack = Vec::new();
for row in range.start.row().0..=range.end.row().0 {
if let Some(selection) = self.selections.build_columnar_selection(
&display_map,
DisplayRow(row),
&positions,
selection.reversed,
oldest_selection.reversed,
&text_layout_details,
) {
stack.push(selection.id);
columnar_selections.push(selection);
selections.push(selection);
}
}
if !stack.is_empty() {
if above {
stack.reverse();
}
state.groups.push(AddSelectionsGroup { above, stack });
if above {
stack.reverse();
}
}
let mut final_selections = Vec::new();
let end_row = if above {
DisplayRow(0)
} else {
display_map.max_point().row()
};
AddSelectionsState { above, stack }
});
let mut last_added_item_per_group = HashMap::default();
for group in state.groups.iter_mut() {
if let Some(last_id) = group.stack.last() {
last_added_item_per_group.insert(*last_id, group);
}
}
let last_added_selection = *state.stack.last().unwrap();
let mut new_selections = Vec::new();
if above == state.above {
let end_row = if above {
DisplayRow(0)
} else {
display_map.max_point().row()
};
for selection in columnar_selections {
if let Some(group) = last_added_item_per_group.get_mut(&selection.id) {
if above == group.above {
'outer: for selection in selections {
if selection.id == last_added_selection {
let range = selection.display_range(&display_map).sorted();
debug_assert_eq!(range.start.row(), range.end.row());
let mut row = range.start.row();
@@ -12805,13 +12733,13 @@ impl Editor {
start_x.min(end_x)..start_x.max(end_x)
};
let mut maybe_new_selection = None;
while row != end_row {
if above {
row.0 -= 1;
} else {
row.0 += 1;
}
if let Some(new_selection) = self.selections.build_columnar_selection(
&display_map,
row,
@@ -12819,50 +12747,32 @@ impl Editor {
selection.reversed,
&text_layout_details,
) {
maybe_new_selection = Some(new_selection);
break;
}
}
state.stack.push(new_selection.id);
if above {
new_selections.push(new_selection);
new_selections.push(selection);
} else {
new_selections.push(selection);
new_selections.push(new_selection);
}
if let Some(new_selection) = maybe_new_selection {
group.stack.push(new_selection.id);
if above {
final_selections.push(new_selection);
final_selections.push(selection);
} else {
final_selections.push(selection);
final_selections.push(new_selection);
continue 'outer;
}
} else {
final_selections.push(selection);
}
} else {
group.stack.pop();
}
} else {
final_selections.push(selection);
new_selections.push(selection);
}
} else {
new_selections = selections;
new_selections.retain(|s| s.id != last_added_selection);
state.stack.pop();
}
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.select(final_selections);
s.select(new_selections);
});
let final_selection_ids: HashSet<_> = self
.selections
.all::<Point>(cx)
.iter()
.map(|s| s.id)
.collect();
state.groups.retain_mut(|group| {
// selections might get merged above so we remove invalid items from stacks
group.stack.retain(|id| final_selection_ids.contains(id));
// single selection in stack can be treated as initial state
group.stack.len() > 1
});
if !state.groups.is_empty() {
if state.stack.len() > 1 {
self.add_selections_state = Some(state);
}
}
@@ -15971,59 +15881,6 @@ impl Editor {
});
}
fn pull_diagnostics(
&mut self,
buffer_id: Option<BufferId>,
window: &Window,
cx: &mut Context<Self>,
) -> Option<()> {
let project = self.project.as_ref()?.downgrade();
let pull_diagnostics_settings = ProjectSettings::get_global(cx)
.diagnostics
.lsp_pull_diagnostics;
if !pull_diagnostics_settings.enabled {
return None;
}
let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms);
let mut buffers = self.buffer.read(cx).all_buffers();
if let Some(buffer_id) = buffer_id {
buffers.retain(|buffer| buffer.read(cx).remote_id() == buffer_id);
}
self.pull_diagnostics_task = cx.spawn_in(window, async move |editor, cx| {
cx.background_executor().timer(debounce).await;
let Ok(mut pull_diagnostics_tasks) = cx.update(|_, cx| {
buffers
.into_iter()
.flat_map(|buffer| {
Some(project.upgrade()?.pull_diagnostics_for_buffer(buffer, cx))
})
.collect::<FuturesUnordered<_>>()
}) else {
return;
};
while let Some(pull_task) = pull_diagnostics_tasks.next().await {
match pull_task {
Ok(()) => {
if editor
.update_in(cx, |editor, window, cx| {
editor.update_diagnostics_state(window, cx);
})
.is_err()
{
return;
}
}
Err(e) => log::error!("Failed to update project diagnostics: {e:#}"),
}
}
});
Some(())
}
pub fn set_selections_from_remote(
&mut self,
selections: Vec<Selection<Anchor>>,
@@ -18737,7 +18594,7 @@ impl Editor {
match event {
multi_buffer::Event::Edited {
singleton_buffer_edited,
edited_buffer,
edited_buffer: buffer_edited,
} => {
self.scrollbar_marker_state.dirty = true;
self.active_indent_guides_state.dirty = true;
@@ -18748,22 +18605,16 @@ impl Editor {
if self.has_active_inline_completion() {
self.update_visible_inline_completion(window, cx);
}
if let Some(project) = self.project.as_ref() {
if let Some(edited_buffer) = edited_buffer {
project.update(cx, |project, cx| {
self.registered_buffers
.entry(edited_buffer.read(cx).remote_id())
.or_insert_with(|| {
project
.register_buffer_with_language_servers(&edited_buffer, cx)
});
});
if edited_buffer.read(cx).file().is_some() {
self.pull_diagnostics(
Some(edited_buffer.read(cx).remote_id()),
window,
cx,
);
if let Some(buffer) = buffer_edited {
let buffer_id = buffer.read(cx).remote_id();
if !self.registered_buffers.contains_key(&buffer_id) {
if let Some(project) = self.project.as_ref() {
project.update(cx, |project, cx| {
self.registered_buffers.insert(
buffer_id,
project.register_buffer_with_language_servers(&buffer, cx),
);
})
}
}
}
@@ -18884,19 +18735,15 @@ impl Editor {
| multi_buffer::Event::BufferDiffChanged => cx.emit(EditorEvent::TitleChanged),
multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed),
multi_buffer::Event::DiagnosticsUpdated => {
self.update_diagnostics_state(window, cx);
self.refresh_active_diagnostics(cx);
self.refresh_inline_diagnostics(true, window, cx);
self.scrollbar_marker_state.dirty = true;
cx.notify();
}
_ => {}
};
}
fn update_diagnostics_state(&mut self, window: &mut Window, cx: &mut Context<'_, Editor>) {
self.refresh_active_diagnostics(cx);
self.refresh_inline_diagnostics(true, window, cx);
self.scrollbar_marker_state.dirty = true;
cx.notify();
}
pub fn start_temporary_diff_override(&mut self) {
self.load_diff_task.take();
self.temporary_diff_override = true;
@@ -19564,90 +19411,6 @@ impl Editor {
cx.notify();
}
pub fn observe_pending_input(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let mut pending: String = window
.pending_input_keystrokes()
.into_iter()
.flatten()
.filter_map(|keystroke| {
if keystroke.modifiers.is_subset_of(&Modifiers::shift()) {
Some(keystroke.key_char.clone().unwrap_or(keystroke.key.clone()))
} else {
None
}
})
.collect();
if !self.input_enabled || self.read_only || !self.focus_handle.is_focused(window) {
pending = "".to_string();
}
let existing_pending = self
.text_highlights::<PendingInput>(cx)
.map(|(_, ranges)| ranges.iter().cloned().collect::<Vec<_>>());
if existing_pending.is_none() && pending.is_empty() {
return;
}
let transaction =
self.transact(window, cx, |this, window, cx| {
let selections = this.selections.all::<usize>(cx);
let edits = selections
.iter()
.map(|selection| (selection.end..selection.end, pending.clone()));
this.edit(edits, cx);
this.change_selections(None, window, cx, |s| {
s.select_ranges(selections.into_iter().enumerate().map(|(ix, sel)| {
sel.start + ix * pending.len()..sel.end + ix * pending.len()
}));
});
if let Some(existing_ranges) = existing_pending {
let edits = existing_ranges.iter().map(|range| (range.clone(), ""));
this.edit(edits, cx);
}
});
let snapshot = self.snapshot(window, cx);
let ranges = self
.selections
.all::<usize>(cx)
.into_iter()
.map(|selection| {
snapshot.buffer_snapshot.anchor_after(selection.end)
..snapshot
.buffer_snapshot
.anchor_before(selection.end + pending.len())
})
.collect();
if pending.is_empty() {
self.clear_highlights::<PendingInput>(cx);
} else {
self.highlight_text::<PendingInput>(
ranges,
HighlightStyle {
underline: Some(UnderlineStyle {
thickness: px(1.),
color: None,
wavy: false,
}),
..Default::default()
},
cx,
);
}
self.ime_transaction = self.ime_transaction.or(transaction);
if let Some(transaction) = self.ime_transaction {
self.buffer.update(cx, |buffer, cx| {
buffer.group_until_transaction(transaction, cx);
});
}
if self.text_highlights::<PendingInput>(cx).is_none() {
self.ime_transaction.take();
}
}
pub fn register_action<A: Action>(
&mut self,
listener: impl Fn(&A, &mut Window, &mut App) + 'static,
@@ -20547,12 +20310,6 @@ pub trait SemanticsProvider {
new_name: String,
cx: &mut App,
) -> Option<Task<Result<ProjectTransaction>>>;
fn pull_diagnostics_for_buffer(
&self,
buffer: Entity<Buffer>,
cx: &mut App,
) -> Task<anyhow::Result<()>>;
}
pub trait CompletionProvider {
@@ -20568,13 +20325,11 @@ pub trait CompletionProvider {
fn resolve_completions(
&self,
_buffer: Entity<Buffer>,
_completion_indices: Vec<usize>,
_completions: Rc<RefCell<Box<[Completion]>>>,
_cx: &mut Context<Editor>,
) -> Task<Result<bool>> {
Task::ready(Ok(false))
}
buffer: Entity<Buffer>,
completion_indices: Vec<usize>,
completions: Rc<RefCell<Box<[Completion]>>>,
cx: &mut Context<Editor>,
) -> Task<Result<bool>>;
fn apply_additional_edits_for_completion(
&self,
@@ -21070,85 +20825,6 @@ impl SemanticsProvider for Entity<Project> {
project.perform_rename(buffer.clone(), position, new_name, cx)
}))
}
fn pull_diagnostics_for_buffer(
&self,
buffer: Entity<Buffer>,
cx: &mut App,
) -> Task<anyhow::Result<()>> {
let diagnostics = self.update(cx, |project, cx| {
project
.lsp_store()
.update(cx, |lsp_store, cx| lsp_store.pull_diagnostics(buffer, cx))
});
let project = self.clone();
cx.spawn(async move |cx| {
let diagnostics = diagnostics.await.context("pulling diagnostics")?;
project.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
for diagnostics_set in diagnostics {
let LspPullDiagnostics::Response {
server_id,
uri,
diagnostics,
} = diagnostics_set
else {
continue;
};
let adapter = lsp_store.language_server_adapter_for_id(server_id);
let disk_based_sources = adapter
.as_ref()
.map(|adapter| adapter.disk_based_diagnostic_sources.as_slice())
.unwrap_or(&[]);
match diagnostics {
PulledDiagnostics::Unchanged { result_id } => {
lsp_store
.merge_diagnostics(
server_id,
lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics: Vec::new(),
version: None,
},
Some(result_id),
DiagnosticSourceKind::Pulled,
disk_based_sources,
|_, _| true,
cx,
)
.log_err();
}
PulledDiagnostics::Changed {
diagnostics,
result_id,
} => {
lsp_store
.merge_diagnostics(
server_id,
lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics,
version: None,
},
result_id,
DiagnosticSourceKind::Pulled,
disk_based_sources,
|old_diagnostic, _| match old_diagnostic.source_kind {
DiagnosticSourceKind::Pulled => false,
DiagnosticSourceKind::Other
| DiagnosticSourceKind::Pushed => true,
},
cx,
)
.log_err();
}
}
}
})
})
})
}
}
fn inlay_hint_settings(

View File

@@ -6300,296 +6300,6 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) {
));
}
#[gpui::test]
async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.set_state(indoc!(
r#"line onˇe
liˇne two
line three
line four"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_below(&Default::default(), window, cx);
});
// test multiple cursors expand in the same direction
cx.assert_editor_state(indoc!(
r#"line onˇe
liˇne twˇo
liˇne three
line four"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_below(&Default::default(), window, cx);
});
cx.update_editor(|editor, window, cx| {
editor.add_selection_below(&Default::default(), window, cx);
});
// test multiple cursors expand below overflow
cx.assert_editor_state(indoc!(
r#"line onˇe
liˇne twˇo
liˇne thˇree
liˇne foˇur"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_above(&Default::default(), window, cx);
});
// test multiple cursors retrieves back correctly
cx.assert_editor_state(indoc!(
r#"line onˇe
liˇne twˇo
liˇne thˇree
line four"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_above(&Default::default(), window, cx);
});
cx.update_editor(|editor, window, cx| {
editor.add_selection_above(&Default::default(), window, cx);
});
// test multiple cursor groups maintain independent direction - first expands up, second shrinks above
cx.assert_editor_state(indoc!(
r#"liˇne onˇe
liˇne two
line three
line four"#
));
cx.update_editor(|editor, window, cx| {
editor.undo_selection(&Default::default(), window, cx);
});
// test undo
cx.assert_editor_state(indoc!(
r#"line onˇe
liˇne twˇo
line three
line four"#
));
cx.update_editor(|editor, window, cx| {
editor.redo_selection(&Default::default(), window, cx);
});
// test redo
cx.assert_editor_state(indoc!(
r#"liˇne onˇe
liˇne two
line three
line four"#
));
cx.set_state(indoc!(
r#"abcd
ef«ghˇ»
ijkl
«mˇ»nop"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_above(&Default::default(), window, cx);
});
// test multiple selections expand in the same direction
cx.assert_editor_state(indoc!(
r#"ab«cdˇ»
ef«ghˇ»
«iˇ»jkl
«mˇ»nop"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_above(&Default::default(), window, cx);
});
// test multiple selection upward overflow
cx.assert_editor_state(indoc!(
r#"ab«cdˇ»
«eˇ»f«ghˇ»
«iˇ»jkl
«mˇ»nop"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_below(&Default::default(), window, cx);
});
// test multiple selection retrieves back correctly
cx.assert_editor_state(indoc!(
r#"abcd
ef«ghˇ»
«iˇ»jkl
«mˇ»nop"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_below(&Default::default(), window, cx);
});
// test multiple cursor groups maintain independent direction - first shrinks down, second expands below
cx.assert_editor_state(indoc!(
r#"abcd
ef«ghˇ»
ij«klˇ»
«mˇ»nop"#
));
cx.update_editor(|editor, window, cx| {
editor.undo_selection(&Default::default(), window, cx);
});
// test undo
cx.assert_editor_state(indoc!(
r#"abcd
ef«ghˇ»
«iˇ»jkl
«mˇ»nop"#
));
cx.update_editor(|editor, window, cx| {
editor.redo_selection(&Default::default(), window, cx);
});
// test redo
cx.assert_editor_state(indoc!(
r#"abcd
ef«ghˇ»
ij«klˇ»
«mˇ»nop"#
));
}
#[gpui::test]
async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.set_state(indoc!(
r#"line onˇe
liˇne two
line three
line four"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_below(&Default::default(), window, cx);
editor.add_selection_below(&Default::default(), window, cx);
editor.add_selection_below(&Default::default(), window, cx);
});
// initial state with two multi cursor groups
cx.assert_editor_state(indoc!(
r#"line onˇe
liˇne twˇo
liˇne thˇree
liˇne foˇur"#
));
// add single cursor in middle - simulate opt click
cx.update_editor(|editor, window, cx| {
let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
editor.begin_selection(new_cursor_point, true, 1, window, cx);
editor.end_selection(window, cx);
});
cx.assert_editor_state(indoc!(
r#"line onˇe
liˇne twˇo
liˇneˇ thˇree
liˇne foˇur"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_above(&Default::default(), window, cx);
});
// test new added selection expands above and existing selection shrinks
cx.assert_editor_state(indoc!(
r#"line onˇe
liˇneˇ twˇo
liˇneˇ thˇree
line four"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_above(&Default::default(), window, cx);
});
// test new added selection expands above and existing selection shrinks
cx.assert_editor_state(indoc!(
r#"lineˇ onˇe
liˇneˇ twˇo
lineˇ three
line four"#
));
// intial state with two selection groups
cx.set_state(indoc!(
r#"abcd
ef«ghˇ»
ijkl
«mˇ»nop"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_above(&Default::default(), window, cx);
editor.add_selection_above(&Default::default(), window, cx);
});
cx.assert_editor_state(indoc!(
r#"ab«cdˇ»
«eˇ»f«ghˇ»
«iˇ»jkl
«mˇ»nop"#
));
// add single selection in middle - simulate opt drag
cx.update_editor(|editor, window, cx| {
let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
editor.begin_selection(new_cursor_point, true, 1, window, cx);
editor.update_selection(
DisplayPoint::new(DisplayRow(2), 4),
0,
gpui::Point::<f32>::default(),
window,
cx,
);
editor.end_selection(window, cx);
});
cx.assert_editor_state(indoc!(
r#"ab«cdˇ»
«eˇ»f«ghˇ»
«iˇ»jk«lˇ»
«mˇ»nop"#
));
cx.update_editor(|editor, window, cx| {
editor.add_selection_below(&Default::default(), window, cx);
});
// test new added selection expands below, others shrinks from above
cx.assert_editor_state(indoc!(
r#"abcd
ef«ghˇ»
«iˇ»jk«lˇ»
«mˇ»no«pˇ»"#
));
}
#[gpui::test]
async fn test_select_next(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -13940,8 +13650,6 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
},
],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -21854,204 +21562,3 @@ fn assert_hunk_revert(
cx.assert_editor_state(expected_reverted_text_with_selections);
assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
}
#[gpui::test(iterations = 10)]
async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let diagnostic_requests = Arc::new(AtomicUsize::new(0));
let counter = diagnostic_requests.clone();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/a"),
json!({
"first.rs": "fn main() { let a = 5; }",
"second.rs": "// Test file",
}),
)
.await;
let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
lsp::DiagnosticOptions {
identifier: None,
inter_file_dependencies: true,
workspace_diagnostics: true,
work_done_progress_options: Default::default(),
},
)),
..Default::default()
},
..Default::default()
},
);
let editor = workspace
.update(cx, |workspace, window, cx| {
workspace.open_abs_path(
PathBuf::from(path!("/a/first.rs")),
OpenOptions::default(),
window,
cx,
)
})
.unwrap()
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let fake_server = fake_servers.next().await.unwrap();
let mut first_request = fake_server
.set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
let result_id = Some(new_result_id.to_string());
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
);
async move {
Ok(lsp::DocumentDiagnosticReportResult::Report(
lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
related_documents: None,
full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
items: Vec::new(),
result_id,
},
}),
))
}
});
let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
project.update(cx, |project, cx| {
let buffer_id = editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.expect("created a singleton buffer")
.read(cx)
.remote_id();
let buffer_result_id = project.lsp_store().read(cx).result_id(buffer_id);
assert_eq!(expected, buffer_result_id);
});
};
ensure_result_id(None, cx);
cx.executor().advance_clock(Duration::from_millis(60));
cx.executor().run_until_parked();
assert_eq!(
diagnostic_requests.load(atomic::Ordering::Acquire),
1,
"Opening file should trigger diagnostic request"
);
first_request
.next()
.await
.expect("should have sent the first diagnostics pull request");
ensure_result_id(Some("1".to_string()), cx);
// Editing should trigger diagnostics
editor.update_in(cx, |editor, window, cx| {
editor.handle_input("2", window, cx)
});
cx.executor().advance_clock(Duration::from_millis(60));
cx.executor().run_until_parked();
assert_eq!(
diagnostic_requests.load(atomic::Ordering::Acquire),
2,
"Editing should trigger diagnostic request"
);
ensure_result_id(Some("2".to_string()), cx);
// Moving cursor should not trigger diagnostic request
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
});
});
cx.executor().advance_clock(Duration::from_millis(60));
cx.executor().run_until_parked();
assert_eq!(
diagnostic_requests.load(atomic::Ordering::Acquire),
2,
"Cursor movement should not trigger diagnostic request"
);
ensure_result_id(Some("2".to_string()), cx);
// Multiple rapid edits should be debounced
for _ in 0..5 {
editor.update_in(cx, |editor, window, cx| {
editor.handle_input("x", window, cx)
});
}
cx.executor().advance_clock(Duration::from_millis(60));
cx.executor().run_until_parked();
let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
assert!(
final_requests <= 4,
"Multiple rapid edits should be debounced (got {final_requests} requests)",
);
ensure_result_id(Some(final_requests.to_string()), cx);
}
#[gpui::test]
async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
// Regression test for issue #11671
// Previously, adding a cursor after moving multiple cursors would reset
// the cursor count instead of adding to the existing cursors.
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
// Create a simple buffer with cursor at start
cx.set_state(indoc! {"
ˇaaaa
bbbb
cccc
dddd
eeee
ffff
gggg
hhhh"});
// Add 2 cursors below (so we have 3 total)
cx.update_editor(|editor, window, cx| {
editor.add_selection_below(&Default::default(), window, cx);
editor.add_selection_below(&Default::default(), window, cx);
});
// Verify we have 3 cursors
let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
assert_eq!(
initial_count, 3,
"Should have 3 cursors after adding 2 below"
);
// Move down one line
cx.update_editor(|editor, window, cx| {
editor.move_down(&MoveDown, window, cx);
});
// Add another cursor below
cx.update_editor(|editor, window, cx| {
editor.add_selection_below(&Default::default(), window, cx);
});
// Should now have 4 cursors (3 original + 1 new)
let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
assert_eq!(
final_count, 4,
"Should have 4 cursors after moving and adding another"
);
}

View File

@@ -3881,8 +3881,7 @@ impl EditorElement {
let edit_prediction = if edit_prediction_popover_visible {
self.editor.update(cx, move |editor, cx| {
let accept_binding =
editor.accept_edit_prediction_keybind(false, window, cx);
let accept_binding = editor.accept_edit_prediction_keybind(window, cx);
let mut element = editor.render_edit_prediction_cursor_popover(
min_width,
max_width,
@@ -5131,7 +5130,7 @@ impl EditorElement {
let is_singleton = self.editor.read(cx).is_singleton(cx);
let line_height = layout.position_map.line_height;
window.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox);
window.set_cursor_style(CursorStyle::Arrow, Some(&layout.gutter_hitbox));
for LineNumberLayout {
shaped_line,
@@ -5158,9 +5157,9 @@ impl EditorElement {
// In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
// In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
if is_singleton {
window.set_cursor_style(CursorStyle::IBeam, &hitbox);
window.set_cursor_style(CursorStyle::IBeam, Some(&hitbox));
} else {
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
window.set_cursor_style(CursorStyle::PointingHand, Some(&hitbox));
}
}
}
@@ -5378,7 +5377,7 @@ impl EditorElement {
.read(cx)
.all_diff_hunks_expanded()
{
window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox);
window.set_cursor_style(CursorStyle::PointingHand, Some(hunk_hitbox));
}
}
}
@@ -5452,7 +5451,7 @@ impl EditorElement {
|window| {
let editor = self.editor.read(cx);
if editor.mouse_cursor_hidden {
window.set_window_cursor_style(CursorStyle::None);
window.set_cursor_style(CursorStyle::None, None);
} else if editor
.hovered_link_state
.as_ref()
@@ -5460,10 +5459,13 @@ impl EditorElement {
{
window.set_cursor_style(
CursorStyle::PointingHand,
&layout.position_map.text_hitbox,
Some(&layout.position_map.text_hitbox),
);
} else {
window.set_cursor_style(CursorStyle::IBeam, &layout.position_map.text_hitbox);
window.set_cursor_style(
CursorStyle::IBeam,
Some(&layout.position_map.text_hitbox),
);
};
self.paint_lines_background(layout, window, cx);
@@ -5604,7 +5606,6 @@ impl EditorElement {
let Some(scrollbars_layout) = layout.scrollbars_layout.take() else {
return;
};
let any_scrollbar_dragged = self.editor.read(cx).scroll_manager.any_scrollbar_dragged();
for (scrollbar_layout, axis) in scrollbars_layout.iter_scrollbars() {
let hitbox = &scrollbar_layout.hitbox;
@@ -5670,11 +5671,7 @@ impl EditorElement {
BorderStyle::Solid,
));
if any_scrollbar_dragged {
window.set_window_cursor_style(CursorStyle::Arrow);
} else {
window.set_cursor_style(CursorStyle::Arrow, &hitbox);
}
window.set_cursor_style(CursorStyle::Arrow, Some(&hitbox));
}
})
}
@@ -5742,7 +5739,7 @@ impl EditorElement {
}
});
if any_scrollbar_dragged {
if self.editor.read(cx).scroll_manager.any_scrollbar_dragged() {
window.on_mouse_event({
let editor = self.editor.clone();
move |_: &MouseUpEvent, phase, window, cx| {
@@ -6128,7 +6125,6 @@ impl EditorElement {
fn paint_minimap(&self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
if let Some(mut layout) = layout.minimap.take() {
let minimap_hitbox = layout.thumb_layout.hitbox.clone();
let dragging_minimap = self.editor.read(cx).scroll_manager.is_dragging_minimap();
window.paint_layer(layout.thumb_layout.hitbox.bounds, |window| {
window.with_element_namespace("minimap", |window| {
@@ -6180,11 +6176,7 @@ impl EditorElement {
});
});
if dragging_minimap {
window.set_window_cursor_style(CursorStyle::Arrow);
} else {
window.set_cursor_style(CursorStyle::Arrow, &minimap_hitbox);
}
window.set_cursor_style(CursorStyle::Arrow, Some(&minimap_hitbox));
let minimap_axis = ScrollbarAxis::Vertical;
let pixels_per_line = (minimap_hitbox.size.height / layout.max_scroll_top)
@@ -6245,7 +6237,7 @@ impl EditorElement {
}
});
if dragging_minimap {
if self.editor.read(cx).scroll_manager.is_dragging_minimap() {
window.on_mouse_event({
let editor = self.editor.clone();
move |event: &MouseUpEvent, phase, window, cx| {
@@ -6673,7 +6665,7 @@ impl AcceptEditPredictionBinding {
pub fn keystroke(&self) -> Option<&Keystroke> {
if let Some(binding) = self.0.as_ref() {
match &binding.keystrokes() {
[keystroke, ..] => Some(keystroke),
[keystroke] => Some(keystroke),
_ => None,
}
} else {

View File

@@ -869,7 +869,6 @@ impl InfoPopover {
let keyboard_grace = Rc::clone(&self.keyboard_grace);
div()
.id("info_popover")
.occlude()
.elevation_2(cx)
// Prevent a mouse down/move on the popover from being propagated to the editor,
// because that would dismiss the popover.

View File

@@ -42,8 +42,8 @@ where
.selections
.disjoint_anchors()
.iter()
.filter_map(|selection| Some((selection.head(), selection.head().buffer_id?)))
.unique_by(|(_, buffer_id)| *buffer_id)
.filter(|selection| selection.start == selection.end)
.filter_map(|selection| Some((selection.start, selection.start.buffer_id?)))
.filter_map(|(trigger_anchor, buffer_id)| {
let buffer = editor.buffer().read(cx).buffer(buffer_id)?;
let language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
@@ -53,6 +53,7 @@ where
None
}
})
.unique_by(|(_, buffer, _)| buffer.read(cx).remote_id())
.collect::<Vec<_>>();
let applicable_buffer_tasks = applicable_buffers

View File

@@ -522,12 +522,4 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
) -> Option<Task<anyhow::Result<project::ProjectTransaction>>> {
None
}
fn pull_diagnostics_for_buffer(
&self,
_: Entity<Buffer>,
_: &mut App,
) -> Task<anyhow::Result<()>> {
Task::ready(Ok(()))
}
}

View File

@@ -132,6 +132,9 @@ pub fn expand_macro_recursively(
window: &mut Window,
cx: &mut Context<Editor>,
) {
if editor.selections.count() == 0 {
return;
}
let Some(project) = &editor.project else {
return;
};

View File

@@ -24,6 +24,7 @@ anyhow.workspace = true
assistant_tool.workspace = true
assistant_tools.workspace = true
async-trait.workspace = true
async-watch.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
clap.workspace = true
@@ -65,6 +66,5 @@ toml.workspace = true
unindent.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true

View File

@@ -385,7 +385,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
extension::init(cx);
let (mut tx, rx) = watch::channel(None);
let (tx, rx) = async_watch::channel(None);
cx.observe_global::<SettingsStore>(move |cx| {
let settings = &ProjectSettings::get_global(cx).node;
let options = NodeBinaryOptions {

View File

@@ -294,7 +294,6 @@ impl ExampleContext {
| ThreadEvent::MessageDeleted(_)
| ThreadEvent::SummaryChanged
| ThreadEvent::SummaryGenerated
| ThreadEvent::ProfileChanged
| ThreadEvent::ReceivedTextChunk
| ThreadEvent::StreamedToolUse { .. }
| ThreadEvent::CheckpointChanged

View File

@@ -306,19 +306,17 @@ impl ExampleInstance {
let thread_store = thread_store.await?;
let profile_id = meta.profile_id.clone();
thread_store.update(cx, |thread_store, cx| thread_store.load_profile_by_id(profile_id, cx)).expect("Failed to load profile");
let thread =
thread_store.update(cx, |thread_store, cx| {
let thread = if let Some(json) = &meta.existing_thread_json {
if let Some(json) = &meta.existing_thread_json {
let serialized = SerializedThread::from_json(json.as_bytes()).expect("Can't read serialized thread");
thread_store.create_thread_from_serialized(serialized, cx)
} else {
thread_store.create_thread(cx)
};
thread.update(cx, |thread, cx| {
thread.set_profile(meta.profile_id.clone(), cx);
});
thread
}
})?;

View File

@@ -87,7 +87,7 @@ impl extension::Extension for WasmExtension {
resource,
)
.await?
.map_err(|err| store.data().extension_error(err))?;
.map_err(|err| anyhow!("{err}"))?;
Ok(command.into())
}
@@ -113,7 +113,7 @@ impl extension::Extension for WasmExtension {
resource,
)
.await?
.map_err(|err| store.data().extension_error(err))?;
.map_err(|err| anyhow!("{err}"))?;
anyhow::Ok(options)
}
.boxed()
@@ -136,7 +136,7 @@ impl extension::Extension for WasmExtension {
resource,
)
.await?
.map_err(|err| store.data().extension_error(err))?;
.map_err(|err| anyhow!("{err}"))?;
anyhow::Ok(options)
}
.boxed()
@@ -161,7 +161,7 @@ impl extension::Extension for WasmExtension {
resource,
)
.await?
.map_err(|err| store.data().extension_error(err))?;
.map_err(|err| anyhow!("{err}"))?;
anyhow::Ok(options)
}
.boxed()
@@ -186,7 +186,7 @@ impl extension::Extension for WasmExtension {
resource,
)
.await?
.map_err(|err| store.data().extension_error(err))?;
.map_err(|err| anyhow!("{err}"))?;
anyhow::Ok(options)
}
.boxed()
@@ -208,7 +208,7 @@ impl extension::Extension for WasmExtension {
completions.into_iter().map(Into::into).collect(),
)
.await?
.map_err(|err| store.data().extension_error(err))?;
.map_err(|err| anyhow!("{err}"))?;
Ok(labels
.into_iter()
@@ -234,7 +234,7 @@ impl extension::Extension for WasmExtension {
symbols.into_iter().map(Into::into).collect(),
)
.await?
.map_err(|err| store.data().extension_error(err))?;
.map_err(|err| anyhow!("{err}"))?;
Ok(labels
.into_iter()
@@ -256,7 +256,7 @@ impl extension::Extension for WasmExtension {
let completions = extension
.call_complete_slash_command_argument(store, &command.into(), &arguments)
.await?
.map_err(|err| store.data().extension_error(err))?;
.map_err(|err| anyhow!("{err}"))?;
Ok(completions.into_iter().map(Into::into).collect())
}
@@ -282,7 +282,7 @@ impl extension::Extension for WasmExtension {
let output = extension
.call_run_slash_command(store, &command.into(), &arguments, resource)
.await?
.map_err(|err| store.data().extension_error(err))?;
.map_err(|err| anyhow!("{err}"))?;
Ok(output.into())
}
@@ -302,7 +302,7 @@ impl extension::Extension for WasmExtension {
let command = extension
.call_context_server_command(store, context_server_id.clone(), project_resource)
.await?
.map_err(|err| store.data().extension_error(err))?;
.map_err(|err| anyhow!("{err}"))?;
anyhow::Ok(command.into())
}
.boxed()
@@ -325,7 +325,7 @@ impl extension::Extension for WasmExtension {
project_resource,
)
.await?
.map_err(|err| store.data().extension_error(err))?
.map_err(|err| anyhow!("{err}"))?
else {
return Ok(None);
};
@@ -343,7 +343,7 @@ impl extension::Extension for WasmExtension {
let packages = extension
.call_suggest_docs_packages(store, provider.as_ref())
.await?
.map_err(|err| store.data().extension_error(err))?;
.map_err(|err| anyhow!("{err:?}"))?;
Ok(packages)
}
@@ -369,7 +369,7 @@ impl extension::Extension for WasmExtension {
kv_store_resource,
)
.await?
.map_err(|err| store.data().extension_error(err))?;
.map_err(|err| anyhow!("{err:?}"))?;
anyhow::Ok(())
}
@@ -390,7 +390,7 @@ impl extension::Extension for WasmExtension {
let dap_binary = extension
.call_get_dap_binary(store, dap_name, config, user_installed_path, resource)
.await?
.map_err(|err| store.data().extension_error(err))?;
.map_err(|err| anyhow!("{err:?}"))?;
let dap_binary = dap_binary.try_into()?;
Ok(dap_binary)
}
@@ -406,7 +406,7 @@ impl extension::Extension for WasmExtension {
.call_dap_schema(store)
.await
.and_then(|schema| serde_json::to_value(schema).map_err(|err| err.to_string()))
.map_err(|err| store.data().extension_error(err))
.map_err(|err| anyhow!(err.to_string()))
}
.boxed()
})
@@ -680,15 +680,6 @@ impl WasmState {
fn work_dir(&self) -> PathBuf {
self.host.work_dir.join(self.manifest.id.as_ref())
}
fn extension_error(&self, message: String) -> anyhow::Error {
anyhow!(
"from extension \"{}\" version {}: {}",
self.manifest.name,
self.manifest.version,
message
)
}
}
impl wasi::WasiView for WasmState {

View File

@@ -459,7 +459,7 @@ enum Match {
}
impl Match {
fn relative_path(&self) -> Option<&Arc<Path>> {
fn path(&self) -> Option<&Arc<Path>> {
match self {
Match::History { path, .. } => Some(&path.project.path),
Match::Search(panel_match) => Some(&panel_match.0.path),
@@ -467,26 +467,6 @@ impl Match {
}
}
fn abs_path(&self, project: &Entity<Project>, cx: &App) -> Option<PathBuf> {
match self {
Match::History { path, .. } => path.absolute.clone().or_else(|| {
project
.read(cx)
.worktree_for_id(path.project.worktree_id, cx)?
.read(cx)
.absolutize(&path.project.path)
.ok()
}),
Match::Search(ProjectPanelOrdMatch(path_match)) => project
.read(cx)
.worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx)?
.read(cx)
.absolutize(&path_match.path)
.ok(),
Match::CreateNew(_) => None,
}
}
fn panel_match(&self) -> Option<&ProjectPanelOrdMatch> {
match self {
Match::History { panel_match, .. } => panel_match.as_ref(),
@@ -521,7 +501,7 @@ impl Matches {
// reason for the matches set to change.
self.matches
.iter()
.position(|m| match m.relative_path() {
.position(|m| match m.path() {
Some(p) => path.project.path == *p,
None => false,
})
@@ -1590,8 +1570,7 @@ impl PickerDelegate for FileFinderDelegate {
if !settings.file_icons {
return None;
}
let abs_path = path_match.abs_path(&self.project, cx)?;
let file_name = abs_path.file_name()?;
let file_name = path_match.path()?.file_name()?;
let icon = FileIcons::get_icon(file_name.as_ref(), cx)?;
Some(Icon::from_path(icon).color(Color::Muted))
});

View File

@@ -5,7 +5,7 @@ use futures::future::{self, BoxFuture};
use git::{
blame::Blame,
repository::{
AskPassDelegate, Branch, CommitDetails, CommitOptions, FetchOptions, GitRepository,
AskPassDelegate, Branch, CommitDetails, CommitOptions, GitRepository,
GitRepositoryCheckpoint, PushOptions, Remote, RepoPath, ResetMode,
},
status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
@@ -405,7 +405,6 @@ impl GitRepository for FakeGitRepository {
fn fetch(
&self,
_fetch_options: FetchOptions,
_askpass: AskPassDelegate,
_env: Arc<HashMap<String, String>>,
_cx: AsyncApp,

View File

@@ -46,11 +46,9 @@ actions!(
TrashUntrackedFiles,
Uncommit,
Push,
PushTo,
ForcePush,
Pull,
Fetch,
FetchFrom,
Commit,
Amend,
Cancel,

View File

@@ -193,44 +193,6 @@ pub enum ResetMode {
Mixed,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum FetchOptions {
All,
Remote(Remote),
}
impl FetchOptions {
pub fn to_proto(&self) -> Option<String> {
match self {
FetchOptions::All => None,
FetchOptions::Remote(remote) => Some(remote.clone().name.into()),
}
}
pub fn from_proto(remote_name: Option<String>) -> Self {
match remote_name {
Some(name) => FetchOptions::Remote(Remote { name: name.into() }),
None => FetchOptions::All,
}
}
pub fn name(&self) -> SharedString {
match self {
Self::All => "Fetch all remotes".into(),
Self::Remote(remote) => remote.name.clone(),
}
}
}
impl std::fmt::Display for FetchOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FetchOptions::All => write!(f, "--all"),
FetchOptions::Remote(remote) => write!(f, "{}", remote.name),
}
}
}
/// Modifies .git/info/exclude temporarily
pub struct GitExcludeOverride {
git_exclude_path: PathBuf,
@@ -419,7 +381,6 @@ pub trait GitRepository: Send + Sync {
fn fetch(
&self,
fetch_options: FetchOptions,
askpass: AskPassDelegate,
env: Arc<HashMap<String, String>>,
// This method takes an AsyncApp to ensure it's invoked on the main thread,
@@ -1235,20 +1196,18 @@ impl GitRepository for RealGitRepository {
fn fetch(
&self,
fetch_options: FetchOptions,
ask_pass: AskPassDelegate,
env: Arc<HashMap<String, String>>,
cx: AsyncApp,
) -> BoxFuture<Result<RemoteCommandOutput>> {
let working_directory = self.working_directory();
let remote_name = format!("{}", fetch_options);
let executor = cx.background_executor().clone();
async move {
let mut command = new_smol_command("git");
command
.envs(env.iter())
.current_dir(&working_directory?)
.args(["fetch", &remote_name])
.args(["fetch", "--all"])
.stdout(smol::process::Stdio::piped())
.stderr(smol::process::Stdio::piped());

View File

@@ -20,8 +20,8 @@ use editor::{
use futures::StreamExt as _;
use git::blame::ParsedCommitMessage;
use git::repository::{
Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, PushOptions,
Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,
Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, PushOptions, Remote,
RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,
};
use git::status::StageStatus;
use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus};
@@ -383,6 +383,7 @@ pub(crate) fn commit_message_editor(
commit_editor.set_show_gutter(false, cx);
commit_editor.set_show_wrap_guides(false, cx);
commit_editor.set_show_indent_guides(false, cx);
commit_editor.set_hard_wrap(Some(72), cx);
let placeholder = placeholder.unwrap_or("Enter commit message".into());
commit_editor.set_placeholder_text(placeholder, cx);
commit_editor
@@ -1482,48 +1483,15 @@ impl GitPanel {
}
}
fn custom_or_suggested_commit_message(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<String> {
let git_commit_language = self.commit_editor.read(cx).language_at(0, cx);
fn custom_or_suggested_commit_message(&self, cx: &mut Context<Self>) -> Option<String> {
let message = self.commit_editor.read(cx).text(cx);
if message.is_empty() {
return self
.suggest_commit_message(cx)
.filter(|message| !message.trim().is_empty());
} else if message.trim().is_empty() {
return None;
}
let buffer = cx.new(|cx| {
let mut buffer = Buffer::local(message, cx);
buffer.set_language(git_commit_language, cx);
buffer
});
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(&Default::default(), window, cx);
editor.text(cx)
});
if wrapped_message.trim().is_empty() {
return None;
}
Some(wrapped_message)
}
fn has_commit_message(&self, cx: &mut Context<Self>) -> bool {
let text = self.commit_editor.read(cx).text(cx);
if !text.trim().is_empty() {
return true;
} else if text.is_empty() {
return self
.suggest_commit_message(cx)
.is_some_and(|text| !text.trim().is_empty());
} else {
return false;
if !message.trim().is_empty() {
return Some(message);
}
self.suggest_commit_message(cx)
.filter(|message| !message.trim().is_empty())
}
pub(crate) fn commit_changes(
@@ -1552,7 +1520,7 @@ impl GitPanel {
return;
}
let commit_message = self.custom_or_suggested_commit_message(window, cx);
let commit_message = self.custom_or_suggested_commit_message(cx);
let Some(mut message) = commit_message else {
self.commit_editor.read(cx).focus_handle(cx).focus(window);
@@ -1840,49 +1808,7 @@ impl GitPanel {
}));
}
fn get_fetch_options(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Option<FetchOptions>> {
let repo = self.active_repository.clone();
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |_, cx| {
let repo = repo?;
let remotes = repo
.update(cx, |repo, _| repo.get_remotes(None))
.ok()?
.await
.ok()?
.log_err()?;
let mut remotes: Vec<_> = remotes.into_iter().map(FetchOptions::Remote).collect();
if remotes.len() > 1 {
remotes.push(FetchOptions::All);
}
let selection = cx
.update(|window, cx| {
picker_prompt::prompt(
"Pick which remote to fetch",
remotes.iter().map(|r| r.name()).collect(),
workspace,
window,
cx,
)
})
.ok()?
.await?;
remotes.get(selection).cloned()
})
}
pub(crate) fn fetch(
&mut self,
is_fetch_all: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
pub(crate) fn fetch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self.can_push_and_pull(cx) {
return;
}
@@ -1893,28 +1819,13 @@ impl GitPanel {
telemetry::event!("Git Fetched");
let askpass = self.askpass_delegate("git fetch", window, cx);
let this = cx.weak_entity();
let fetch_options = if is_fetch_all {
Task::ready(Some(FetchOptions::All))
} else {
self.get_fetch_options(window, cx)
};
window
.spawn(cx, async move |cx| {
let Some(fetch_options) = fetch_options.await else {
return Ok(());
};
let fetch = repo.update(cx, |repo, cx| {
repo.fetch(fetch_options.clone(), askpass, cx)
})?;
let fetch = repo.update(cx, |repo, cx| repo.fetch(askpass, cx))?;
let remote_message = fetch.await?;
this.update(cx, |this, cx| {
let action = match fetch_options {
FetchOptions::All => RemoteAction::Fetch(None),
FetchOptions::Remote(remote) => RemoteAction::Fetch(Some(remote)),
};
let action = RemoteAction::Fetch;
match remote_message {
Ok(remote_message) => this.show_remote_output(action, remote_message, cx),
Err(e) => {
@@ -2025,7 +1936,7 @@ impl GitPanel {
};
telemetry::event!("Git Pulled");
let branch = branch.clone();
let remote = self.get_remote(false, window, cx);
let remote = self.get_current_remote(window, cx);
cx.spawn_in(window, async move |this, cx| {
let remote = match remote.await {
Ok(Some(remote)) => remote,
@@ -2070,13 +1981,7 @@ impl GitPanel {
.detach_and_log_err(cx);
}
pub(crate) fn push(
&mut self,
force_push: bool,
select_remote: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
pub(crate) fn push(&mut self, force_push: bool, window: &mut Window, cx: &mut Context<Self>) {
if !self.can_push_and_pull(cx) {
return;
}
@@ -2101,7 +2006,7 @@ impl GitPanel {
_ => None,
}
};
let remote = self.get_remote(select_remote, window, cx);
let remote = self.get_current_remote(window, cx);
cx.spawn_in(window, async move |this, cx| {
let remote = match remote.await {
@@ -2175,9 +2080,8 @@ impl GitPanel {
!self.project.read(cx).is_via_collab()
}
fn get_remote(
fn get_current_remote(
&mut self,
always_select: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl Future<Output = anyhow::Result<Option<Remote>>> + use<> {
@@ -2187,37 +2091,38 @@ impl GitPanel {
async move {
let repo = repo.context("No active repository")?;
let current_remotes: Vec<Remote> = repo
let mut current_remotes: Vec<Remote> = repo
.update(&mut cx, |repo, _| {
let current_branch = if always_select {
None
} else {
let current_branch = repo.branch.as_ref().context("No active branch")?;
Some(current_branch.name().to_string())
};
anyhow::Ok(repo.get_remotes(current_branch))
let current_branch = repo.branch.as_ref().context("No active branch")?;
anyhow::Ok(repo.get_remotes(Some(current_branch.name().to_string())))
})??
.await??;
let current_remotes: Vec<_> = current_remotes
.into_iter()
.map(|remotes| remotes.name)
.collect();
let selection = cx
.update(|window, cx| {
picker_prompt::prompt(
"Pick which remote to push to",
current_remotes.clone(),
workspace,
window,
cx,
)
})?
.await;
if current_remotes.len() == 0 {
anyhow::bail!("No active remote");
} else if current_remotes.len() == 1 {
return Ok(Some(current_remotes.pop().unwrap()));
} else {
let current_remotes: Vec<_> = current_remotes
.into_iter()
.map(|remotes| remotes.name)
.collect();
let selection = cx
.update(|window, cx| {
picker_prompt::prompt(
"Pick which remote to push to",
current_remotes.clone(),
workspace,
window,
cx,
)
})?
.await;
Ok(selection.map(|selection| Remote {
name: current_remotes[selection].clone(),
}))
Ok(selection.map(|selection| Remote {
name: current_remotes[selection].clone(),
}))
}
}
}
@@ -2927,7 +2832,7 @@ impl GitPanel {
(false, "No changes to commit")
} else if self.pending_commit.is_some() {
(false, "Commit in progress")
} else if !self.has_commit_message(cx) {
} else if self.custom_or_suggested_commit_message(cx).is_none() {
(false, "No commit message")
} else if !self.has_write_access(cx) {
(false, "You do not have write access to this project")

View File

@@ -59,15 +59,7 @@ pub fn init(cx: &mut App) {
return;
};
panel.update(cx, |panel, cx| {
panel.fetch(true, window, cx);
});
});
workspace.register_action(|workspace, _: &git::FetchFrom, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.fetch(false, window, cx);
panel.fetch(window, cx);
});
});
workspace.register_action(|workspace, _: &git::Push, window, cx| {
@@ -75,15 +67,7 @@ pub fn init(cx: &mut App) {
return;
};
panel.update(cx, |panel, cx| {
panel.push(false, false, window, cx);
});
});
workspace.register_action(|workspace, _: &git::PushTo, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.push(false, true, window, cx);
panel.push(false, window, cx);
});
});
workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
@@ -91,7 +75,7 @@ pub fn init(cx: &mut App) {
return;
};
panel.update(cx, |panel, cx| {
panel.push(true, false, window, cx);
panel.push(true, window, cx);
});
});
workspace.register_action(|workspace, _: &git::Pull, window, cx| {
@@ -383,11 +367,9 @@ mod remote_button {
el.context(keybinding_target.clone())
})
.action("Fetch", git::Fetch.boxed_clone())
.action("Fetch From", git::FetchFrom.boxed_clone())
.action("Pull", git::Pull.boxed_clone())
.separator()
.action("Push", git::Push.boxed_clone())
.action("Push To", git::PushTo.boxed_clone())
.action("Force Push", git::ForcePush.boxed_clone())
}))
})

View File

@@ -28,8 +28,6 @@ pub fn prompt(
) -> Task<Option<usize>> {
if options.is_empty() {
return Task::ready(None);
} else if options.len() == 1 {
return Task::ready(Some(0));
}
let prompt = prompt.to_string().into();

View File

@@ -6,7 +6,7 @@ use util::ResultExt as _;
#[derive(Clone)]
pub enum RemoteAction {
Fetch(Option<Remote>),
Fetch,
Pull(Remote),
Push(SharedString, Remote),
}
@@ -14,7 +14,7 @@ pub enum RemoteAction {
impl RemoteAction {
pub fn name(&self) -> &'static str {
match self {
RemoteAction::Fetch(_) => "fetch",
RemoteAction::Fetch => "fetch",
RemoteAction::Pull(_) => "pull",
RemoteAction::Push(_, _) => "push",
}
@@ -34,19 +34,15 @@ pub struct SuccessMessage {
pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> SuccessMessage {
match action {
RemoteAction::Fetch(remote) => {
RemoteAction::Fetch => {
if output.stderr.is_empty() {
SuccessMessage {
message: "Already up to date".into(),
style: SuccessStyle::Toast,
}
} else {
let message = match remote {
Some(remote) => format!("Synchronized with {}", remote.name),
None => "Synchronized with remotes".into(),
};
SuccessMessage {
message,
message: "Synchronized with remotes".into(),
style: SuccessStyle::ToastWithLog { output },
}
}

View File

@@ -508,16 +508,6 @@ pub enum Model {
Gemini25ProPreview0325,
#[serde(rename = "gemini-2.5-flash-preview-04-17")]
Gemini25FlashPreview0417,
#[serde(
rename = "gemini-2.5-flash-preview-latest",
alias = "gemini-2.5-flash-preview-05-20"
)]
Gemini25FlashPreview,
#[serde(
rename = "gemini-2.5-pro-preview-latest",
alias = "gemini-2.5-pro-preview-06-05"
)]
Gemini25ProPreview,
#[serde(rename = "custom")]
Custom {
name: String,
@@ -545,24 +535,6 @@ impl Model {
Model::Gemini25ProExp0325 => "gemini-2.5-pro-exp-03-25",
Model::Gemini25ProPreview0325 => "gemini-2.5-pro-preview-03-25",
Model::Gemini25FlashPreview0417 => "gemini-2.5-flash-preview-04-17",
Model::Gemini25FlashPreview => "gemini-2.5-flash-preview-latest",
Model::Gemini25ProPreview => "gemini-2.5-pro-preview-latest",
Model::Custom { name, .. } => name,
}
}
pub fn request_id(&self) -> &str {
match self {
Model::Gemini15Pro => "gemini-1.5-pro",
Model::Gemini15Flash => "gemini-1.5-flash",
Model::Gemini20Pro => "gemini-2.0-pro-exp",
Model::Gemini20Flash => "gemini-2.0-flash",
Model::Gemini20FlashThinking => "gemini-2.0-flash-thinking-exp",
Model::Gemini20FlashLite => "gemini-2.0-flash-lite-preview",
Model::Gemini25ProExp0325 => "gemini-2.5-pro-exp-03-25",
Model::Gemini25ProPreview0325 => "gemini-2.5-pro-preview-03-25",
Model::Gemini25FlashPreview0417 => "gemini-2.5-flash-preview-04-17",
Model::Gemini25FlashPreview => "gemini-2.5-flash-preview-05-20",
Model::Gemini25ProPreview => "gemini-2.5-pro-preview-06-05",
Model::Custom { name, .. } => name,
}
}
@@ -576,10 +548,8 @@ impl Model {
Model::Gemini20FlashThinking => "Gemini 2.0 Flash Thinking",
Model::Gemini20FlashLite => "Gemini 2.0 Flash Lite",
Model::Gemini25ProExp0325 => "Gemini 2.5 Pro Exp",
Model::Gemini25ProPreview0325 => "Gemini 2.5 Pro Preview (0325)",
Model::Gemini25FlashPreview0417 => "Gemini 2.5 Flash Preview (0417)",
Model::Gemini25FlashPreview => "Gemini 2.5 Flash Preview",
Model::Gemini25ProPreview => "Gemini 2.5 Pro Preview",
Model::Gemini25ProPreview0325 => "Gemini 2.5 Pro Preview",
Model::Gemini25FlashPreview0417 => "Gemini 2.5 Flash Preview",
Self::Custom {
name, display_name, ..
} => display_name.as_ref().unwrap_or(name),
@@ -599,8 +569,6 @@ impl Model {
Model::Gemini25ProExp0325 => ONE_MILLION,
Model::Gemini25ProPreview0325 => ONE_MILLION,
Model::Gemini25FlashPreview0417 => ONE_MILLION,
Model::Gemini25FlashPreview => ONE_MILLION,
Model::Gemini25ProPreview => ONE_MILLION,
Model::Custom { max_tokens, .. } => *max_tokens,
}
}
@@ -614,8 +582,6 @@ impl Model {
| Self::Gemini20FlashThinking
| Self::Gemini20FlashLite
| Self::Gemini25ProExp0325
| Self::Gemini25ProPreview
| Self::Gemini25FlashPreview
| Self::Gemini25ProPreview0325
| Self::Gemini25FlashPreview0417 => GoogleModelMode::Default,
Self::Custom { mode, .. } => *mode,

View File

@@ -1,14 +1,13 @@
use gpui::{
Application, Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder,
PathStyle, Pixels, Point, Render, SharedString, StrokeOptions, Window, WindowOptions, canvas,
div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size,
PathStyle, Pixels, Point, Render, StrokeOptions, Window, WindowOptions, canvas, div,
linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size,
};
struct PaintingViewer {
default_lines: Vec<(Path<Pixels>, Background)>,
lines: Vec<Vec<Point<Pixels>>>,
start: Point<Pixels>,
dashed: bool,
_painting: bool,
}
@@ -141,7 +140,7 @@ impl PaintingViewer {
.with_line_join(lyon::path::LineJoin::Bevel);
let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options));
builder.move_to(point(px(40.), px(320.)));
for i in 1..50 {
for i in 0..50 {
builder.line_to(point(
px(40.0 + i as f32 * 10.0),
px(320.0 + (i as f32 * 10.0).sin() * 40.0),
@@ -154,7 +153,6 @@ impl PaintingViewer {
default_lines: lines.clone(),
lines: vec![],
start: point(px(0.), px(0.)),
dashed: false,
_painting: false,
}
}
@@ -164,30 +162,10 @@ impl PaintingViewer {
cx.notify();
}
}
fn button(
text: &str,
cx: &mut Context<PaintingViewer>,
on_click: impl Fn(&mut PaintingViewer, &mut Context<PaintingViewer>) + 'static,
) -> impl IntoElement {
div()
.id(SharedString::from(text.to_string()))
.child(text.to_string())
.bg(gpui::black())
.text_color(gpui::white())
.active(|this| this.opacity(0.8))
.flex()
.px_3()
.py_1()
.on_click(cx.listener(move |this, _, _, cx| on_click(this, cx)))
}
impl Render for PaintingViewer {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let default_lines = self.default_lines.clone();
let lines = self.lines.clone();
let dashed = self.dashed;
div()
.font_family(".SystemUIFont")
.bg(gpui::white())
@@ -204,14 +182,17 @@ impl Render for PaintingViewer {
.child("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)")
.child(
div()
.id("clear")
.child("Clean up")
.bg(gpui::black())
.text_color(gpui::white())
.active(|this| this.opacity(0.8))
.flex()
.gap_x_2()
.child(button(
if dashed { "Solid" } else { "Dashed" },
cx,
move |this, _| this.dashed = !dashed,
))
.child(button("Clear", cx, |this, cx| this.clear(cx))),
.px_3()
.py_1()
.on_click(cx.listener(|this, _, _, cx| {
this.clear(cx);
})),
),
)
.child(
@@ -221,6 +202,7 @@ impl Render for PaintingViewer {
canvas(
move |_, _, _| {},
move |_, _, window, _| {
for (path, color) in default_lines {
window.paint_path(path, color);
}
@@ -231,9 +213,6 @@ impl Render for PaintingViewer {
}
let mut builder = PathBuilder::stroke(px(1.));
if dashed {
builder = builder.dash_array(&[px(4.), px(2.)]);
}
for (i, p) in points.into_iter().enumerate() {
if i == 0 {
builder.move_to(p);

View File

@@ -1,60 +0,0 @@
use gpui::{
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
size,
};
struct Scrollable {}
impl Render for Scrollable {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.id("vertical")
.p_4()
.overflow_scroll()
.bg(gpui::white())
.child("Example for test 2 way scroll in nested layout")
.child(
div()
.h(px(5000.))
.border_1()
.border_color(gpui::blue())
.bg(gpui::blue().opacity(0.05))
.p_4()
.child(
div()
.mb_5()
.w_full()
.id("horizontal")
.overflow_scroll()
.child(
div()
.w(px(2000.))
.h(px(150.))
.bg(gpui::green().opacity(0.1))
.hover(|this| this.bg(gpui::green().opacity(0.2)))
.border_1()
.border_color(gpui::green())
.p_4()
.child("Scroll Horizontal"),
),
)
.child("Scroll Vertical"),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| Scrollable {}),
)
.unwrap();
cx.activate(true);
});
}

View File

@@ -61,7 +61,7 @@ impl Render for WindowShadow {
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
Some(&hitbox),
);
},
)

View File

@@ -21,8 +21,7 @@ use crate::{
HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent,
LayoutId, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
Overflow, ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
StyleRefinement, Styled, Task, TooltipId, Visibility, Window, WindowControlArea, point, px,
size,
StyleRefinement, Styled, Task, TooltipId, Visibility, Window, point, px, size,
};
use collections::HashMap;
use refineable::Refineable;
@@ -576,12 +575,6 @@ impl Interactivity {
self.hitbox_behavior = HitboxBehavior::BlockMouse;
}
/// Set the bounds of this element as a window control area for the platform window.
/// The imperative API equivalent to [`InteractiveElement::window_control_area`]
pub fn window_control_area(&mut self, area: WindowControlArea) {
self.window_control = Some(area);
}
/// Block non-scroll mouse interactions with elements behind this element's hitbox. See
/// [`Hitbox::is_hovered`] for details.
///
@@ -965,13 +958,6 @@ pub trait InteractiveElement: Sized {
self
}
/// Set the bounds of this element as a window control area for the platform window.
/// The fluent API equivalent to [`Interactivity::window_control_area`]
fn window_control_area(mut self, area: WindowControlArea) -> Self {
self.interactivity().window_control_area(area);
self
}
/// Block non-scroll mouse interactions with elements behind this element's hitbox. See
/// [`Hitbox::is_hovered`] for details.
///
@@ -1461,7 +1447,6 @@ pub struct Interactivity {
pub(crate) drag_listener: Option<(Arc<dyn Any>, DragListener)>,
pub(crate) hover_listener: Option<Box<dyn Fn(&bool, &mut Window, &mut App)>>,
pub(crate) tooltip_builder: Option<TooltipBuilder>,
pub(crate) window_control: Option<WindowControlArea>,
pub(crate) hitbox_behavior: HitboxBehavior,
#[cfg(any(feature = "inspector", debug_assertions))]
@@ -1626,7 +1611,6 @@ impl Interactivity {
fn should_insert_hitbox(&self, style: &Style, window: &Window, cx: &App) -> bool {
self.hitbox_behavior != HitboxBehavior::Normal
|| self.window_control.is_some()
|| style.mouse_cursor.is_some()
|| self.group.is_some()
|| self.scroll_offset.is_some()
@@ -1744,11 +1728,11 @@ impl Interactivity {
if let Some(drag) = cx.active_drag.as_ref() {
if let Some(mouse_cursor) = drag.cursor_style {
window.set_window_cursor_style(mouse_cursor);
window.set_cursor_style(mouse_cursor, None);
}
} else {
if let Some(mouse_cursor) = style.mouse_cursor {
window.set_cursor_style(mouse_cursor, hitbox);
window.set_cursor_style(mouse_cursor, Some(hitbox));
}
}
@@ -1756,11 +1740,6 @@ impl Interactivity {
GroupHitboxes::push(group, hitbox.id, cx);
}
if let Some(area) = self.window_control {
window
.insert_window_control_hitbox(area, hitbox.clone());
}
self.paint_mouse_listeners(
hitbox,
element_state.as_mut(),
@@ -2320,6 +2299,7 @@ impl Interactivity {
}
scroll_offset.y += delta_y;
scroll_offset.x += delta_x;
cx.stop_propagation();
if *scroll_offset != old_scroll_offset {
cx.notify(current_view);
}

View File

@@ -769,7 +769,7 @@ impl Element for InteractiveText {
.iter()
.any(|range| range.contains(&ix))
{
window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
window.set_cursor_style(crate::CursorStyle::PointingHand, Some(hitbox))
}
}

View File

@@ -50,8 +50,8 @@
/// KeyBinding::new("cmd-k left", pane::SplitLeft, Some("Pane"))
///
use crate::{
Action, ActionRegistry, App, BindingIndex, DispatchPhase, EntityId, FocusId, KeyBinding,
KeyContext, Keymap, Keystroke, ModifiersChangedEvent, Window,
Action, ActionRegistry, App, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap,
Keystroke, ModifiersChangedEvent, Window,
};
use collections::FxHashMap;
use smallvec::SmallVec;
@@ -392,67 +392,22 @@ impl DispatchTree {
/// Returns key bindings that invoke an action on the currently focused element. Bindings are
/// returned in the order they were added. For display, the last binding should take precedence.
///
/// Bindings are only included if they are the highest precedence match for their keystrokes, so
/// shadowed bindings are not included.
pub fn bindings_for_action(
&self,
action: &dyn Action,
context_stack: &[KeyContext],
) -> Vec<KeyBinding> {
// Ideally this would return a `DoubleEndedIterator` to avoid `highest_precedence_*`
// methods, but this can't be done very cleanly since keymap must be borrowed.
let keymap = self.keymap.borrow();
keymap
.bindings_for_action_with_indices(action)
.filter(|(binding_index, binding)| {
Self::binding_matches_predicate_and_not_shadowed(
&keymap,
*binding_index,
&binding.keystrokes,
context_stack,
)
.bindings_for_action(action)
.filter(|binding| {
let (bindings, _) = keymap.bindings_for_input(&binding.keystrokes, context_stack);
bindings.iter().any(|b| b.action.partial_eq(action))
})
.map(|(_, binding)| binding.clone())
.cloned()
.collect()
}
/// Returns the highest precedence binding for the given action and context stack. This is the
/// same as the last result of `bindings_for_action`, but more efficient than getting all bindings.
pub fn highest_precedence_binding_for_action(
&self,
action: &dyn Action,
context_stack: &[KeyContext],
) -> Option<KeyBinding> {
let keymap = self.keymap.borrow();
keymap
.bindings_for_action_with_indices(action)
.rev()
.find_map(|(binding_index, binding)| {
let found = Self::binding_matches_predicate_and_not_shadowed(
&keymap,
binding_index,
&binding.keystrokes,
context_stack,
);
if found { Some(binding.clone()) } else { None }
})
}
fn binding_matches_predicate_and_not_shadowed(
keymap: &Keymap,
binding_index: BindingIndex,
keystrokes: &[Keystroke],
context_stack: &[KeyContext],
) -> bool {
let (bindings, _) = keymap.bindings_for_input_with_indices(&keystrokes, context_stack);
if let Some((highest_precedence_index, _)) = bindings.iter().next() {
binding_index == *highest_precedence_index
} else {
false
}
}
fn bindings_for_input(
&self,
input: &[Keystroke],

View File

@@ -23,10 +23,6 @@ pub struct Keymap {
version: KeymapVersion,
}
/// Index of a binding within a keymap.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct BindingIndex(usize);
impl Keymap {
/// Create a new keymap with the given bindings.
pub fn new(bindings: Vec<KeyBinding>) -> Self {
@@ -67,7 +63,7 @@ impl Keymap {
}
/// Iterate over all bindings, in the order they were added.
pub fn bindings(&self) -> impl DoubleEndedIterator<Item = &KeyBinding> + ExactSizeIterator {
pub fn bindings(&self) -> impl DoubleEndedIterator<Item = &KeyBinding> {
self.bindings.iter()
}
@@ -77,15 +73,6 @@ impl Keymap {
&'a self,
action: &'a dyn Action,
) -> impl 'a + DoubleEndedIterator<Item = &'a KeyBinding> {
self.bindings_for_action_with_indices(action)
.map(|(_, binding)| binding)
}
/// Like `bindings_for_action_with_indices`, but also returns the binding indices.
pub fn bindings_for_action_with_indices<'a>(
&'a self,
action: &'a dyn Action,
) -> impl 'a + DoubleEndedIterator<Item = (BindingIndex, &'a KeyBinding)> {
let action_id = action.type_id();
let binding_indices = self
.binding_indices_by_action_id
@@ -118,7 +105,7 @@ impl Keymap {
}
}
Some((BindingIndex(*ix), binding))
Some(binding)
})
}
@@ -136,7 +123,7 @@ impl Keymap {
/// Returns a list of bindings that match the given input, and a boolean indicating whether or
/// not more bindings might match if the input was longer. Bindings are returned in precedence
/// order (higher precedence first, reverse of the order they were added to the keymap).
/// order.
///
/// Precedence is defined by the depth in the tree (matches on the Editor take precedence over
/// matches on the Pane, then the Workspace, etc.). Bindings with no context are treated as the
@@ -153,36 +140,18 @@ impl Keymap {
input: &[Keystroke],
context_stack: &[KeyContext],
) -> (SmallVec<[KeyBinding; 1]>, bool) {
let (bindings, pending) = self.bindings_for_input_with_indices(input, context_stack);
let bindings = bindings
.into_iter()
.map(|(_, binding)| binding)
.collect::<SmallVec<[KeyBinding; 1]>>();
(bindings, pending)
}
let possibilities = self.bindings().rev().filter_map(|binding| {
binding
.match_keystrokes(input)
.map(|pending| (binding, pending))
});
/// Like `bindings_for_input`, but also returns the binding indices.
pub fn bindings_for_input_with_indices(
&self,
input: &[Keystroke],
context_stack: &[KeyContext],
) -> (SmallVec<[(BindingIndex, KeyBinding); 1]>, bool) {
let possibilities = self
.bindings()
.enumerate()
.rev()
.filter_map(|(ix, binding)| {
binding
.match_keystrokes(input)
.map(|pending| (BindingIndex(ix), binding, pending))
});
let mut bindings: SmallVec<[(BindingIndex, KeyBinding, usize); 1]> = SmallVec::new();
let mut bindings: SmallVec<[(KeyBinding, usize); 1]> = SmallVec::new();
// (pending, is_no_action, depth, keystrokes)
let mut pending_info_opt: Option<(bool, bool, usize, &[Keystroke])> = None;
'outer: for (binding_index, binding, pending) in possibilities {
'outer: for (binding, pending) in possibilities {
for depth in (0..=context_stack.len()).rev() {
if self.binding_enabled(binding, &context_stack[0..depth]) {
let is_no_action = is_no_action(&*binding.action);
@@ -222,21 +191,20 @@ impl Keymap {
}
if !pending {
bindings.push((binding_index, binding.clone(), depth));
bindings.push((binding.clone(), depth));
continue 'outer;
}
}
}
}
// sort by descending depth
bindings.sort_by(|a, b| a.2.cmp(&b.2).reverse());
bindings.sort_by(|a, b| a.1.cmp(&b.1).reverse());
let bindings = bindings
.into_iter()
.map_while(|(binding_index, binding, _)| {
.map_while(|(binding, _)| {
if is_no_action(&*binding.action) {
None
} else {
Some((binding_index, binding))
Some(binding)
}
})
.collect();
@@ -255,6 +223,34 @@ impl Keymap {
true
}
/// WARN: Assumes the bindings are in the order they were added to the keymap
/// returns the last binding for the given bindings, which
/// should be the user's binding in their keymap.json if they've set one,
/// otherwise, the last declared binding for this action in the base keymaps
/// (with Vim mode bindings being considered as declared later if Vim mode
/// is enabled)
///
/// If you are considering changing the behavior of this function
/// (especially to fix a user reported issue) see issues #23621, #24931,
/// and possibly others as evidence that it has swapped back and forth a
/// couple times. The decision as of now is to pick a side and leave it
/// as is, until we have a better way to decide which binding to display
/// that is consistent and not confusing.
pub fn binding_to_display_from_bindings(mut bindings: Vec<KeyBinding>) -> Option<KeyBinding> {
bindings.pop()
}
/// Returns the first binding present in the iterator, which tends to be the
/// default binding without any key context. This is useful for cases where no
/// key context is available on binding display. Otherwise, bindings with a
/// more specific key context would take precedence and result in a
/// potentially invalid keybind being returned.
pub fn default_binding_from_bindings_iterator<'a>(
mut bindings: impl Iterator<Item = &'a KeyBinding>,
) -> Option<&'a KeyBinding> {
bindings.next()
}
}
#[cfg(test)]

View File

@@ -27,7 +27,6 @@ pub struct PathBuilder {
transform: Option<lyon::math::Transform>,
/// PathStyle of the PathBuilder
pub style: PathStyle,
dash_array: Option<Vec<Pixels>>,
}
impl From<lyon::path::Builder> for PathBuilder {
@@ -78,7 +77,6 @@ impl Default for PathBuilder {
raw: lyon::path::Path::builder().with_svg(),
style: PathStyle::Fill(FillOptions::default()),
transform: None,
dash_array: None,
}
}
}
@@ -102,24 +100,6 @@ impl PathBuilder {
Self { style, ..self }
}
/// Sets the dash array of the [`PathBuilder`].
///
/// [MDN](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/stroke-dasharray)
pub fn dash_array(mut self, dash_array: &[Pixels]) -> Self {
// If an odd number of values is provided, then the list of values is repeated to yield an even number of values.
// Thus, 5,3,2 is equivalent to 5,3,2,5,3,2.
let array = if dash_array.len() % 2 == 1 {
let mut new_dash_array = dash_array.to_vec();
new_dash_array.extend_from_slice(dash_array);
new_dash_array
} else {
dash_array.to_vec()
};
self.dash_array = Some(array);
self
}
/// Move the current point to the given point.
#[inline]
pub fn move_to(&mut self, to: Point<Pixels>) {
@@ -249,7 +229,7 @@ impl PathBuilder {
};
match self.style {
PathStyle::Stroke(options) => Self::tessellate_stroke(self.dash_array, &path, &options),
PathStyle::Stroke(options) => Self::tessellate_stroke(&path, &options),
PathStyle::Fill(options) => Self::tessellate_fill(&path, &options),
}
}
@@ -273,37 +253,9 @@ impl PathBuilder {
}
fn tessellate_stroke(
dash_array: Option<Vec<Pixels>>,
path: &lyon::path::Path,
options: &StrokeOptions,
) -> Result<Path<Pixels>, Error> {
let path = if let Some(dash_array) = dash_array {
let measurements = lyon::algorithms::measure::PathMeasurements::from_path(&path, 0.01);
let mut sampler = measurements
.create_sampler(path, lyon::algorithms::measure::SampleType::Normalized);
let mut builder = lyon::path::Path::builder();
let total_length = sampler.length();
let dash_array_len = dash_array.len();
let mut pos = 0.;
let mut dash_index = 0;
while pos < total_length {
let dash_length = dash_array[dash_index % dash_array_len].0;
let next_pos = (pos + dash_length).min(total_length);
if dash_index % 2 == 0 {
let start = pos / total_length;
let end = next_pos / total_length;
sampler.split_range(start..end, &mut builder);
}
pos = next_pos;
dash_index += 1;
}
&builder.build()
} else {
path
};
// Will contain the result of the tessellation.
let mut buf: VertexBuffers<lyon::math::Point, u16> = VertexBuffers::new();
let mut tessellator = StrokeTessellator::new();

View File

@@ -36,7 +36,7 @@ use crate::{
ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput,
Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene,
ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SvgSize, Task, TaskLabel, Window,
WindowControlArea, hash, point, px, size,
hash, point, px, size,
};
use anyhow::Result;
use async_task::Runnable;
@@ -436,7 +436,6 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
fn on_moved(&self, callback: Box<dyn FnMut()>);
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
fn on_close(&self, callback: Box<dyn FnOnce()>);
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
fn draw(&self, scene: &Scene);
@@ -839,7 +838,7 @@ impl PlatformInputHandler {
.ok();
}
pub fn replace_and_mark_text_in_range(
fn replace_and_mark_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,

View File

@@ -31,8 +31,8 @@ use crate::{
AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
ResizeEdge, ScaledPixels, Size, Tiling, WaylandClientStatePtr, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls, WindowDecorations,
WindowParams, px, size,
WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowParams, px,
size,
};
#[derive(Default)]
@@ -978,9 +978,6 @@ impl PlatformWindow for WaylandWindow {
self.0.callbacks.borrow_mut().close = Some(callback);
}
fn on_hit_test_window_control(&self, _callback: Box<dyn FnMut() -> Option<WindowControlArea>>) {
}
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
self.0.callbacks.borrow_mut().appearance_changed = Some(callback);
}

View File

@@ -5,8 +5,8 @@ use crate::{
AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, GpuSpecs, Modifiers,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
Point, PromptButton, PromptLevel, RequestFrameOptions, ResizeEdge, ScaledPixels, Scene, Size,
Tiling, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea,
WindowDecorations, WindowKind, WindowParams, X11ClientStatePtr, px, size,
Tiling, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowDecorations,
WindowKind, WindowParams, X11ClientStatePtr, px, size,
};
use blade_graphics as gpu;
@@ -1322,6 +1322,10 @@ impl PlatformWindow for X11Window {
Ok(())
}
fn set_edited(&mut self, _edited: bool) {
log::info!("ignoring macOS specific set_edited");
}
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
let mut state = self.0.state.borrow_mut();
state.background_appearance = background_appearance;
@@ -1329,6 +1333,10 @@ impl PlatformWindow for X11Window {
state.renderer.update_transparency(transparent);
}
fn show_character_palette(&self) {
log::info!("ignoring macOS specific show_character_palette");
}
fn minimize(&self) {
let state = self.0.state.borrow();
const WINDOW_ICONIC_STATE: u32 = 3;
@@ -1408,9 +1416,6 @@ impl PlatformWindow for X11Window {
self.0.callbacks.borrow_mut().close = Some(callback);
}
fn on_hit_test_window_control(&self, _callback: Box<dyn FnMut() -> Option<WindowControlArea>>) {
}
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
self.0.callbacks.borrow_mut().appearance_changed = Some(callback);
}

View File

@@ -315,9 +315,6 @@ impl MacPlatform {
action,
os_action,
} => {
// Note that this is intentionally using earlier bindings, whereas typically
// later ones take display precedence. See the discussion on
// https://github.com/zed-industries/zed/issues/23621
let keystrokes = keymap
.bindings_for_action(action.as_ref())
.find_or_first(|binding| {

View File

@@ -4,8 +4,8 @@ use crate::{
KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, ScaledPixels, Size,
Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea,
WindowKind, WindowParams, platform::PlatformInputHandler, point, px, size,
Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowKind, WindowParams,
platform::PlatformInputHandler, point, px, size,
};
use block::ConcreteBlock;
use cocoa::{
@@ -1146,9 +1146,6 @@ impl PlatformWindow for MacWindow {
self.0.as_ref().lock().close_callback = Some(callback);
}
fn on_hit_test_window_control(&self, _callback: Box<dyn FnMut() -> Option<WindowControlArea>>) {
}
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
self.0.lock().appearance_changed_callback = Some(callback);
}

View File

@@ -2,7 +2,7 @@ use crate::{
AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DispatchEventResult, GpuSpecs,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
Point, PromptButton, RequestFrameOptions, ScaledPixels, Size, TestPlatform, TileId,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowParams,
};
use collections::HashMap;
use parking_lot::Mutex;
@@ -21,7 +21,6 @@ pub(crate) struct TestWindowState {
platform: Weak<TestPlatform>,
sprite_atlas: Arc<dyn PlatformAtlas>,
pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>,
hit_test_window_control_callback: Option<Box<dyn FnMut() -> Option<WindowControlArea>>>,
input_callback: Option<Box<dyn FnMut(PlatformInput) -> DispatchEventResult>>,
active_status_change_callback: Option<Box<dyn FnMut(bool)>>,
hover_status_change_callback: Option<Box<dyn FnMut(bool)>>,
@@ -66,7 +65,6 @@ impl TestWindow {
title: Default::default(),
edited: false,
should_close_handler: None,
hit_test_window_control_callback: None,
input_callback: None,
active_status_change_callback: None,
hover_status_change_callback: None,
@@ -256,10 +254,6 @@ impl PlatformWindow for TestWindow {
fn on_close(&self, _callback: Box<dyn FnOnce()>) {}
fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>) {
self.0.lock().hit_test_window_control_callback = Some(callback);
}
fn on_appearance_changed(&self, _callback: Box<dyn FnMut()>) {}
fn draw(&self, _scene: &crate::Scene) {}

View File

@@ -1351,7 +1351,7 @@ fn apply_font_features(
}
#[inline]
const fn make_direct_write_feature(feature_name: &str, parameter: u32) -> DWRITE_FONT_FEATURE {
fn make_direct_write_feature(feature_name: &str, parameter: u32) -> DWRITE_FONT_FEATURE {
let tag = make_direct_write_tag(feature_name);
DWRITE_FONT_FEATURE {
nameTag: tag,
@@ -1360,14 +1360,17 @@ const fn make_direct_write_feature(feature_name: &str, parameter: u32) -> DWRITE
}
#[inline]
const fn make_open_type_tag(tag_name: &str) -> u32 {
let bytes = tag_name.as_bytes();
debug_assert!(bytes.len() == 4);
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
fn make_open_type_tag(tag_name: &str) -> u32 {
let bytes = tag_name.bytes().collect_vec();
assert_eq!(bytes.len(), 4);
((bytes[3] as u32) << 24)
| ((bytes[2] as u32) << 16)
| ((bytes[1] as u32) << 8)
| (bytes[0] as u32)
}
#[inline]
const fn make_direct_write_tag(tag_name: &str) -> DWRITE_FONT_FEATURE_TAG {
fn make_direct_write_tag(tag_name: &str) -> DWRITE_FONT_FEATURE_TAG {
DWRITE_FONT_FEATURE_TAG(make_open_type_tag(tag_name))
}

View File

@@ -35,7 +35,7 @@ pub(crate) fn handle_msg(
state_ptr: Rc<WindowsWindowStatePtr>,
) -> LRESULT {
let handled = match msg {
WM_ACTIVATE => handle_activate_msg(wparam, state_ptr),
WM_ACTIVATE => handle_activate_msg(handle, wparam, state_ptr),
WM_CREATE => handle_create_msg(handle, state_ptr),
WM_MOVE => handle_move_msg(handle, lparam, state_ptr),
WM_SIZE => handle_size_msg(wparam, lparam, state_ptr),
@@ -83,11 +83,11 @@ pub(crate) fn handle_msg(
WM_XBUTTONUP => handle_xbutton_msg(handle, wparam, lparam, handle_mouse_up_msg, state_ptr),
WM_MOUSEWHEEL => handle_mouse_wheel_msg(handle, wparam, lparam, state_ptr),
WM_MOUSEHWHEEL => handle_mouse_horizontal_wheel_msg(handle, wparam, lparam, state_ptr),
WM_SYSKEYDOWN => handle_syskeydown_msg(handle, wparam, lparam, state_ptr),
WM_SYSKEYUP => handle_syskeyup_msg(handle, wparam, lparam, state_ptr),
WM_SYSKEYDOWN => handle_syskeydown_msg(wparam, lparam, state_ptr),
WM_SYSKEYUP => handle_syskeyup_msg(wparam, lparam, state_ptr),
WM_SYSCOMMAND => handle_system_command(wparam, state_ptr),
WM_KEYDOWN => handle_keydown_msg(handle, wparam, lparam, state_ptr),
WM_KEYUP => handle_keyup_msg(handle, wparam, lparam, state_ptr),
WM_KEYDOWN => handle_keydown_msg(wparam, lparam, state_ptr),
WM_KEYUP => handle_keyup_msg(wparam, lparam, state_ptr),
WM_CHAR => handle_char_msg(wparam, state_ptr),
WM_DEADCHAR => handle_dead_char_msg(wparam, state_ptr),
WM_IME_STARTCOMPOSITION => handle_ime_position(handle, state_ptr),
@@ -337,13 +337,12 @@ fn handle_mouse_leave_msg(state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize>
}
fn handle_syskeydown_msg(
handle: HWND,
wparam: WPARAM,
lparam: LPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
let mut lock = state_ptr.state.borrow_mut();
let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
let input = handle_key_event(wparam, lparam, &mut lock, |keystroke| {
PlatformInput::KeyDown(KeyDownEvent {
keystroke,
is_held: lparam.0 & (0x1 << 30) > 0,
@@ -359,6 +358,7 @@ fn handle_syskeydown_msg(
if handled {
lock.system_key_handled = true;
lock.suppress_next_char_msg = true;
Some(0)
} else {
// we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}`
@@ -368,13 +368,12 @@ fn handle_syskeydown_msg(
}
fn handle_syskeyup_msg(
handle: HWND,
wparam: WPARAM,
lparam: LPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
let mut lock = state_ptr.state.borrow_mut();
let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
let input = handle_key_event(wparam, lparam, &mut lock, |keystroke| {
PlatformInput::KeyUp(KeyUpEvent { keystroke })
})?;
let mut func = lock.callbacks.input.take()?;
@@ -389,13 +388,12 @@ fn handle_syskeyup_msg(
// It's a known bug that you can't trigger `ctrl-shift-0`. See:
// https://superuser.com/questions/1455762/ctrl-shift-number-key-combination-has-stopped-working-for-a-few-numbers
fn handle_keydown_msg(
handle: HWND,
wparam: WPARAM,
lparam: LPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
let mut lock = state_ptr.state.borrow_mut();
let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
let Some(input) = handle_key_event(wparam, lparam, &mut lock, |keystroke| {
PlatformInput::KeyDown(KeyDownEvent {
keystroke,
is_held: lparam.0 & (0x1 << 30) > 0,
@@ -403,42 +401,32 @@ fn handle_keydown_msg(
}) else {
return Some(1);
};
drop(lock);
let is_composing = with_input_handler(&state_ptr, |input_handler| {
input_handler.marked_text_range()
})
.flatten()
.is_some();
if is_composing {
translate_message(handle, wparam, lparam);
return Some(0);
}
let Some(mut func) = state_ptr.state.borrow_mut().callbacks.input.take() else {
let Some(mut func) = lock.callbacks.input.take() else {
return Some(1);
};
drop(lock);
let handled = !func(input).propagate;
state_ptr.state.borrow_mut().callbacks.input = Some(func);
let mut lock = state_ptr.state.borrow_mut();
lock.callbacks.input = Some(func);
if handled {
lock.suppress_next_char_msg = true;
Some(0)
} else {
translate_message(handle, wparam, lparam);
Some(1)
}
}
fn handle_keyup_msg(
handle: HWND,
wparam: WPARAM,
lparam: LPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
let mut lock = state_ptr.state.borrow_mut();
let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
let Some(input) = handle_key_event(wparam, lparam, &mut lock, |keystroke| {
PlatformInput::KeyUp(KeyUpEvent { keystroke })
}) else {
return Some(1);
@@ -691,36 +679,42 @@ fn handle_ime_composition_inner(
lparam: LPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
let lparam = lparam.0 as u32;
if lparam == 0 {
if lparam.0 == 0 {
// Japanese IME may send this message with lparam = 0, which indicates that
// there is no composition string.
with_input_handler(&state_ptr, |input_handler| {
input_handler.replace_text_in_range(None, "");
})?;
Some(0)
} else {
if lparam & GCS_COMPSTR.0 > 0 {
let comp_string = parse_ime_composition_string(ctx, GCS_COMPSTR)?;
let caret_pos = (lparam & GCS_CURSORPOS.0 > 0).then(|| {
let pos = retrieve_composition_cursor_position(ctx);
pos..pos
});
with_input_handler(&state_ptr, |input_handler| {
input_handler.replace_and_mark_text_in_range(None, &comp_string, caret_pos);
})?;
}
if lparam & GCS_RESULTSTR.0 > 0 {
let comp_result = parse_ime_composition_string(ctx, GCS_RESULTSTR)?;
with_input_handler(&state_ptr, |input_handler| {
input_handler.replace_text_in_range(None, &comp_result);
})?;
return Some(0);
}
// currently, we don't care other stuff
None
return Some(0);
}
let mut ime_input = None;
if lparam.0 as u32 & GCS_COMPSTR.0 > 0 {
let comp_string = parse_ime_compostion_string(ctx)?;
with_input_handler(&state_ptr, |input_handler| {
input_handler.replace_and_mark_text_in_range(None, &comp_string, None);
})?;
ime_input = Some(comp_string);
}
if lparam.0 as u32 & GCS_CURSORPOS.0 > 0 {
let comp_string = &ime_input?;
let caret_pos = retrieve_composition_cursor_position(ctx);
with_input_handler(&state_ptr, |input_handler| {
input_handler.replace_and_mark_text_in_range(
None,
comp_string,
Some(caret_pos..caret_pos),
);
})?;
}
if lparam.0 as u32 & GCS_RESULTSTR.0 > 0 {
let comp_result = parse_ime_compostion_result(ctx)?;
with_input_handler(&state_ptr, |input_handler| {
input_handler.replace_text_in_range(None, &comp_result);
})?;
return Some(0);
}
// currently, we don't care other stuff
None
}
/// SEE: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize
@@ -778,8 +772,21 @@ fn handle_calc_client_size(
Some(0)
}
fn handle_activate_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
fn handle_activate_msg(
handle: HWND,
wparam: WPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
let activated = wparam.loword() > 0;
if state_ptr.hide_title_bar {
if let Some(titlebar_rect) = state_ptr.state.borrow().get_titlebar_rect().log_err() {
unsafe {
InvalidateRect(Some(handle), Some(&titlebar_rect), false)
.ok()
.log_err()
};
}
}
let this = state_ptr.clone();
state_ptr
.executor
@@ -887,6 +894,9 @@ fn handle_hit_test_msg(
if !state_ptr.is_movable {
return None;
}
if !state_ptr.hide_title_bar {
return None;
}
// default handler for resize areas
let hit = unsafe { DefWindowProcW(handle, msg, wparam, lparam) };
@@ -922,22 +932,20 @@ fn handle_hit_test_msg(
return Some(HTTOP as _);
}
let mut lock = state_ptr.state.borrow_mut();
if let Some(mut callback) = lock.callbacks.hit_test_window_control.take() {
drop(lock);
let area = callback();
state_ptr
.state
.borrow_mut()
.callbacks
.hit_test_window_control = Some(callback);
if let Some(area) = area {
return match area {
WindowControlArea::Drag => Some(HTCAPTION as _),
WindowControlArea::Close => Some(HTCLOSE as _),
WindowControlArea::Max => Some(HTMAXBUTTON as _),
WindowControlArea::Min => Some(HTMINBUTTON as _),
};
let titlebar_rect = state_ptr.state.borrow().get_titlebar_rect();
if let Ok(titlebar_rect) = titlebar_rect {
if cursor_point.y < titlebar_rect.bottom {
let caption_btn_width = (state_ptr.state.borrow().caption_button_width().0
* state_ptr.state.borrow().scale_factor) as i32;
if cursor_point.x >= titlebar_rect.right - caption_btn_width {
return Some(HTCLOSE as _);
} else if cursor_point.x >= titlebar_rect.right - caption_btn_width * 2 {
return Some(HTMAXBUTTON as _);
} else if cursor_point.x >= titlebar_rect.right - caption_btn_width * 3 {
return Some(HTMINBUTTON as _);
}
return Some(HTCAPTION as _);
}
}
@@ -949,6 +957,10 @@ fn handle_nc_mouse_move_msg(
lparam: LPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
if !state_ptr.hide_title_bar {
return None;
}
start_tracking_mouse(handle, &state_ptr, TME_LEAVE | TME_NONCLIENT);
let mut lock = state_ptr.state.borrow_mut();
@@ -979,6 +991,10 @@ fn handle_nc_mouse_down_msg(
lparam: LPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
if !state_ptr.hide_title_bar {
return None;
}
let mut lock = state_ptr.state.borrow_mut();
if let Some(mut func) = lock.callbacks.input.take() {
let scale_factor = lock.scale_factor;
@@ -1030,6 +1046,10 @@ fn handle_nc_mouse_up_msg(
lparam: LPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
if !state_ptr.hide_title_bar {
return None;
}
let mut lock = state_ptr.state.borrow_mut();
if let Some(mut func) = lock.callbacks.input.take() {
let scale_factor = lock.scale_factor;
@@ -1193,23 +1213,7 @@ fn handle_input_language_changed(
Some(0)
}
#[inline]
fn translate_message(handle: HWND, wparam: WPARAM, lparam: LPARAM) {
let msg = MSG {
hwnd: handle,
message: WM_KEYDOWN,
wParam: wparam,
lParam: lparam,
// It seems like leaving the following two parameters empty doesn't break key events, they still work as expected.
// But if any bugs pop up after this PR, this is probably the place to look first.
time: 0,
pt: POINT::default(),
};
unsafe { TranslateMessage(&msg).ok().log_err() };
}
fn handle_key_event<F>(
handle: HWND,
wparam: WPARAM,
lparam: LPARAM,
state: &mut WindowsWindowState,
@@ -1218,10 +1222,15 @@ fn handle_key_event<F>(
where
F: FnOnce(Keystroke) -> PlatformInput,
{
state.suppress_next_char_msg = false;
let virtual_key = VIRTUAL_KEY(wparam.loword());
let mut modifiers = current_modifiers();
match virtual_key {
VK_PROCESSKEY => {
// IME composition
None
}
VK_SHIFT | VK_CONTROL | VK_MENU | VK_LWIN | VK_RWIN => {
if state
.last_reported_modifiers
@@ -1235,11 +1244,6 @@ where
}))
}
vkey => {
let vkey = if vkey == VK_PROCESSKEY {
VIRTUAL_KEY(unsafe { ImmGetVirtualKey(handle) } as u16)
} else {
vkey
};
let keystroke = parse_normal_key(vkey, lparam, modifiers)?;
Some(f(keystroke))
}
@@ -1321,14 +1325,14 @@ fn parse_normal_key(
})
}
fn parse_ime_composition_string(ctx: HIMC, comp_type: IME_COMPOSITION_STRING) -> Option<String> {
fn parse_ime_compostion_string(ctx: HIMC) -> Option<String> {
unsafe {
let string_len = ImmGetCompositionStringW(ctx, comp_type, None, 0);
let string_len = ImmGetCompositionStringW(ctx, GCS_COMPSTR, None, 0);
if string_len >= 0 {
let mut buffer = vec![0u8; string_len as usize + 2];
ImmGetCompositionStringW(
ctx,
comp_type,
GCS_COMPSTR,
Some(buffer.as_mut_ptr() as _),
string_len as _,
);
@@ -1348,6 +1352,29 @@ fn retrieve_composition_cursor_position(ctx: HIMC) -> usize {
unsafe { ImmGetCompositionStringW(ctx, GCS_CURSORPOS, None, 0) as usize }
}
fn parse_ime_compostion_result(ctx: HIMC) -> Option<String> {
unsafe {
let string_len = ImmGetCompositionStringW(ctx, GCS_RESULTSTR, None, 0);
if string_len >= 0 {
let mut buffer = vec![0u8; string_len as usize + 2];
ImmGetCompositionStringW(
ctx,
GCS_RESULTSTR,
Some(buffer.as_mut_ptr() as _),
string_len as _,
);
let wstring = std::slice::from_raw_parts::<u16>(
buffer.as_mut_ptr().cast::<u16>(),
string_len as usize / 2,
);
let string = String::from_utf16_lossy(wstring);
Some(string)
} else {
None
}
}
}
#[inline]
fn is_virtual_key_pressed(vkey: VIRTUAL_KEY) -> bool {
unsafe { GetKeyState(vkey.0 as i32) < 0 }
@@ -1464,7 +1491,12 @@ fn with_input_handler<F, R>(state_ptr: &Rc<WindowsWindowStatePtr>, f: F) -> Opti
where
F: FnOnce(&mut PlatformInputHandler) -> R,
{
let mut input_handler = state_ptr.state.borrow_mut().input_handler.take()?;
let mut lock = state_ptr.state.borrow_mut();
if lock.suppress_next_char_msg {
return None;
}
let mut input_handler = lock.input_handler.take()?;
drop(lock);
let result = f(&mut input_handler);
state_ptr.state.borrow_mut().input_handler = Some(input_handler);
Some(result)
@@ -1478,6 +1510,9 @@ where
F: FnOnce(&mut PlatformInputHandler, f32) -> Option<R>,
{
let mut lock = state_ptr.state.borrow_mut();
if lock.suppress_next_char_msg {
return None;
}
let mut input_handler = lock.input_handler.take()?;
let scale_factor = lock.scale_factor;
drop(lock);

View File

@@ -231,6 +231,9 @@ impl WindowsPlatform {
}
}
_ => {
// todo(windows)
// crate `windows 0.56` reports true as Err
TranslateMessage(&msg).as_bool();
DispatchMessageW(&msg);
}
}
@@ -780,7 +783,7 @@ fn file_open_dialog(options: PathPromptOptions) -> Result<Option<Vec<PathBuf>>>
return Ok(None);
}
let mut paths = Vec::with_capacity(file_count as usize);
let mut paths = Vec::new();
for i in 0..file_count {
let item = unsafe { results.GetItemAt(i)? };
let path = unsafe { item.GetDisplayName(SIGDN_FILESYSPATH)?.to_string()? };

View File

@@ -43,6 +43,7 @@ pub struct WindowsWindowState {
pub callbacks: Callbacks,
pub input_handler: Option<PlatformInputHandler>,
pub last_reported_modifiers: Option<Modifiers>,
pub suppress_next_char_msg: bool,
pub system_key_handled: bool,
pub hovered: bool,
@@ -102,6 +103,7 @@ impl WindowsWindowState {
let callbacks = Callbacks::default();
let input_handler = None;
let last_reported_modifiers = None;
let suppress_next_char_msg = false;
let system_key_handled = false;
let hovered = false;
let click_state = ClickState::new();
@@ -121,6 +123,7 @@ impl WindowsWindowState {
callbacks,
input_handler,
last_reported_modifiers,
suppress_next_char_msg,
system_key_handled,
hovered,
renderer,
@@ -190,6 +193,40 @@ impl WindowsWindowState {
fn content_size(&self) -> Size<Pixels> {
self.logical_size
}
fn title_bar_padding(&self) -> Pixels {
// using USER_DEFAULT_SCREEN_DPI because GPUI handles the scale with the scale factor
let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, USER_DEFAULT_SCREEN_DPI) };
px(padding as f32)
}
fn title_bar_top_offset(&self) -> Pixels {
if self.is_maximized() {
self.title_bar_padding() * 2
} else {
px(0.)
}
}
fn title_bar_height(&self) -> Pixels {
// todo(windows) this is hardcoded to match the ui title bar
// in the future the ui title bar component will report the size
px(32.) + self.title_bar_top_offset()
}
pub(crate) fn caption_button_width(&self) -> Pixels {
// todo(windows) this is hardcoded to match the ui title bar
// in the future the ui title bar component will report the size
px(36.)
}
pub(crate) fn get_titlebar_rect(&self) -> anyhow::Result<RECT> {
let height = self.title_bar_height();
let mut rect = RECT::default();
unsafe { GetClientRect(self.hwnd, &mut rect) }?;
rect.bottom = rect.top + ((height.0 * self.scale_factor).round() as i32);
Ok(rect)
}
}
impl WindowsWindowStatePtr {
@@ -313,7 +350,6 @@ pub(crate) struct Callbacks {
pub(crate) moved: Option<Box<dyn FnMut()>>,
pub(crate) should_close: Option<Box<dyn FnMut() -> bool>>,
pub(crate) close: Option<Box<dyn FnOnce()>>,
pub(crate) hit_test_window_control: Option<Box<dyn FnMut() -> Option<WindowControlArea>>>,
pub(crate) appearance_changed: Option<Box<dyn FnMut()>>,
}
@@ -763,10 +799,6 @@ impl PlatformWindow for WindowsWindow {
self.0.state.borrow_mut().callbacks.close = Some(callback);
}
fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>) {
self.0.state.borrow_mut().callbacks.hit_test_window_control = Some(callback);
}
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
self.0.state.borrow_mut().callbacks.appearance_changed = Some(callback);
}

Some files were not shown because too many files have changed in this diff Show More