Compare commits
1 Commits
arm_github
...
github-tok
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6a5f55a05 |
6
.github/actions/build_docs/action.yml
vendored
6
.github/actions/build_docs/action.yml
vendored
@@ -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: |
|
||||
|
||||
64
.github/workflows/ci.yml
vendored
64
.github/workflows/ci.yml
vendored
@@ -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 }}
|
||||
|
||||
44
.github/workflows/release_nightly.yml
vendored
44
.github/workflows/release_nightly.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/unit_evals.yml
vendored
2
.github/workflows/unit_evals.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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
44
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == insert && !menu",
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
// "j k": "vim::SwitchToNormalMode"
|
||||
// "j k": ["workspace::SendKeystrokes", "escape"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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", "..."]
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -1144,10 +1144,6 @@ impl ActiveThread {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
ThreadEvent::ProfileChanged => {
|
||||
self.save_thread(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1378,8 +1378,7 @@ impl AgentDiff {
|
||||
| ThreadEvent::CheckpointChanged
|
||||
| ThreadEvent::ToolConfirmationNeeded
|
||||
| ThreadEvent::ToolUseLimitReached
|
||||
| ThreadEvent::CancelEditing
|
||||
| ThreadEvent::ProfileChanged => {}
|
||||
| ThreadEvent::CancelEditing => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -11,9 +11,6 @@ workspace = true
|
||||
[lib]
|
||||
path = "src/context_server.rs"
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
|
||||
@@ -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)
|
||||
})?
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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}")))
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -294,7 +294,6 @@ impl ExampleContext {
|
||||
| ThreadEvent::MessageDeleted(_)
|
||||
| ThreadEvent::SummaryChanged
|
||||
| ThreadEvent::SummaryGenerated
|
||||
| ThreadEvent::ProfileChanged
|
||||
| ThreadEvent::ReceivedTextChunk
|
||||
| ThreadEvent::StreamedToolUse { .. }
|
||||
| ThreadEvent::CheckpointChanged
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})?;
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -46,11 +46,9 @@ actions!(
|
||||
TrashUntrackedFiles,
|
||||
Uncommit,
|
||||
Push,
|
||||
PushTo,
|
||||
ForcePush,
|
||||
Pull,
|
||||
Fetch,
|
||||
FetchFrom,
|
||||
Commit,
|
||||
Amend,
|
||||
Cancel,
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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())
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -61,7 +61,7 @@ impl Render for WindowShadow {
|
||||
CursorStyle::ResizeUpRightDownLeft
|
||||
}
|
||||
},
|
||||
&hitbox,
|
||||
Some(&hitbox),
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()? };
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user