Compare commits
98 Commits
v0.193.1-p
...
xcode-styl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06f4f46504 | ||
|
|
e5bcd720e1 | ||
|
|
41583fb066 | ||
|
|
521a223681 | ||
|
|
c8c6468f9c | ||
|
|
3f4098e87b | ||
|
|
1d684c8890 | ||
|
|
97c5c5a6e7 | ||
|
|
ba4fc1bcfc | ||
|
|
bbf16bda75 | ||
|
|
c56b8904cc | ||
|
|
695118d110 | ||
|
|
a675ca7a1e | ||
|
|
6e762d9c05 | ||
|
|
28380d714d | ||
|
|
f338c46bf7 | ||
|
|
5fbb7b0d40 | ||
|
|
f12b0dddf4 | ||
|
|
14bb10d783 | ||
|
|
c9ce4aec91 | ||
|
|
01dfb6fa82 | ||
|
|
f9987a1141 | ||
|
|
7432e947bc | ||
|
|
157199b65b | ||
|
|
d74f3f4ea6 | ||
|
|
9e2023bffc | ||
|
|
3ab4ad6de8 | ||
|
|
e3ce0618a3 | ||
|
|
865dd4c5fc | ||
|
|
2178f66af6 | ||
|
|
338a7395a7 | ||
|
|
4c2415b338 | ||
|
|
e6bc1308af | ||
|
|
6c46e1129d | ||
|
|
fbb5628ec6 | ||
|
|
8c9116daa5 | ||
|
|
20a3e613b8 | ||
|
|
ba1c05abf2 | ||
|
|
2823771c06 | ||
|
|
343f155ab9 | ||
|
|
2dece13d83 | ||
|
|
985dcf7523 | ||
|
|
b079871428 | ||
|
|
4983b01c89 | ||
|
|
35863c4302 | ||
|
|
a0bd25f218 | ||
|
|
8a1e795746 | ||
|
|
f4818b648e | ||
|
|
7031ed8b87 | ||
|
|
6073d2c93c | ||
|
|
00499aadd4 | ||
|
|
d1eb69c6cd | ||
|
|
40cbfb7eb2 | ||
|
|
5d0f02d356 | ||
|
|
ca8e213151 | ||
|
|
90c893747c | ||
|
|
d09c7eb317 | ||
|
|
1753432406 | ||
|
|
d9218b10ea | ||
|
|
dfdeb1bf51 | ||
|
|
b9f81c7ce7 | ||
|
|
b1450b6d71 | ||
|
|
1af9f98c1d | ||
|
|
1330cb7a1f | ||
|
|
dae4e84bc5 | ||
|
|
6fb5500ef2 | ||
|
|
8f9817173d | ||
|
|
aae4778b4e | ||
|
|
e5c812fbcb | ||
|
|
294147f473 | ||
|
|
4516b099e7 | ||
|
|
8e831ced5b | ||
|
|
3740eec5bf | ||
|
|
2a5a1814cd | ||
|
|
cc62125244 | ||
|
|
224de2ec6c | ||
|
|
c0acd8e8b1 | ||
|
|
91c9281cea | ||
|
|
84494ab26b | ||
|
|
93d670af13 | ||
|
|
7d087ea5d2 | ||
|
|
19c9fb3118 | ||
|
|
b0bab0bf9a | ||
|
|
630a326a07 | ||
|
|
6848073c38 | ||
|
|
fb877ddf4c | ||
|
|
5135326294 | ||
|
|
4acdb447cf | ||
|
|
1e91d68e08 | ||
|
|
4276901e28 | ||
|
|
44152c412f | ||
|
|
c19ff51465 | ||
|
|
f41747b422 | ||
|
|
f8d20986a1 | ||
|
|
82fa6d7e53 | ||
|
|
d5392cf53f | ||
|
|
a07a090b5a | ||
|
|
046dbba964 |
14
.github/workflows/ci.yml
vendored
@@ -29,6 +29,7 @@ jobs:
|
||||
outputs:
|
||||
run_tests: ${{ steps.filter.outputs.run_tests }}
|
||||
run_license: ${{ steps.filter.outputs.run_license }}
|
||||
run_docs: ${{ steps.filter.outputs.run_docs }}
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
@@ -58,6 +59,11 @@ jobs:
|
||||
else
|
||||
echo "run_tests=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep '^docs/') ]]; then
|
||||
echo "run_docs=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "run_docs=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep '^Cargo.lock') ]]; then
|
||||
echo "run_license=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
@@ -198,7 +204,9 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
name: Check docs
|
||||
needs: [job_spec]
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
(needs.job_spec.outputs.run_tests == 'true' || needs.job_spec.outputs.run_docs == 'true')
|
||||
runs-on:
|
||||
- buildjet-8vcpu-ubuntu-2204
|
||||
steps:
|
||||
@@ -452,8 +460,10 @@ jobs:
|
||||
RET_CODE=0
|
||||
# Always check style
|
||||
[[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }
|
||||
[[ "${{ needs.check_docs.result }}" != 'success' ]] && { RET_CODE=1; echo "docs checks failed"; }
|
||||
|
||||
if [[ "${{ needs.job_spec.outputs.run_docs }}" == "true" ]]; then
|
||||
[[ "${{ needs.check_docs.result }}" != 'success' ]] && { RET_CODE=1; echo "docs checks failed"; }
|
||||
fi
|
||||
# Only check test jobs if they were supposed to run
|
||||
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
|
||||
[[ "${{ needs.workspace_hack.result }}" != 'success' ]] && { RET_CODE=1; echo "Workspace Hack failed"; }
|
||||
|
||||
34
.github/workflows/community_delete_comments.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Delete Mediafire Comments
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
delete_comment:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for specific strings in comment
|
||||
id: check_comment
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
script: |
|
||||
const comment = context.payload.comment.body;
|
||||
const triggerStrings = ['www.mediafire.com'];
|
||||
return triggerStrings.some(triggerString => comment.includes(triggerString));
|
||||
|
||||
- name: Delete comment if it contains any of the specific strings
|
||||
if: steps.check_comment.outputs.result == 'true'
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
script: |
|
||||
const commentId = context.payload.comment.id;
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: commentId
|
||||
});
|
||||
12
.github/workflows/eval.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [synchronize, reopened, labeled]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -25,16 +25,6 @@ env:
|
||||
ZED_EVAL_TELEMETRY: 1
|
||||
|
||||
jobs:
|
||||
# This is a no-op job that we run to prevent GitHub from marking the workflow
|
||||
# as failed for PRs that don't have the `run-eval` label.
|
||||
noop:
|
||||
name: No-op
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- name: No-op
|
||||
run: echo "Nothing to do"
|
||||
|
||||
run_eval:
|
||||
timeout-minutes: 60
|
||||
name: Run Agent Eval
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
},
|
||||
"file_types": {
|
||||
"Dockerfile": ["Dockerfile*[!dockerignore]"],
|
||||
"JSONC": ["assets/**/*.json", "renovate.json"],
|
||||
"Git Ignore": ["dockerignore"]
|
||||
},
|
||||
"hard_tabs": false,
|
||||
|
||||
52
Cargo.lock
generated
@@ -14,6 +14,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"language",
|
||||
"project",
|
||||
"proto",
|
||||
"release_channel",
|
||||
"smallvec",
|
||||
"ui",
|
||||
@@ -77,6 +78,7 @@ dependencies = [
|
||||
"language",
|
||||
"language_model",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
@@ -109,18 +111,11 @@ dependencies = [
|
||||
name = "agent_settings"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anthropic",
|
||||
"anyhow",
|
||||
"collections",
|
||||
"deepseek",
|
||||
"fs",
|
||||
"gpui",
|
||||
"language_model",
|
||||
"lmstudio",
|
||||
"log",
|
||||
"mistral",
|
||||
"ollama",
|
||||
"open_ai",
|
||||
"paths",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -4161,6 +4156,7 @@ dependencies = [
|
||||
"paths",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"task",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
@@ -4314,14 +4310,18 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"alacritty_terminal",
|
||||
"anyhow",
|
||||
"bitflags 2.9.0",
|
||||
"client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
"dap",
|
||||
"dap_adapters",
|
||||
"db",
|
||||
"debugger_tools",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"feature_flags",
|
||||
"file_icons",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
@@ -9020,11 +9020,13 @@ dependencies = [
|
||||
"collections",
|
||||
"copilot",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"lsp",
|
||||
"picker",
|
||||
"project",
|
||||
"release_channel",
|
||||
"serde_json",
|
||||
@@ -9242,7 +9244,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libwebrtc"
|
||||
version = "0.3.10"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
|
||||
dependencies = [
|
||||
"cxx",
|
||||
"jni",
|
||||
@@ -9322,7 +9324,7 @@ checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
|
||||
[[package]]
|
||||
name = "livekit"
|
||||
version = "0.7.8"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"futures-util",
|
||||
@@ -9345,7 +9347,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "livekit-api"
|
||||
version = "0.4.2"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
@@ -9369,7 +9371,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "livekit-protocol"
|
||||
version = "0.3.9"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"livekit-runtime",
|
||||
@@ -9386,7 +9388,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "livekit-runtime"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
|
||||
dependencies = [
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
@@ -13168,6 +13170,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"urlencoding",
|
||||
"util",
|
||||
"which 6.0.3",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -14555,12 +14558,12 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"smallvec",
|
||||
"streaming-iterator",
|
||||
"tree-sitter",
|
||||
"tree-sitter-json",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -15527,6 +15530,18 @@ version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
|
||||
|
||||
[[package]]
|
||||
name = "svg_preview"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor",
|
||||
"file_icons",
|
||||
"gpui",
|
||||
"ui",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "svgtypes"
|
||||
version = "0.15.3"
|
||||
@@ -16038,7 +16053,6 @@ dependencies = [
|
||||
"indexmap",
|
||||
"log",
|
||||
"palette",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
@@ -16869,8 +16883,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter-python"
|
||||
version = "0.23.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d065aaa27f3aaceaf60c1f0e0ac09e1cb9eb8ed28e7bcdaa52129cffc7f4b04"
|
||||
source = "git+https://github.com/zed-industries/tree-sitter-python?rev=218fcbf3fda3d029225f3dec005cb497d111b35e#218fcbf3fda3d029225f3dec005cb497d111b35e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
@@ -18286,7 +18299,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "webrtc-sys"
|
||||
version = "0.3.7"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cxx",
|
||||
@@ -18299,7 +18312,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "webrtc-sys-build"
|
||||
version = "0.3.6"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
|
||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
|
||||
dependencies = [
|
||||
"fs2",
|
||||
"regex",
|
||||
@@ -19919,7 +19932,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.193.1"
|
||||
version = "0.194.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
@@ -20024,6 +20037,7 @@ dependencies = [
|
||||
"snippet_provider",
|
||||
"snippets_ui",
|
||||
"supermaven",
|
||||
"svg_preview",
|
||||
"sysinfo",
|
||||
"tab_switcher",
|
||||
"task",
|
||||
|
||||
@@ -95,6 +95,7 @@ members = [
|
||||
"crates/markdown_preview",
|
||||
"crates/media",
|
||||
"crates/menu",
|
||||
"crates/svg_preview",
|
||||
"crates/migrator",
|
||||
"crates/mistral",
|
||||
"crates/multi_buffer",
|
||||
@@ -304,6 +305,7 @@ lmstudio = { path = "crates/lmstudio" }
|
||||
lsp = { path = "crates/lsp" }
|
||||
markdown = { path = "crates/markdown" }
|
||||
markdown_preview = { path = "crates/markdown_preview" }
|
||||
svg_preview = { path = "crates/svg_preview" }
|
||||
media = { path = "crates/media" }
|
||||
menu = { path = "crates/menu" }
|
||||
migrator = { path = "crates/migrator" }
|
||||
@@ -595,7 +597,7 @@ tree-sitter-html = "0.23"
|
||||
tree-sitter-jsdoc = "0.23"
|
||||
tree-sitter-json = "0.24"
|
||||
tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" }
|
||||
tree-sitter-python = "0.23"
|
||||
tree-sitter-python = { git = "https://github.com/zed-industries/tree-sitter-python", rev = "218fcbf3fda3d029225f3dec005cb497d111b35e" }
|
||||
tree-sitter-regex = "0.24"
|
||||
tree-sitter-ruby = "0.23"
|
||||
tree-sitter-rust = "0.24"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.87-bookworm as builder
|
||||
FROM rust:1.88-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
1
assets/icons/arrow_down10.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-down10-icon lucide-arrow-down-1-0"><path d="m3 16 4 4 4-4"/><path d="M7 20V4"/><path d="M17 10V4h-2"/><path d="M15 10h4"/><rect x="15" y="14" width="4" height="6" ry="2"/></svg>
|
||||
|
After Width: | Height: | Size: 386 B |
3
assets/icons/bolt_filled_alt.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.75776 5.50003H8.49988C8.70769 5.50003 8.89518 5.62971 8.95455 5.82346C9.04049 6.01876 8.9858 6.23906 8.82956 6.37656L4.82971 9.87643C4.65315 10.0295 4.39488 10.042 4.20614 9.90455C4.01724 9.76705 3.94849 9.51706 4.04052 9.30301L5.24219 6.49999H3.48601C3.2918 6.49999 3.10524 6.37031 3.03197 6.17657C2.9587 5.98126 3.014 5.76096 3.1708 5.62346L7.17018 2.12375C7.34674 1.97001 7.60454 1.95829 7.7936 2.09547C7.98265 2.23275 8.0514 2.48218 7.95922 2.69695L6.75776 5.50003Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 601 B |
12
assets/icons/lsp_debug.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 3L7 4" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 4L10 3" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.002 6V5.51658C5.98992 5.32067 6.03266 5.12502 6.12762 4.94143C6.22259 4.75784 6.36781 4.59012 6.55453 4.44839C6.74125 4.30666 6.9656 4.19386 7.21403 4.1168C7.46246 4.03973 7.72983 4 8 4C8.27017 4 8.53754 4.03973 8.78597 4.1168C9.0344 4.19386 9.25875 4.30666 9.44547 4.44839C9.63219 4.59012 9.77741 4.75784 9.87238 4.94143C9.96734 5.12502 10.0101 5.32067 9.998 5.51658V6" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 13C6.35 13 5 11.5462 5 9.76923V8.15385C5 7.58261 5.21071 7.03477 5.58579 6.63085C5.96086 6.22692 6.46957 6 7 6H9C9.53043 6 10.0391 6.22692 10.4142 6.63085C10.7893 7.03477 11 7.58261 11 8.15385V9.76923C11 11.5462 9.65 13 8 13Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 6.16663C3.90652 6.06663 3 5.21663 3 4.16663" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 9H3" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 13C3 11.95 3.89474 11.05 5 11" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13 4C13 5.05 12.0857 5.9 11 6" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13 9H11" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 11C12.1053 11.05 13 11.95 13 13" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
4
assets/icons/lsp_restart.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.84265 10.7778C4.39206 11.6001 5.17295 12.241 6.08658 12.6194C7.00021 12.9978 8.00555 13.0969 8.97545 12.9039C9.94535 12.711 10.8363 12.2348 11.5355 11.5355C12.2348 10.8363 12.711 9.94535 12.9039 8.97545C13.0969 8.00555 12.9978 7.00021 12.6194 6.08658C12.241 5.17295 11.6001 4.39206 10.7778 3.84265C9.9556 3.29324 8.9889 3 8 3C6.60219 3.00526 5.26054 3.55068 4.25556 4.52222L3 5.77778" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 3V6H6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 685 B |
4
assets/icons/lsp_stop.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 5L11 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 409 B |
1
assets/icons/scroll_text.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scroll-text-icon lucide-scroll-text"><path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/></svg>
|
||||
|
After Width: | Height: | Size: 441 B |
1
assets/icons/split_alt.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-split-icon lucide-split"><path d="M16 3h5v5"/><path d="M8 3H3v5"/><path d="M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3"/><path d="m15 9 6-6"/></svg>
|
||||
|
After Width: | Height: | Size: 345 B |
@@ -41,7 +41,8 @@
|
||||
"shift-f11": "debugger::StepOut",
|
||||
"f11": "zed::ToggleFullScreen",
|
||||
"ctrl-alt-z": "edit_prediction::RateCompletions",
|
||||
"ctrl-shift-i": "edit_prediction::ToggleMenu"
|
||||
"ctrl-shift-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-alt-l": "lsp_tool::ToggleMenu"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -243,8 +244,8 @@
|
||||
"ctrl-alt-e": "agent::RemoveAllContext",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-enter": "agent::ContinueThread",
|
||||
"alt-enter": "agent::ContinueWithBurnMode",
|
||||
"ctrl-alt-b": "agent::ToggleBurnMode"
|
||||
"super-ctrl-b": "agent::ToggleBurnMode",
|
||||
"alt-enter": "agent::ContinueWithBurnMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -490,13 +491,27 @@
|
||||
"ctrl-k r": "editor::RevealInFileManager",
|
||||
"ctrl-k p": "editor::CopyPath",
|
||||
"ctrl-\\": "pane::SplitRight",
|
||||
"ctrl-k v": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
|
||||
"alt-.": "editor::GoToHunk",
|
||||
"alt-,": "editor::GoToPreviousHunk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && extension == md",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-k v": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-shift-v": "markdown::OpenPreview"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && extension == svg",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-k v": "svg::OpenPreviewToTheSide",
|
||||
"ctrl-shift-v": "svg::OpenPreview"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
@@ -904,7 +919,9 @@
|
||||
"context": "BreakpointList",
|
||||
"bindings": {
|
||||
"space": "debugger::ToggleEnableBreakpoint",
|
||||
"backspace": "debugger::UnsetBreakpoint"
|
||||
"backspace": "debugger::UnsetBreakpoint",
|
||||
"left": "debugger::PreviousBreakpointProperty",
|
||||
"right": "debugger::NextBreakpointProperty"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -47,7 +47,8 @@
|
||||
"fn-f": "zed::ToggleFullScreen",
|
||||
"ctrl-cmd-f": "zed::ToggleFullScreen",
|
||||
"ctrl-cmd-z": "edit_prediction::RateCompletions",
|
||||
"ctrl-cmd-i": "edit_prediction::ToggleMenu"
|
||||
"ctrl-cmd-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-cmd-l": "lsp_tool::ToggleMenu"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -282,9 +283,9 @@
|
||||
"cmd->": "assistant::QuoteSelection",
|
||||
"cmd-alt-e": "agent::RemoveAllContext",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-ctrl-b": "agent::ToggleBurnMode",
|
||||
"cmd-shift-enter": "agent::ContinueThread",
|
||||
"alt-enter": "agent::ContinueWithBurnMode",
|
||||
"cmd-alt-b": "agent::ToggleBurnMode"
|
||||
"alt-enter": "agent::ContinueWithBurnMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -544,11 +545,25 @@
|
||||
"cmd-k r": "editor::RevealInFileManager",
|
||||
"cmd-k p": "editor::CopyPath",
|
||||
"cmd-\\": "pane::SplitRight",
|
||||
"cmd-k v": "markdown::OpenPreviewToTheSide",
|
||||
"cmd-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-cmd-c": "editor::DisplayCursorNames"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && extension == md",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-k v": "markdown::OpenPreviewToTheSide",
|
||||
"cmd-shift-v": "markdown::OpenPreview"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && extension == svg",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-k v": "svg::OpenPreviewToTheSide",
|
||||
"cmd-shift-v": "svg::OpenPreview"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"use_key_equivalents": true,
|
||||
@@ -587,7 +602,7 @@
|
||||
"alt-cmd-o": ["projects::OpenRecent", { "create_new_window": false }],
|
||||
"ctrl-cmd-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
|
||||
"ctrl-cmd-shift-o": ["projects::OpenRemote", { "from_existing_connection": true, "create_new_window": false }],
|
||||
"alt-cmd-b": "branches::OpenRecent",
|
||||
"cmd-ctrl-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd-k s": "workspace::SaveWithoutFormat",
|
||||
@@ -605,6 +620,7 @@
|
||||
"cmd-8": ["workspace::ActivatePane", 7],
|
||||
"cmd-9": ["workspace::ActivatePane", 8],
|
||||
"cmd-b": "workspace::ToggleLeftDock",
|
||||
"cmd-alt-b": "workspace::ToggleRightDock",
|
||||
"cmd-r": "workspace::ToggleRightDock",
|
||||
"cmd-j": "workspace::ToggleBottomDock",
|
||||
"alt-cmd-y": "workspace::CloseAllDocks",
|
||||
@@ -964,7 +980,9 @@
|
||||
"context": "BreakpointList",
|
||||
"bindings": {
|
||||
"space": "debugger::ToggleEnableBreakpoint",
|
||||
"backspace": "debugger::UnsetBreakpoint"
|
||||
"backspace": "debugger::UnsetBreakpoint",
|
||||
"left": "debugger::PreviousBreakpointProperty",
|
||||
"right": "debugger::NextBreakpointProperty"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"ctrl-shift-i": "agent::ToggleFocus",
|
||||
"ctrl-l": "agent::ToggleFocus",
|
||||
"ctrl-shift-l": "agent::ToggleFocus",
|
||||
"ctrl-alt-b": "agent::ToggleFocus",
|
||||
"ctrl-shift-j": "agent::OpenConfiguration"
|
||||
}
|
||||
},
|
||||
@@ -42,7 +41,6 @@
|
||||
"ctrl-shift-i": "workspace::ToggleRightDock",
|
||||
"ctrl-l": "workspace::ToggleRightDock",
|
||||
"ctrl-shift-l": "workspace::ToggleRightDock",
|
||||
"ctrl-alt-b": "workspace::ToggleRightDock",
|
||||
"ctrl-w": "workspace::ToggleRightDock", // technically should close chat
|
||||
"ctrl-.": "agent::ToggleProfileSelector",
|
||||
"ctrl-/": "agent::ToggleModelSelector",
|
||||
|
||||
@@ -59,7 +59,8 @@
|
||||
"alt->": "editor::MoveToEnd", // end-of-buffer
|
||||
"ctrl-l": "editor::ScrollCursorCenterTopBottom", // recenter-top-bottom
|
||||
"ctrl-s": "buffer_search::Deploy", // isearch-forward
|
||||
"alt-^": "editor::JoinLines" // join-line
|
||||
"alt-^": "editor::JoinLines", // join-line
|
||||
"alt-q": "editor::Rewrap" // fill-paragraph
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"cmd-shift-i": "agent::ToggleFocus",
|
||||
"cmd-l": "agent::ToggleFocus",
|
||||
"cmd-shift-l": "agent::ToggleFocus",
|
||||
"cmd-alt-b": "agent::ToggleFocus",
|
||||
"cmd-shift-j": "agent::OpenConfiguration"
|
||||
}
|
||||
},
|
||||
@@ -43,7 +42,6 @@
|
||||
"cmd-shift-i": "workspace::ToggleRightDock",
|
||||
"cmd-l": "workspace::ToggleRightDock",
|
||||
"cmd-shift-l": "workspace::ToggleRightDock",
|
||||
"cmd-alt-b": "workspace::ToggleRightDock",
|
||||
"cmd-w": "workspace::ToggleRightDock", // technically should close chat
|
||||
"cmd-.": "agent::ToggleProfileSelector",
|
||||
"cmd-/": "agent::ToggleModelSelector",
|
||||
|
||||
@@ -59,7 +59,8 @@
|
||||
"alt->": "editor::MoveToEnd", // end-of-buffer
|
||||
"ctrl-l": "editor::ScrollCursorCenterTopBottom", // recenter-top-bottom
|
||||
"ctrl-s": "buffer_search::Deploy", // isearch-forward
|
||||
"alt-^": "editor::JoinLines" // join-line
|
||||
"alt-^": "editor::JoinLines", // join-line
|
||||
"alt-q": "editor::Rewrap" // fill-paragraph
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1720,6 +1720,11 @@
|
||||
// }
|
||||
// }
|
||||
},
|
||||
// Common language server settings.
|
||||
"global_lsp_settings": {
|
||||
// Whether to show the LSP servers button in the status bar.
|
||||
"button": true
|
||||
},
|
||||
// Jupyter settings
|
||||
"jupyter": {
|
||||
"enabled": true
|
||||
|
||||
@@ -21,6 +21,7 @@ futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
smallvec.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -80,10 +80,13 @@ impl ActivityIndicator {
|
||||
let this = cx.new(|cx| {
|
||||
let mut status_events = languages.language_server_binary_statuses();
|
||||
cx.spawn(async move |this, cx| {
|
||||
while let Some((name, status)) = status_events.next().await {
|
||||
while let Some((name, binary_status)) = status_events.next().await {
|
||||
this.update(cx, |this: &mut ActivityIndicator, cx| {
|
||||
this.statuses.retain(|s| s.name != name);
|
||||
this.statuses.push(ServerStatus { name, status });
|
||||
this.statuses.push(ServerStatus {
|
||||
name,
|
||||
status: LanguageServerStatusUpdate::Binary(binary_status),
|
||||
});
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
@@ -112,8 +115,76 @@ impl ActivityIndicator {
|
||||
|
||||
cx.subscribe(
|
||||
&project.read(cx).lsp_store(),
|
||||
|_, _, event, cx| match event {
|
||||
LspStoreEvent::LanguageServerUpdate { .. } => cx.notify(),
|
||||
|activity_indicator, _, event, cx| match event {
|
||||
LspStoreEvent::LanguageServerUpdate { name, message, .. } => {
|
||||
if let proto::update_language_server::Variant::StatusUpdate(status_update) =
|
||||
message
|
||||
{
|
||||
let Some(name) = name.clone() else {
|
||||
return;
|
||||
};
|
||||
let status = match &status_update.status {
|
||||
Some(proto::status_update::Status::Binary(binary_status)) => {
|
||||
if let Some(binary_status) =
|
||||
proto::ServerBinaryStatus::from_i32(*binary_status)
|
||||
{
|
||||
let binary_status = match binary_status {
|
||||
proto::ServerBinaryStatus::None => BinaryStatus::None,
|
||||
proto::ServerBinaryStatus::CheckingForUpdate => {
|
||||
BinaryStatus::CheckingForUpdate
|
||||
}
|
||||
proto::ServerBinaryStatus::Downloading => {
|
||||
BinaryStatus::Downloading
|
||||
}
|
||||
proto::ServerBinaryStatus::Starting => {
|
||||
BinaryStatus::Starting
|
||||
}
|
||||
proto::ServerBinaryStatus::Stopping => {
|
||||
BinaryStatus::Stopping
|
||||
}
|
||||
proto::ServerBinaryStatus::Stopped => {
|
||||
BinaryStatus::Stopped
|
||||
}
|
||||
proto::ServerBinaryStatus::Failed => {
|
||||
let Some(error) = status_update.message.clone()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
BinaryStatus::Failed { error }
|
||||
}
|
||||
};
|
||||
LanguageServerStatusUpdate::Binary(binary_status)
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Some(proto::status_update::Status::Health(health_status)) => {
|
||||
if let Some(health) =
|
||||
proto::ServerHealth::from_i32(*health_status)
|
||||
{
|
||||
let health = match health {
|
||||
proto::ServerHealth::Ok => ServerHealth::Ok,
|
||||
proto::ServerHealth::Warning => ServerHealth::Warning,
|
||||
proto::ServerHealth::Error => ServerHealth::Error,
|
||||
};
|
||||
LanguageServerStatusUpdate::Health(
|
||||
health,
|
||||
status_update.message.clone().map(SharedString::from),
|
||||
)
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
None => return,
|
||||
};
|
||||
|
||||
activity_indicator.statuses.retain(|s| s.name != name);
|
||||
activity_indicator
|
||||
.statuses
|
||||
.push(ServerStatus { name, status });
|
||||
}
|
||||
cx.notify()
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
@@ -228,9 +299,23 @@ impl ActivityIndicator {
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
updater.update(cx, |updater, cx| updater.dismiss_error(cx));
|
||||
let error_dismissed = if let Some(updater) = &self.auto_updater {
|
||||
updater.update(cx, |updater, cx| updater.dismiss_error(cx))
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if error_dismissed {
|
||||
return;
|
||||
}
|
||||
|
||||
self.project.update(cx, |project, cx| {
|
||||
if project.last_formatting_failure(cx).is_some() {
|
||||
project.reset_last_formatting_failure(cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn pending_language_server_work<'a>(
|
||||
@@ -399,6 +484,12 @@ impl ActivityIndicator {
|
||||
let mut servers_to_clear_statuses = HashSet::<LanguageServerName>::default();
|
||||
for status in &self.statuses {
|
||||
match &status.status {
|
||||
LanguageServerStatusUpdate::Binary(
|
||||
BinaryStatus::Starting | BinaryStatus::Stopping,
|
||||
) => {}
|
||||
LanguageServerStatusUpdate::Binary(BinaryStatus::Stopped) => {
|
||||
servers_to_clear_statuses.insert(status.name.clone());
|
||||
}
|
||||
LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => {
|
||||
checking_for_update.push(status.name.clone());
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ gpui = { workspace = true, "features" = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, "features" = ["test-support"] }
|
||||
language_model = { workspace = true, "features" = ["test-support"] }
|
||||
parking_lot.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -85,6 +85,14 @@ impl AgentProfile {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn is_tool_enabled(&self, source: ToolSource, tool_name: String, cx: &App) -> bool {
|
||||
let Some(settings) = AgentSettings::get_global(cx).profiles.get(&self.id) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
return Self::is_enabled(settings, source, tool_name);
|
||||
}
|
||||
|
||||
fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool {
|
||||
match source {
|
||||
ToolSource::Native => *settings.tools.get(name.as_str()).unwrap_or(&false),
|
||||
|
||||
@@ -71,7 +71,7 @@ impl Column for DataType {
|
||||
}
|
||||
}
|
||||
|
||||
const RULES_FILE_NAMES: [&'static str; 8] = [
|
||||
const RULES_FILE_NAMES: [&'static str; 9] = [
|
||||
".rules",
|
||||
".cursorrules",
|
||||
".windsurfrules",
|
||||
@@ -80,6 +80,7 @@ const RULES_FILE_NAMES: [&'static str; 8] = [
|
||||
"CLAUDE.md",
|
||||
"AGENT.md",
|
||||
"AGENTS.md",
|
||||
"GEMINI.md",
|
||||
];
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
|
||||
@@ -12,17 +12,10 @@ workspace = true
|
||||
path = "src/agent_settings.rs"
|
||||
|
||||
[dependencies]
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
gpui.workspace = true
|
||||
language_model.workspace = true
|
||||
lmstudio = { workspace = true, features = ["schemars"] }
|
||||
log.workspace = true
|
||||
ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
deepseek = { workspace = true, features = ["schemars"] }
|
||||
mistral = { workspace = true, features = ["schemars"] }
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
|
||||
@@ -2,16 +2,10 @@ mod agent_profile;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::open_ai::Model as OpenAiModel;
|
||||
use anthropic::Model as AnthropicModel;
|
||||
use anyhow::{Result, bail};
|
||||
use collections::IndexMap;
|
||||
use deepseek::Model as DeepseekModel;
|
||||
use gpui::{App, Pixels, SharedString};
|
||||
use language_model::LanguageModel;
|
||||
use lmstudio::Model as LmStudioModel;
|
||||
use mistral::Model as MistralModel;
|
||||
use ollama::Model as OllamaModel;
|
||||
use schemars::{JsonSchema, schema::Schema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
@@ -48,45 +42,6 @@ pub enum NotifyWhenAgentWaiting {
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "name", rename_all = "snake_case")]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub enum AgentProviderContentV1 {
|
||||
#[serde(rename = "zed.dev")]
|
||||
ZedDotDev { default_model: Option<String> },
|
||||
#[serde(rename = "openai")]
|
||||
OpenAi {
|
||||
default_model: Option<OpenAiModel>,
|
||||
api_url: Option<String>,
|
||||
available_models: Option<Vec<OpenAiModel>>,
|
||||
},
|
||||
#[serde(rename = "anthropic")]
|
||||
Anthropic {
|
||||
default_model: Option<AnthropicModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
#[serde(rename = "ollama")]
|
||||
Ollama {
|
||||
default_model: Option<OllamaModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
#[serde(rename = "lmstudio")]
|
||||
LmStudio {
|
||||
default_model: Option<LmStudioModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
#[serde(rename = "deepseek")]
|
||||
DeepSeek {
|
||||
default_model: Option<DeepseekModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
#[serde(rename = "mistral")]
|
||||
Mistral {
|
||||
default_model: Option<MistralModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct AgentSettings {
|
||||
pub enabled: bool,
|
||||
@@ -168,366 +123,56 @@ impl LanguageModelParameters {
|
||||
}
|
||||
}
|
||||
|
||||
/// Agent panel settings
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
||||
pub struct AgentSettingsContent {
|
||||
#[serde(flatten)]
|
||||
pub inner: Option<AgentSettingsContentInner>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum AgentSettingsContentInner {
|
||||
Versioned(Box<VersionedAgentSettingsContent>),
|
||||
Legacy(LegacyAgentSettingsContent),
|
||||
}
|
||||
|
||||
impl AgentSettingsContentInner {
|
||||
fn for_v2(content: AgentSettingsContentV2) -> Self {
|
||||
AgentSettingsContentInner::Versioned(Box::new(VersionedAgentSettingsContent::V2(content)))
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for AgentSettingsContent {
|
||||
fn schema_name() -> String {
|
||||
VersionedAgentSettingsContent::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> Schema {
|
||||
VersionedAgentSettingsContent::json_schema(r#gen)
|
||||
}
|
||||
|
||||
fn is_referenceable() -> bool {
|
||||
VersionedAgentSettingsContent::is_referenceable()
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentSettingsContent {
|
||||
pub fn is_version_outdated(&self) -> bool {
|
||||
match &self.inner {
|
||||
Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAgentSettingsContent::V1(_) => true,
|
||||
VersionedAgentSettingsContent::V2(_) => false,
|
||||
},
|
||||
Some(AgentSettingsContentInner::Legacy(_)) => true,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> AgentSettingsContentV2 {
|
||||
match &self.inner {
|
||||
Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAgentSettingsContent::V1(ref settings) => AgentSettingsContentV2 {
|
||||
enabled: settings.enabled,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_width,
|
||||
default_model: settings
|
||||
.provider
|
||||
.clone()
|
||||
.and_then(|provider| match provider {
|
||||
AgentProviderContentV1::ZedDotDev { default_model } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
provider: "zed.dev".into(),
|
||||
model,
|
||||
}),
|
||||
AgentProviderContentV1::OpenAi { default_model, .. } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
provider: "openai".into(),
|
||||
model: model.id().to_string(),
|
||||
}),
|
||||
AgentProviderContentV1::Anthropic { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "anthropic".into(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AgentProviderContentV1::Ollama { default_model, .. } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
provider: "ollama".into(),
|
||||
model: model.id().to_string(),
|
||||
}),
|
||||
AgentProviderContentV1::LmStudio { default_model, .. } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
provider: "lmstudio".into(),
|
||||
model: model.id().to_string(),
|
||||
}),
|
||||
AgentProviderContentV1::DeepSeek { default_model, .. } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
provider: "deepseek".into(),
|
||||
model: model.id().to_string(),
|
||||
}),
|
||||
AgentProviderContentV1::Mistral { default_model, .. } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
provider: "mistral".into(),
|
||||
model: model.id().to_string(),
|
||||
}),
|
||||
}),
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
default_profile: None,
|
||||
default_view: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
stream_edits: None,
|
||||
single_file_review: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
enable_feedback: None,
|
||||
play_sound_when_agent_done: None,
|
||||
},
|
||||
VersionedAgentSettingsContent::V2(ref settings) => settings.clone(),
|
||||
},
|
||||
Some(AgentSettingsContentInner::Legacy(settings)) => AgentSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_height,
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "openai".into(),
|
||||
model: settings
|
||||
.default_open_ai_model
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.id()
|
||||
.to_string(),
|
||||
}),
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
default_profile: None,
|
||||
default_view: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
stream_edits: None,
|
||||
single_file_review: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
enable_feedback: None,
|
||||
play_sound_when_agent_done: None,
|
||||
},
|
||||
None => AgentSettingsContentV2::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dock(&mut self, dock: AgentDockPosition) {
|
||||
match &mut self.inner {
|
||||
Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAgentSettingsContent::V1(ref mut settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
VersionedAgentSettingsContent::V2(ref mut settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
},
|
||||
Some(AgentSettingsContentInner::Legacy(settings)) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
None => {
|
||||
self.inner = Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
dock: Some(dock),
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
}
|
||||
self.dock = Some(dock);
|
||||
}
|
||||
|
||||
pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
|
||||
let model = language_model.id().0.to_string();
|
||||
let provider = language_model.provider_id().0.to_string();
|
||||
|
||||
match &mut self.inner {
|
||||
Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAgentSettingsContent::V1(ref mut settings) => match provider.as_ref() {
|
||||
"zed.dev" => {
|
||||
log::warn!("attempted to set zed.dev model on outdated settings");
|
||||
}
|
||||
"anthropic" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AgentProviderContentV1::Anthropic { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AgentProviderContentV1::Anthropic {
|
||||
default_model: AnthropicModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"ollama" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AgentProviderContentV1::Ollama { api_url, .. }) => api_url.clone(),
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AgentProviderContentV1::Ollama {
|
||||
default_model: Some(ollama::Model::new(
|
||||
&model,
|
||||
None,
|
||||
None,
|
||||
Some(language_model.supports_tools()),
|
||||
Some(language_model.supports_images()),
|
||||
None,
|
||||
)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"lmstudio" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AgentProviderContentV1::LmStudio { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AgentProviderContentV1::LmStudio {
|
||||
default_model: Some(lmstudio::Model::new(
|
||||
&model, None, None, false, false,
|
||||
)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"openai" => {
|
||||
let (api_url, available_models) = match &settings.provider {
|
||||
Some(AgentProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
available_models,
|
||||
..
|
||||
}) => (api_url.clone(), available_models.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AgentProviderContentV1::OpenAi {
|
||||
default_model: OpenAiModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
available_models,
|
||||
});
|
||||
}
|
||||
"deepseek" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AgentProviderContentV1::DeepSeek { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AgentProviderContentV1::DeepSeek {
|
||||
default_model: DeepseekModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
VersionedAgentSettingsContent::V2(ref mut settings) => {
|
||||
settings.default_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
},
|
||||
Some(AgentSettingsContentInner::Legacy(settings)) => {
|
||||
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
|
||||
settings.default_open_ai_model = Some(model);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.inner = Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
}),
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
}
|
||||
self.default_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.inline_assistant_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
self.inline_assistant_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.commit_message_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn v2_setting(
|
||||
&mut self,
|
||||
f: impl FnOnce(&mut AgentSettingsContentV2) -> anyhow::Result<()>,
|
||||
) -> anyhow::Result<()> {
|
||||
match self.inner.get_or_insert_with(|| {
|
||||
AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
..Default::default()
|
||||
})
|
||||
}) {
|
||||
AgentSettingsContentInner::Versioned(boxed) => {
|
||||
if let VersionedAgentSettingsContent::V2(ref mut settings) = **boxed {
|
||||
f(settings)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
self.commit_message_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.thread_summary_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
self.thread_summary_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.always_allow_tool_actions = Some(allow);
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
self.always_allow_tool_actions = Some(allow);
|
||||
}
|
||||
|
||||
pub fn set_play_sound_when_agent_done(&mut self, allow: bool) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.play_sound_when_agent_done = Some(allow);
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
self.play_sound_when_agent_done = Some(allow);
|
||||
}
|
||||
|
||||
pub fn set_single_file_review(&mut self, allow: bool) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.single_file_review = Some(allow);
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
self.single_file_review = Some(allow);
|
||||
}
|
||||
|
||||
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.default_profile = Some(profile_id);
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
self.default_profile = Some(profile_id);
|
||||
}
|
||||
|
||||
pub fn create_profile(
|
||||
@@ -535,79 +180,39 @@ impl AgentSettingsContent {
|
||||
profile_id: AgentProfileId,
|
||||
profile_settings: AgentProfileSettings,
|
||||
) -> Result<()> {
|
||||
self.v2_setting(|settings| {
|
||||
let profiles = settings.profiles.get_or_insert_default();
|
||||
if profiles.contains_key(&profile_id) {
|
||||
bail!("profile with ID '{profile_id}' already exists");
|
||||
}
|
||||
let profiles = self.profiles.get_or_insert_default();
|
||||
if profiles.contains_key(&profile_id) {
|
||||
bail!("profile with ID '{profile_id}' already exists");
|
||||
}
|
||||
|
||||
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
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
);
|
||||
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
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[serde(tag = "version")]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub enum VersionedAgentSettingsContent {
|
||||
#[serde(rename = "1")]
|
||||
V1(AgentSettingsContentV1),
|
||||
#[serde(rename = "2")]
|
||||
V2(AgentSettingsContentV2),
|
||||
}
|
||||
|
||||
impl Default for VersionedAgentSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::V2(AgentSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
default_model: None,
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
default_profile: None,
|
||||
default_view: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
stream_edits: None,
|
||||
single_file_review: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
enable_feedback: None,
|
||||
play_sound_when_agent_done: None,
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AgentSettingsContentV2 {
|
||||
pub struct AgentSettingsContent {
|
||||
/// Whether the Agent is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
@@ -779,65 +384,6 @@ pub struct ContextServerPresetContent {
|
||||
pub tools: IndexMap<Arc<str>, bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AgentSettingsContentV1 {
|
||||
/// Whether the Agent is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the Agent panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the Agent.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AgentDockPosition>,
|
||||
/// Default width in pixels when the Agent is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the Agent is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The provider of the Agent service.
|
||||
///
|
||||
/// This can be "openai", "anthropic", "ollama", "lmstudio", "deepseek", "zed.dev"
|
||||
/// each with their respective default models and configurations.
|
||||
provider: Option<AgentProviderContentV1>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct LegacyAgentSettingsContent {
|
||||
/// Whether to show the Agent panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
pub button: Option<bool>,
|
||||
/// Where to dock the Agent.
|
||||
///
|
||||
/// Default: right
|
||||
pub dock: Option<AgentDockPosition>,
|
||||
/// Default width in pixels when the Agent is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
pub default_width: Option<f32>,
|
||||
/// Default height in pixels when the Agent is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
pub default_height: Option<f32>,
|
||||
/// The default OpenAI model to use when creating new chats.
|
||||
///
|
||||
/// Default: gpt-4-1106-preview
|
||||
pub default_open_ai_model: Option<OpenAiModel>,
|
||||
/// OpenAI API base URL to use when creating new chats.
|
||||
///
|
||||
/// Default: <https://api.openai.com/v1>
|
||||
pub openai_api_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Settings for AgentSettings {
|
||||
const KEY: Option<&'static str> = Some("agent");
|
||||
|
||||
@@ -854,11 +400,6 @@ impl Settings for AgentSettings {
|
||||
let mut settings = AgentSettings::default();
|
||||
|
||||
for value in sources.defaults_and_customizations() {
|
||||
if value.is_version_outdated() {
|
||||
settings.using_outdated_settings_version = true;
|
||||
}
|
||||
|
||||
let value = value.upgrade();
|
||||
merge(&mut settings.enabled, value.enabled);
|
||||
merge(&mut settings.button, value.button);
|
||||
merge(&mut settings.dock, value.dock);
|
||||
@@ -870,17 +411,23 @@ impl Settings for AgentSettings {
|
||||
&mut settings.default_height,
|
||||
value.default_height.map(Into::into),
|
||||
);
|
||||
merge(&mut settings.default_model, value.default_model);
|
||||
merge(&mut settings.default_model, value.default_model.clone());
|
||||
settings.inline_assistant_model = value
|
||||
.inline_assistant_model
|
||||
.clone()
|
||||
.or(settings.inline_assistant_model.take());
|
||||
settings.commit_message_model = value
|
||||
.clone()
|
||||
.commit_message_model
|
||||
.or(settings.commit_message_model.take());
|
||||
settings.thread_summary_model = value
|
||||
.clone()
|
||||
.thread_summary_model
|
||||
.or(settings.thread_summary_model.take());
|
||||
merge(&mut settings.inline_alternatives, value.inline_alternatives);
|
||||
merge(
|
||||
&mut settings.inline_alternatives,
|
||||
value.inline_alternatives.clone(),
|
||||
);
|
||||
merge(
|
||||
&mut settings.always_allow_tool_actions,
|
||||
value.always_allow_tool_actions,
|
||||
@@ -895,7 +442,7 @@ impl Settings for AgentSettings {
|
||||
);
|
||||
merge(&mut settings.stream_edits, value.stream_edits);
|
||||
merge(&mut settings.single_file_review, value.single_file_review);
|
||||
merge(&mut settings.default_profile, value.default_profile);
|
||||
merge(&mut settings.default_profile, value.default_profile.clone());
|
||||
merge(&mut settings.default_view, value.default_view);
|
||||
merge(
|
||||
&mut settings.preferred_completion_mode,
|
||||
@@ -907,24 +454,24 @@ impl Settings for AgentSettings {
|
||||
.model_parameters
|
||||
.extend_from_slice(&value.model_parameters);
|
||||
|
||||
if let Some(profiles) = value.profiles {
|
||||
if let Some(profiles) = value.profiles.as_ref() {
|
||||
settings
|
||||
.profiles
|
||||
.extend(profiles.into_iter().map(|(id, profile)| {
|
||||
(
|
||||
id,
|
||||
id.clone(),
|
||||
AgentProfileSettings {
|
||||
name: profile.name.into(),
|
||||
tools: profile.tools,
|
||||
name: profile.name.clone().into(),
|
||||
tools: profile.tools.clone(),
|
||||
enable_all_context_servers: profile
|
||||
.enable_all_context_servers
|
||||
.unwrap_or_default(),
|
||||
context_servers: profile
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|(context_server_id, preset)| {
|
||||
(
|
||||
context_server_id,
|
||||
context_server_id.clone(),
|
||||
ContextServerPreset {
|
||||
tools: preset.tools.clone(),
|
||||
},
|
||||
@@ -945,28 +492,8 @@ impl Settings for AgentSettings {
|
||||
.read_value("chat.agent.enabled")
|
||||
.and_then(|b| b.as_bool())
|
||||
{
|
||||
match &mut current.inner {
|
||||
Some(AgentSettingsContentInner::Versioned(versioned)) => match versioned.as_mut() {
|
||||
VersionedAgentSettingsContent::V1(setting) => {
|
||||
setting.enabled = Some(b);
|
||||
setting.button = Some(b);
|
||||
}
|
||||
|
||||
VersionedAgentSettingsContent::V2(setting) => {
|
||||
setting.enabled = Some(b);
|
||||
setting.button = Some(b);
|
||||
}
|
||||
},
|
||||
Some(AgentSettingsContentInner::Legacy(setting)) => setting.button = Some(b),
|
||||
None => {
|
||||
current.inner =
|
||||
Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
enabled: Some(b),
|
||||
button: Some(b),
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
}
|
||||
current.enabled = Some(b);
|
||||
current.button = Some(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -976,149 +503,3 @@ fn merge<T>(target: &mut T, value: Option<T>) {
|
||||
*target = value;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fs::Fs;
|
||||
use gpui::{ReadGlobal, TestAppContext};
|
||||
use settings::SettingsStore;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_deserialize_agent_settings_with_version(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
fs.create_dir(paths::settings_file().parent().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
let test_settings = settings::SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
AgentSettings::register(cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
assert!(!AgentSettings::get_global(cx).using_outdated_settings_version);
|
||||
assert_eq!(
|
||||
AgentSettings::get_global(cx).default_model,
|
||||
LanguageModelSelection {
|
||||
provider: "zed.dev".into(),
|
||||
model: "claude-sonnet-4".into(),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
settings::SettingsStore::global(cx).update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
|settings, _| {
|
||||
*settings = AgentSettingsContent {
|
||||
inner: Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "test-provider".into(),
|
||||
model: "gpt-99".into(),
|
||||
}),
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
default_profile: None,
|
||||
default_view: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
play_sound_when_agent_done: None,
|
||||
notify_when_agent_waiting: None,
|
||||
stream_edits: None,
|
||||
single_file_review: None,
|
||||
enable_feedback: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
})),
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
|
||||
assert!(raw_settings_value.contains(r#""version": "2""#));
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AgentSettingsTest {
|
||||
agent: AgentSettingsContent,
|
||||
}
|
||||
|
||||
let agent_settings: AgentSettingsTest =
|
||||
serde_json_lenient::from_str(&raw_settings_value).unwrap();
|
||||
|
||||
assert!(!agent_settings.agent.is_version_outdated());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_load_settings_from_old_key(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
fs.create_dir(paths::settings_file().parent().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
let mut test_settings = settings::SettingsStore::test(cx);
|
||||
let user_settings_content = r#"{
|
||||
"assistant": {
|
||||
"enabled": true,
|
||||
"version": "2",
|
||||
"default_model": {
|
||||
"provider": "zed.dev",
|
||||
"model": "gpt-99"
|
||||
},
|
||||
}}"#;
|
||||
test_settings
|
||||
.set_user_settings(user_settings_content, cx)
|
||||
.unwrap();
|
||||
cx.set_global(test_settings);
|
||||
AgentSettings::register(cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let agent_settings = cx.update(|cx| AgentSettings::get_global(cx).clone());
|
||||
assert!(agent_settings.enabled);
|
||||
assert!(!agent_settings.using_outdated_settings_version);
|
||||
assert_eq!(agent_settings.default_model.model, "gpt-99");
|
||||
|
||||
cx.update_global::<SettingsStore, _>(|settings_store, cx| {
|
||||
settings_store.update_user_settings::<AgentSettings>(cx, |settings| {
|
||||
*settings = AgentSettingsContent {
|
||||
inner: Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
enabled: Some(false),
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "xai".to_owned().into(),
|
||||
model: "grok".to_owned(),
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let settings = cx.update(|cx| SettingsStore::global(cx).raw_user_settings().clone());
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AgentSettingsTest {
|
||||
assistant: AgentSettingsContent,
|
||||
agent: Option<serde_json_lenient::Value>,
|
||||
}
|
||||
|
||||
let agent_settings: AgentSettingsTest = serde_json::from_value(settings).unwrap();
|
||||
assert!(agent_settings.agent.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ use audio::{Audio, Sound};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::actions::{MoveUp, Paste};
|
||||
use editor::scroll::Autoscroll;
|
||||
use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer};
|
||||
use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer, SelectionEffects};
|
||||
use gpui::{
|
||||
AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry,
|
||||
ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla,
|
||||
@@ -204,7 +204,7 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
|
||||
MarkdownStyle {
|
||||
base_text_style: text_style.clone(),
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: cx.theme().players().local().selection,
|
||||
selection_background_color: cx.theme().colors().element_selection_background,
|
||||
code_block_overflow_x_scroll: true,
|
||||
table_overflow_x_scroll: true,
|
||||
heading_level_styles: Some(HeadingLevelStyles {
|
||||
@@ -301,7 +301,7 @@ fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
|
||||
MarkdownStyle {
|
||||
base_text_style: text_style,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: cx.theme().players().local().selection,
|
||||
selection_background_color: cx.theme().colors().element_selection_background,
|
||||
code_block_overflow_x_scroll: false,
|
||||
code_block: StyleRefinement {
|
||||
margin: EdgesRefinement::default(),
|
||||
@@ -689,9 +689,12 @@ fn open_markdown_link(
|
||||
})
|
||||
.context("Could not find matching symbol")?;
|
||||
|
||||
editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
|
||||
s.select_anchor_ranges([symbol_range.start..symbol_range.start])
|
||||
});
|
||||
editor.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::center()),
|
||||
window,
|
||||
cx,
|
||||
|s| s.select_anchor_ranges([symbol_range.start..symbol_range.start]),
|
||||
);
|
||||
anyhow::Ok(())
|
||||
})
|
||||
})
|
||||
@@ -708,10 +711,15 @@ fn open_markdown_link(
|
||||
.downcast::<Editor>()
|
||||
.context("Item is not an editor")?;
|
||||
active_editor.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
|
||||
s.select_ranges([Point::new(line_range.start as u32, 0)
|
||||
..Point::new(line_range.start as u32, 0)])
|
||||
});
|
||||
editor.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::center()),
|
||||
window,
|
||||
cx,
|
||||
|s| {
|
||||
s.select_ranges([Point::new(line_range.start as u32, 0)
|
||||
..Point::new(line_range.start as u32, 0)])
|
||||
},
|
||||
);
|
||||
anyhow::Ok(())
|
||||
})
|
||||
})
|
||||
@@ -809,7 +817,12 @@ impl ActiveThread {
|
||||
};
|
||||
|
||||
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
|
||||
this.push_message(&message.id, &message.segments, window, cx);
|
||||
let rendered_message = RenderedMessage::from_segments(
|
||||
&message.segments,
|
||||
this.language_registry.clone(),
|
||||
cx,
|
||||
);
|
||||
this.push_rendered_message(message.id, rendered_message);
|
||||
|
||||
for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) {
|
||||
this.render_tool_use_markdown(
|
||||
@@ -875,36 +888,11 @@ impl ActiveThread {
|
||||
&self.text_thread_store
|
||||
}
|
||||
|
||||
fn push_message(
|
||||
&mut self,
|
||||
id: &MessageId,
|
||||
segments: &[MessageSegment],
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
fn push_rendered_message(&mut self, id: MessageId, rendered_message: RenderedMessage) {
|
||||
let old_len = self.messages.len();
|
||||
self.messages.push(*id);
|
||||
self.messages.push(id);
|
||||
self.list_state.splice(old_len..old_len, 1);
|
||||
|
||||
let rendered_message =
|
||||
RenderedMessage::from_segments(segments, self.language_registry.clone(), cx);
|
||||
self.rendered_messages_by_id.insert(*id, rendered_message);
|
||||
}
|
||||
|
||||
fn edited_message(
|
||||
&mut self,
|
||||
id: &MessageId,
|
||||
segments: &[MessageSegment],
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
|
||||
return;
|
||||
};
|
||||
self.list_state.splice(index..index + 1, 1);
|
||||
let rendered_message =
|
||||
RenderedMessage::from_segments(segments, self.language_registry.clone(), cx);
|
||||
self.rendered_messages_by_id.insert(*id, rendered_message);
|
||||
self.rendered_messages_by_id.insert(id, rendered_message);
|
||||
}
|
||||
|
||||
fn deleted_message(&mut self, id: &MessageId) {
|
||||
@@ -1037,31 +1025,43 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
ThreadEvent::MessageAdded(message_id) => {
|
||||
if let Some(message_segments) = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.message(*message_id)
|
||||
.map(|message| message.segments.clone())
|
||||
{
|
||||
self.push_message(message_id, &message_segments, window, cx);
|
||||
if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
|
||||
thread.message(*message_id).map(|message| {
|
||||
RenderedMessage::from_segments(
|
||||
&message.segments,
|
||||
self.language_registry.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}) {
|
||||
self.push_rendered_message(*message_id, rendered_message);
|
||||
}
|
||||
|
||||
self.save_thread(cx);
|
||||
cx.notify();
|
||||
}
|
||||
ThreadEvent::MessageEdited(message_id) => {
|
||||
if let Some(message_segments) = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.message(*message_id)
|
||||
.map(|message| message.segments.clone())
|
||||
{
|
||||
self.edited_message(message_id, &message_segments, window, cx);
|
||||
if let Some(index) = self.messages.iter().position(|id| id == message_id) {
|
||||
if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
|
||||
thread.message(*message_id).map(|message| {
|
||||
let mut rendered_message = RenderedMessage {
|
||||
language_registry: self.language_registry.clone(),
|
||||
segments: Vec::with_capacity(message.segments.len()),
|
||||
};
|
||||
for segment in &message.segments {
|
||||
rendered_message.push_segment(segment, cx);
|
||||
}
|
||||
rendered_message
|
||||
})
|
||||
}) {
|
||||
self.list_state.splice(index..index + 1, 1);
|
||||
self.rendered_messages_by_id
|
||||
.insert(*message_id, rendered_message);
|
||||
self.scroll_to_bottom(cx);
|
||||
self.save_thread(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
self.scroll_to_bottom(cx);
|
||||
self.save_thread(cx);
|
||||
cx.notify();
|
||||
}
|
||||
ThreadEvent::MessageDeleted(message_id) => {
|
||||
self.deleted_message(message_id);
|
||||
@@ -1148,6 +1148,9 @@ impl ActiveThread {
|
||||
self.save_thread(cx);
|
||||
cx.notify();
|
||||
}
|
||||
ThreadEvent::RetriesFailed { message } => {
|
||||
self.show_notification(message, ui::IconName::Warning, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1311,17 +1314,11 @@ impl ActiveThread {
|
||||
fn start_editing_message(
|
||||
&mut self,
|
||||
message_id: MessageId,
|
||||
message_segments: &[MessageSegment],
|
||||
message_text: impl Into<Arc<str>>,
|
||||
message_creases: &[MessageCrease],
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// User message should always consist of a single text segment,
|
||||
// therefore we can skip returning early if it's not a text segment.
|
||||
let Some(MessageSegment::Text(message_text)) = message_segments.first() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let editor = crate::message_editor::create_editor(
|
||||
self.workspace.clone(),
|
||||
self.context_store.downgrade(),
|
||||
@@ -1333,7 +1330,7 @@ impl ActiveThread {
|
||||
cx,
|
||||
);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_text(message_text.clone(), window, cx);
|
||||
editor.set_text(message_text, window, cx);
|
||||
insert_message_creases(editor, message_creases, &self.context_store, window, cx);
|
||||
editor.focus_handle(cx).focus(window);
|
||||
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
||||
@@ -1828,8 +1825,6 @@ impl ActiveThread {
|
||||
return div().children(loading_dots).into_any();
|
||||
}
|
||||
|
||||
let message_creases = message.creases.clone();
|
||||
|
||||
let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
@@ -1851,9 +1846,10 @@ impl ActiveThread {
|
||||
.filter(|(id, _)| *id == message_id)
|
||||
.map(|(_, state)| state);
|
||||
|
||||
let colors = cx.theme().colors();
|
||||
let editor_bg_color = colors.editor_background;
|
||||
let panel_bg = colors.panel_background;
|
||||
let (editor_bg_color, panel_bg) = {
|
||||
let colors = cx.theme().colors();
|
||||
(colors.editor_background, colors.panel_background)
|
||||
};
|
||||
|
||||
let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::DocumentText)
|
||||
.icon_size(IconSize::XSmall)
|
||||
@@ -2041,137 +2037,162 @@ impl ActiveThread {
|
||||
}
|
||||
});
|
||||
|
||||
let styled_message = match message.role {
|
||||
Role::User => v_flex()
|
||||
.id(("message-container", ix))
|
||||
.pt_2()
|
||||
.pl_2()
|
||||
.pr_2p5()
|
||||
.pb_4()
|
||||
.child(
|
||||
let styled_message = if message.ui_only {
|
||||
self.render_ui_notification(message_content, ix, cx)
|
||||
} else {
|
||||
match message.role {
|
||||
Role::User => {
|
||||
let colors = cx.theme().colors();
|
||||
v_flex()
|
||||
.id(("user-message", ix))
|
||||
.bg(editor_bg_color)
|
||||
.rounded_lg()
|
||||
.shadow_md()
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.hover(|hover| hover.border_color(colors.text_accent.opacity(0.5)))
|
||||
.id(("message-container", ix))
|
||||
.pt_2()
|
||||
.pl_2()
|
||||
.pr_2p5()
|
||||
.pb_4()
|
||||
.child(
|
||||
v_flex()
|
||||
.p_2p5()
|
||||
.gap_1()
|
||||
.children(message_content)
|
||||
.when_some(editing_message_state, |this, state| {
|
||||
let focus_handle = state.editor.focus_handle(cx).clone();
|
||||
.id(("user-message", ix))
|
||||
.bg(editor_bg_color)
|
||||
.rounded_lg()
|
||||
.shadow_md()
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.hover(|hover| hover.border_color(colors.text_accent.opacity(0.5)))
|
||||
.child(
|
||||
v_flex()
|
||||
.p_2p5()
|
||||
.gap_1()
|
||||
.children(message_content)
|
||||
.when_some(editing_message_state, |this, state| {
|
||||
let focus_handle = state.editor.focus_handle(cx).clone();
|
||||
|
||||
this.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
.child(
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
.child(
|
||||
div()
|
||||
.opacity(0.8)
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Indicator)
|
||||
.color(Color::Warning)
|
||||
div()
|
||||
.opacity(0.8)
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Indicator)
|
||||
.color(Color::Warning)
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Editing will restart the thread from this point.")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Editing will restart the thread from this point.")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
IconButton::new(
|
||||
"cancel-edit-message",
|
||||
IconName::Close,
|
||||
)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_color(Color::Error)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Cancel Edit",
|
||||
&menu::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
IconButton::new(
|
||||
"cancel-edit-message",
|
||||
IconName::Close,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(Self::handle_cancel_click)),
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_color(Color::Error)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Cancel Edit",
|
||||
&menu::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(Self::handle_cancel_click)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(
|
||||
"confirm-edit-message",
|
||||
IconName::Return,
|
||||
)
|
||||
.disabled(state.editor.read(cx).is_empty(cx))
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Regenerate",
|
||||
&menu::Confirm,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(Self::handle_regenerate_click),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(
|
||||
"confirm-edit-message",
|
||||
IconName::Return,
|
||||
)
|
||||
.disabled(state.editor.read(cx).is_empty(cx))
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Regenerate",
|
||||
&menu::Confirm,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(Self::handle_regenerate_click),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.on_click(cx.listener({
|
||||
let message_creases = message.creases.clone();
|
||||
move |this, _, window, cx| {
|
||||
if let Some(message_text) =
|
||||
this.thread.read(cx).message(message_id).and_then(|message| {
|
||||
message.segments.first().and_then(|segment| {
|
||||
match segment {
|
||||
MessageSegment::Text(message_text) => {
|
||||
Some(Into::<Arc<str>>::into(message_text.as_str()))
|
||||
}
|
||||
_ => {
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
{
|
||||
this.start_editing_message(
|
||||
message_id,
|
||||
message_text,
|
||||
&message_creases,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
})),
|
||||
)
|
||||
.on_click(cx.listener({
|
||||
let message_segments = message.segments.clone();
|
||||
move |this, _, window, cx| {
|
||||
this.start_editing_message(
|
||||
message_id,
|
||||
&message_segments,
|
||||
&message_creases,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
),
|
||||
Role::Assistant => v_flex()
|
||||
.id(("message-container", ix))
|
||||
.px(RESPONSE_PADDING_X)
|
||||
.gap_2()
|
||||
.children(message_content)
|
||||
.when(has_tool_uses, |parent| {
|
||||
parent.children(tool_uses.into_iter().map(|tool_use| {
|
||||
self.render_tool_use(tool_use, window, workspace.clone(), cx)
|
||||
}))
|
||||
}),
|
||||
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
|
||||
v_flex()
|
||||
.bg(colors.editor_background)
|
||||
.rounded_sm()
|
||||
.child(div().p_4().children(message_content)),
|
||||
),
|
||||
}
|
||||
Role::Assistant => v_flex()
|
||||
.id(("message-container", ix))
|
||||
.px(RESPONSE_PADDING_X)
|
||||
.gap_2()
|
||||
.children(message_content)
|
||||
.when(has_tool_uses, |parent| {
|
||||
parent.children(tool_uses.into_iter().map(|tool_use| {
|
||||
self.render_tool_use(tool_use, window, workspace.clone(), cx)
|
||||
}))
|
||||
}),
|
||||
Role::System => {
|
||||
let colors = cx.theme().colors();
|
||||
div().id(("message-container", ix)).py_1().px_2().child(
|
||||
v_flex()
|
||||
.bg(colors.editor_background)
|
||||
.rounded_sm()
|
||||
.child(div().p_4().children(message_content)),
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let after_editing_message = self
|
||||
@@ -2510,6 +2531,42 @@ impl ActiveThread {
|
||||
.blend(cx.theme().colors().editor_foreground.opacity(0.025))
|
||||
}
|
||||
|
||||
fn render_ui_notification(
|
||||
&self,
|
||||
message_content: impl IntoIterator<Item = impl IntoElement>,
|
||||
ix: usize,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Stateful<Div> {
|
||||
let colors = cx.theme().colors();
|
||||
div().id(("message-container", ix)).py_1().px_2().child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.bg(colors.editor_background)
|
||||
.rounded_sm()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.child(
|
||||
div().flex_none().child(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.text_size(TextSize::Small.rems(cx))
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.children(message_content),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_message_thinking_segment(
|
||||
&self,
|
||||
message_id: MessageId,
|
||||
@@ -3764,9 +3821,9 @@ mod tests {
|
||||
|
||||
// Stream response to user message
|
||||
thread.update(cx, |thread, cx| {
|
||||
let request =
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx);
|
||||
thread.stream_completion(request, model, cx.active_window(), cx)
|
||||
let intent = CompletionIntent::UserPrompt;
|
||||
let request = thread.to_completion_request(model.clone(), intent, cx);
|
||||
thread.stream_completion(request, model, intent, cx.active_window(), cx)
|
||||
});
|
||||
// Follow the agent
|
||||
cx.update(|window, cx| {
|
||||
@@ -3826,13 +3883,15 @@ mod tests {
|
||||
});
|
||||
|
||||
active_thread.update_in(cx, |active_thread, window, cx| {
|
||||
active_thread.start_editing_message(
|
||||
message.id,
|
||||
message.segments.as_slice(),
|
||||
message.creases.as_slice(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) {
|
||||
active_thread.start_editing_message(
|
||||
message.id,
|
||||
message_text,
|
||||
message.creases.as_slice(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
let editor = active_thread
|
||||
.editing_message
|
||||
.as_ref()
|
||||
@@ -3847,13 +3906,15 @@ mod tests {
|
||||
|
||||
let message = thread.update(cx, |thread, _| thread.message(message.id).cloned().unwrap());
|
||||
active_thread.update_in(cx, |active_thread, window, cx| {
|
||||
active_thread.start_editing_message(
|
||||
message.id,
|
||||
message.segments.as_slice(),
|
||||
message.creases.as_slice(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) {
|
||||
active_thread.start_editing_message(
|
||||
message.id,
|
||||
message_text,
|
||||
message.creases.as_slice(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
let editor = active_thread
|
||||
.editing_message
|
||||
.as_ref()
|
||||
@@ -3935,13 +3996,15 @@ mod tests {
|
||||
|
||||
// Edit the message while the completion is still running
|
||||
active_thread.update_in(cx, |active_thread, window, cx| {
|
||||
active_thread.start_editing_message(
|
||||
message.id,
|
||||
message.segments.as_slice(),
|
||||
message.creases.as_slice(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) {
|
||||
active_thread.start_editing_message(
|
||||
message.id,
|
||||
message_text,
|
||||
message.creases.as_slice(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
let editor = active_thread
|
||||
.editing_message
|
||||
.as_ref()
|
||||
|
||||
@@ -748,7 +748,7 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
|
||||
|
||||
MarkdownStyle {
|
||||
base_text_style: text_style.clone(),
|
||||
selection_background_color: cx.theme().players().local().selection,
|
||||
selection_background_color: colors.element_selection_background,
|
||||
link: TextStyleRefinement {
|
||||
background_color: Some(colors.editor_foreground.opacity(0.025)),
|
||||
underline: Some(UnderlineStyle {
|
||||
|
||||
@@ -272,42 +272,35 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
let server_id = server_id.clone();
|
||||
let tool_name = tool_name.clone();
|
||||
move |settings: &mut AgentSettingsContent, _cx| {
|
||||
settings
|
||||
.v2_setting(|v2_settings| {
|
||||
let profiles = v2_settings.profiles.get_or_insert_default();
|
||||
let profile =
|
||||
profiles
|
||||
.entry(profile_id)
|
||||
.or_insert_with(|| AgentProfileContent {
|
||||
name: default_profile.name.into(),
|
||||
tools: default_profile.tools,
|
||||
enable_all_context_servers: Some(
|
||||
default_profile.enable_all_context_servers,
|
||||
),
|
||||
context_servers: default_profile
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
let profiles = settings.profiles.get_or_insert_default();
|
||||
let profile = profiles
|
||||
.entry(profile_id)
|
||||
.or_insert_with(|| AgentProfileContent {
|
||||
name: default_profile.name.into(),
|
||||
tools: default_profile.tools,
|
||||
enable_all_context_servers: Some(
|
||||
default_profile.enable_all_context_servers,
|
||||
),
|
||||
context_servers: default_profile
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
|
||||
if let Some(server_id) = server_id {
|
||||
let preset = profile.context_servers.entry(server_id).or_default();
|
||||
*preset.tools.entry(tool_name).or_default() = !is_currently_enabled;
|
||||
} else {
|
||||
*profile.tools.entry(tool_name).or_default() = !is_currently_enabled;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
if let Some(server_id) = server_id {
|
||||
let preset = profile.context_servers.entry(server_id).or_default();
|
||||
*preset.tools.entry(tool_name).or_default() = !is_currently_enabled;
|
||||
} else {
|
||||
*profile.tools.entry(tool_name).or_default() = !is_currently_enabled;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ use anyhow::Result;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot, ToPoint,
|
||||
Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
|
||||
SelectionEffects, ToPoint,
|
||||
actions::{GoToHunk, GoToPreviousHunk},
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
@@ -171,15 +172,9 @@ impl AgentDiffPane {
|
||||
|
||||
if let Some(first_hunk) = first_hunk {
|
||||
let first_hunk_start = first_hunk.multi_buffer_range().start;
|
||||
editor.change_selections(
|
||||
Some(Autoscroll::fit()),
|
||||
window,
|
||||
cx,
|
||||
|selections| {
|
||||
selections
|
||||
.select_anchor_ranges([first_hunk_start..first_hunk_start]);
|
||||
},
|
||||
)
|
||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||
selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +237,7 @@ impl AgentDiffPane {
|
||||
|
||||
if let Some(first_hunk) = first_hunk {
|
||||
let first_hunk_start = first_hunk.multi_buffer_range().start;
|
||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
|
||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||
selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
|
||||
})
|
||||
}
|
||||
@@ -416,7 +411,7 @@ fn update_editor_selection(
|
||||
};
|
||||
|
||||
if let Some(target_hunk) = target_hunk {
|
||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
|
||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||
let next_hunk_start = target_hunk.multi_buffer_range().start;
|
||||
selections.select_anchor_ranges([next_hunk_start..next_hunk_start]);
|
||||
})
|
||||
@@ -1380,6 +1375,7 @@ impl AgentDiff {
|
||||
| ThreadEvent::ToolConfirmationNeeded
|
||||
| ThreadEvent::ToolUseLimitReached
|
||||
| ThreadEvent::CancelEditing
|
||||
| ThreadEvent::RetriesFailed { .. }
|
||||
| ThreadEvent::ProfileChanged => {}
|
||||
}
|
||||
}
|
||||
@@ -1543,7 +1539,7 @@ impl AgentDiff {
|
||||
let first_hunk_start = first_hunk.multi_buffer_range().start;
|
||||
|
||||
editor.change_selections(
|
||||
Some(Autoscroll::center()),
|
||||
SelectionEffects::scroll(Autoscroll::center()),
|
||||
window,
|
||||
cx,
|
||||
|selections| {
|
||||
@@ -1867,7 +1863,7 @@ mod tests {
|
||||
|
||||
// Rejecting a hunk also moves the cursor to the next hunk, possibly cycling if it's at the end.
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
|
||||
selections.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
|
||||
});
|
||||
});
|
||||
@@ -2123,7 +2119,7 @@ mod tests {
|
||||
|
||||
// Rejecting a hunk also moves the cursor to the next hunk, possibly cycling if it's at the end.
|
||||
editor1.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
|
||||
selections.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
|
||||
pub struct AgentModelSelector {
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
@@ -94,20 +94,35 @@ impl Render for AgentModelSelector {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let model = self.selector.read(cx).delegate.active_model(cx);
|
||||
let model_name = model
|
||||
.as_ref()
|
||||
.map(|model| model.model.name().0)
|
||||
.unwrap_or_else(|| SharedString::from("No model selected"));
|
||||
let provider_icon = model
|
||||
.as_ref()
|
||||
.map(|model| model.provider.icon())
|
||||
.unwrap_or_else(|| IconName::Ai);
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
Button::new("active-model", model_name)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ChevronDown)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_color(Color::Muted),
|
||||
ButtonLike::new("active-model")
|
||||
.child(
|
||||
Icon::new(provider_icon)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.ml_0p5(),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Change Model",
|
||||
|
||||
@@ -4,6 +4,7 @@ mod agent_diff;
|
||||
mod agent_model_selector;
|
||||
mod agent_panel;
|
||||
mod buffer_codegen;
|
||||
mod burn_mode_tooltip;
|
||||
mod context_picker;
|
||||
mod context_server_configuration;
|
||||
mod context_strip;
|
||||
@@ -11,7 +12,6 @@ mod debug;
|
||||
mod inline_assistant;
|
||||
mod inline_prompt_editor;
|
||||
mod language_model_selector;
|
||||
mod max_mode_tooltip;
|
||||
mod message_editor;
|
||||
mod profile_selector;
|
||||
mod slash_command;
|
||||
|
||||
@@ -1094,15 +1094,9 @@ mod tests {
|
||||
};
|
||||
use language_model::{LanguageModelRegistry, TokenUsage};
|
||||
use rand::prelude::*;
|
||||
use serde::Serialize;
|
||||
use settings::SettingsStore;
|
||||
use std::{future, sync::Arc};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DummyCompletionRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
init_test(cx);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use gpui::{Context, FontWeight, IntoElement, Render, Window};
|
||||
use ui::{prelude::*, tooltip_container};
|
||||
|
||||
pub struct MaxModeTooltip {
|
||||
pub struct BurnModeTooltip {
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl MaxModeTooltip {
|
||||
impl BurnModeTooltip {
|
||||
pub fn new() -> Self {
|
||||
Self { selected: false }
|
||||
}
|
||||
@@ -16,7 +16,7 @@ impl MaxModeTooltip {
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for MaxModeTooltip {
|
||||
impl Render for BurnModeTooltip {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let (icon, color) = if self.selected {
|
||||
(IconName::ZedBurnModeOn, Color::Error)
|
||||
@@ -661,7 +661,7 @@ fn recent_context_picker_entries(
|
||||
|
||||
let active_thread_id = workspace
|
||||
.panel::<AgentPanel>(cx)
|
||||
.and_then(|panel| Some(panel.read(cx).active_thread()?.read(cx).id()));
|
||||
.and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id()));
|
||||
|
||||
if let Some((thread_store, text_thread_store)) = thread_store
|
||||
.and_then(|store| store.upgrade())
|
||||
@@ -930,8 +930,8 @@ impl MentionLink {
|
||||
format!(
|
||||
"[@{} ({}-{})]({}:{}:{}-{})",
|
||||
file_name,
|
||||
line_range.start,
|
||||
line_range.end,
|
||||
line_range.start + 1,
|
||||
line_range.end + 1,
|
||||
Self::SELECTION,
|
||||
full_path,
|
||||
line_range.start,
|
||||
|
||||
@@ -161,7 +161,7 @@ impl ContextStrip {
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
let panel = workspace.read(cx).panel::<AgentPanel>(cx)?.read(cx);
|
||||
|
||||
if let Some(active_thread) = panel.active_thread() {
|
||||
if let Some(active_thread) = panel.active_thread(cx) {
|
||||
let weak_active_thread = active_thread.downgrade();
|
||||
|
||||
let active_thread = active_thread.read(cx);
|
||||
|
||||
@@ -18,6 +18,7 @@ use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
||||
use editor::SelectionEffects;
|
||||
use editor::{
|
||||
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
|
||||
MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
|
||||
@@ -1159,7 +1160,7 @@ impl InlineAssistant {
|
||||
|
||||
let position = assist.range.start;
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
|
||||
selections.select_anchor_ranges([position..position])
|
||||
});
|
||||
|
||||
|
||||
@@ -575,7 +575,7 @@ impl MessageEditor {
|
||||
fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let thread = self.thread.read(cx);
|
||||
let model = thread.configured_model();
|
||||
if !model?.model.supports_max_mode() {
|
||||
if !model?.model.supports_burn_mode() {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
burn_mode_tooltip::BurnModeTooltip,
|
||||
language_model_selector::{
|
||||
LanguageModelSelector, ToggleModelSelector, language_model_selector,
|
||||
},
|
||||
max_mode_tooltip::MaxModeTooltip,
|
||||
};
|
||||
use agent_settings::{AgentSettings, CompletionMode};
|
||||
use anyhow::Result;
|
||||
@@ -21,7 +21,6 @@ use editor::{
|
||||
BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
|
||||
RenderBlock, ToDisplayPoint,
|
||||
},
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
use editor::{FoldPlaceholder, display_map::CreaseId};
|
||||
use fs::Fs;
|
||||
@@ -69,7 +68,7 @@ use workspace::{
|
||||
searchable::{Direction, SearchableItemHandle},
|
||||
};
|
||||
use workspace::{
|
||||
Save, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
Save, Toast, Workspace,
|
||||
item::{self, FollowableItem, Item, ItemHandle},
|
||||
notifications::NotificationId,
|
||||
pane,
|
||||
@@ -389,7 +388,7 @@ impl TextThreadEditor {
|
||||
cursor..cursor
|
||||
};
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
|
||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||
selections.select_ranges([new_selection])
|
||||
});
|
||||
});
|
||||
@@ -449,8 +448,7 @@ impl TextThreadEditor {
|
||||
if let Some(command) = self.slash_commands.command(name, cx) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor
|
||||
.change_selections(Some(Autoscroll::fit()), window, cx, |s| s.try_cancel());
|
||||
editor.change_selections(Default::default(), window, cx, |s| s.try_cancel());
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let newest_cursor = editor.selections.newest::<Point>(cx).head();
|
||||
if newest_cursor.column > 0
|
||||
@@ -1583,7 +1581,7 @@ impl TextThreadEditor {
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.transact(window, cx, |this, window, cx| {
|
||||
this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
this.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select(selections);
|
||||
});
|
||||
this.insert("", window, cx);
|
||||
@@ -2075,12 +2073,12 @@ impl TextThreadEditor {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let context = self.context().read(cx);
|
||||
let active_model = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.model)?;
|
||||
if !active_model.supports_max_mode() {
|
||||
if !active_model.supports_burn_mode() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -2107,7 +2105,7 @@ impl TextThreadEditor {
|
||||
});
|
||||
}))
|
||||
.tooltip(move |_window, cx| {
|
||||
cx.new(|_| MaxModeTooltip::new().selected(burn_mode_enabled))
|
||||
cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled))
|
||||
.into()
|
||||
})
|
||||
.into_any_element(),
|
||||
@@ -2122,12 +2120,21 @@ impl TextThreadEditor {
|
||||
let active_model = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.model);
|
||||
let focus_handle = self.editor().focus_handle(cx).clone();
|
||||
let model_name = match active_model {
|
||||
Some(model) => model.name().0,
|
||||
None => SharedString::from("No model selected"),
|
||||
};
|
||||
|
||||
let active_provider = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
let provider_icon = match active_provider {
|
||||
Some(provider) => provider.icon(),
|
||||
None => IconName::Ai,
|
||||
};
|
||||
|
||||
let focus_handle = self.editor().focus_handle(cx).clone();
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
@@ -2135,10 +2142,16 @@ impl TextThreadEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Icon::new(provider_icon)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
.ml_0p5(),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
@@ -2575,7 +2588,7 @@ impl Render for TextThreadEditor {
|
||||
};
|
||||
|
||||
let language_model_selector = self.language_model_selector_menu_handle.clone();
|
||||
let max_mode_toggle = self.render_max_mode_toggle(cx);
|
||||
let burn_mode_toggle = self.render_burn_mode_toggle(cx);
|
||||
|
||||
v_flex()
|
||||
.key_context("ContextEditor")
|
||||
@@ -2630,7 +2643,7 @@ impl Render for TextThreadEditor {
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(self.render_inject_context_menu(cx))
|
||||
.when_some(max_mode_toggle, |this, element| this.child(element)),
|
||||
.when_some(burn_mode_toggle, |this, element| this.child(element)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -2924,13 +2937,6 @@ impl FollowableItem for TextThreadEditor {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContextEditorToolbarItem {
|
||||
active_context_editor: Option<WeakEntity<TextThreadEditor>>,
|
||||
model_summary_editor: Entity<Editor>,
|
||||
}
|
||||
|
||||
impl ContextEditorToolbarItem {}
|
||||
|
||||
pub fn render_remaining_tokens(
|
||||
context_editor: &Entity<TextThreadEditor>,
|
||||
cx: &App,
|
||||
@@ -2983,98 +2989,6 @@ pub fn render_remaining_tokens(
|
||||
)
|
||||
}
|
||||
|
||||
impl Render for ContextEditorToolbarItem {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let left_side = h_flex()
|
||||
.group("chat-title-group")
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.flex_grow()
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.when(self.active_context_editor.is_some(), |left_side| {
|
||||
left_side.child(self.model_summary_editor.clone())
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div().visible_on_hover("chat-title-group").child(
|
||||
IconButton::new("regenerate-context", IconName::RefreshTitle)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text("Regenerate Title"))
|
||||
.on_click(cx.listener(move |_, _, _window, cx| {
|
||||
cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
let right_side = h_flex()
|
||||
.gap_2()
|
||||
// TODO display this in a nicer way, once we have a design for it.
|
||||
// .children({
|
||||
// let project = self
|
||||
// .workspace
|
||||
// .upgrade()
|
||||
// .map(|workspace| workspace.read(cx).project().downgrade());
|
||||
//
|
||||
// let scan_items_remaining = cx.update_global(|db: &mut SemanticDb, cx| {
|
||||
// project.and_then(|project| db.remaining_summaries(&project, cx))
|
||||
// });
|
||||
// scan_items_remaining
|
||||
// .map(|remaining_items| format!("Files to scan: {}", remaining_items))
|
||||
// })
|
||||
.children(
|
||||
self.active_context_editor
|
||||
.as_ref()
|
||||
.and_then(|editor| editor.upgrade())
|
||||
.and_then(|editor| render_remaining_tokens(&editor, cx)),
|
||||
);
|
||||
|
||||
h_flex()
|
||||
.px_0p5()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(left_side)
|
||||
.child(right_side)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarItemView for ContextEditorToolbarItem {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
self.active_context_editor = active_pane_item
|
||||
.and_then(|item| item.act_as::<TextThreadEditor>(cx))
|
||||
.map(|editor| editor.downgrade());
|
||||
cx.notify();
|
||||
if self.active_context_editor.is_none() {
|
||||
ToolbarItemLocation::Hidden
|
||||
} else {
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
}
|
||||
}
|
||||
|
||||
fn pane_focus_update(
|
||||
&mut self,
|
||||
_pane_focused: bool,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
|
||||
|
||||
pub enum ContextEditorToolbarItemEvent {
|
||||
RegenerateSummary,
|
||||
}
|
||||
impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
|
||||
|
||||
enum PendingSlashCommand {}
|
||||
|
||||
fn invoked_slash_command_fold_placeholder(
|
||||
@@ -3240,6 +3154,7 @@ pub fn make_lsp_adapter_delegate(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use editor::SelectionEffects;
|
||||
use fs::FakeFs;
|
||||
use gpui::{App, TestAppContext, VisualTestContext};
|
||||
use indoc::indoc;
|
||||
@@ -3465,7 +3380,9 @@ mod tests {
|
||||
) {
|
||||
context_editor.update_in(cx, |context_editor, window, cx| {
|
||||
context_editor.editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([range]));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([range])
|
||||
});
|
||||
});
|
||||
|
||||
context_editor.copy(&Default::default(), window, cx);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
mod agent_notification;
|
||||
mod animated_label;
|
||||
mod burn_mode_tooltip;
|
||||
mod context_pill;
|
||||
mod max_mode_tooltip;
|
||||
mod onboarding_modal;
|
||||
pub mod preview;
|
||||
mod upsell;
|
||||
|
||||
pub use agent_notification::*;
|
||||
pub use animated_label::*;
|
||||
pub use burn_mode_tooltip::*;
|
||||
pub use context_pill::*;
|
||||
pub use max_mode_tooltip::*;
|
||||
pub use onboarding_modal::*;
|
||||
|
||||
@@ -2346,13 +2346,13 @@ impl AssistantContext {
|
||||
completion_request.messages.push(request_message);
|
||||
}
|
||||
}
|
||||
let supports_max_mode = if let Some(model) = model {
|
||||
model.supports_max_mode()
|
||||
let supports_burn_mode = if let Some(model) = model {
|
||||
model.supports_burn_mode()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if supports_max_mode {
|
||||
if supports_burn_mode {
|
||||
completion_request.mode = Some(self.completion_mode.into());
|
||||
}
|
||||
completion_request
|
||||
@@ -2523,6 +2523,12 @@ impl AssistantContext {
|
||||
}
|
||||
|
||||
let message = start_message;
|
||||
let at_end = range.end >= message.offset_range.end.saturating_sub(1);
|
||||
let role_after = if range.start == range.end || at_end {
|
||||
Role::User
|
||||
} else {
|
||||
message.role
|
||||
};
|
||||
let role = message.role;
|
||||
let mut edited_buffer = false;
|
||||
|
||||
@@ -2557,7 +2563,7 @@ impl AssistantContext {
|
||||
};
|
||||
|
||||
let suffix_metadata = MessageMetadata {
|
||||
role,
|
||||
role: role_after,
|
||||
status: MessageStatus::Done,
|
||||
timestamp: suffix.id.0,
|
||||
cache: None,
|
||||
|
||||
@@ -74,7 +74,7 @@ impl SlashCommand for DeltaSlashCommand {
|
||||
.slice(section.range.to_offset(&context_buffer)),
|
||||
);
|
||||
file_command_new_outputs.push(Arc::new(FileSlashCommand).run(
|
||||
&[metadata.path.clone()],
|
||||
std::slice::from_ref(&metadata.path),
|
||||
context_slash_command_output_sections,
|
||||
context_buffer.clone(),
|
||||
workspace.clone(),
|
||||
|
||||
@@ -1470,7 +1470,7 @@ impl EditAgentTest {
|
||||
Project::init_settings(cx);
|
||||
language::init(cx);
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), cx);
|
||||
crate::init(client.http_client(), cx);
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use assistant_tool::{
|
||||
ToolUseStatus,
|
||||
};
|
||||
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
|
||||
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, scroll::Autoscroll};
|
||||
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
|
||||
@@ -823,7 +823,7 @@ impl ToolCard for EditFileToolCard {
|
||||
let first_hunk_start =
|
||||
first_hunk.multi_buffer_range().start;
|
||||
editor.change_selections(
|
||||
Some(Autoscroll::fit()),
|
||||
Default::default(),
|
||||
window,
|
||||
cx,
|
||||
|selections| {
|
||||
@@ -1065,7 +1065,7 @@ fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
||||
|
||||
MarkdownStyle {
|
||||
base_text_style: text_style.clone(),
|
||||
selection_background_color: cx.theme().players().local().selection,
|
||||
selection_background_color: cx.theme().colors().element_selection_background,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,7 +691,7 @@ fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
||||
|
||||
MarkdownStyle {
|
||||
base_text_style: text_style.clone(),
|
||||
selection_background_color: cx.theme().players().local().selection,
|
||||
selection_background_color: cx.theme().colors().element_selection_background,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use auto_update::AutoUpdater;
|
||||
use client::proto::UpdateNotification;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{App, Context, DismissEvent, Entity, SharedString, Window, actions, prelude::*};
|
||||
use gpui::{App, Context, DismissEvent, Entity, Window, actions, prelude::*};
|
||||
use http_client::HttpClient;
|
||||
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
@@ -94,7 +94,6 @@ fn view_release_notes_locally(
|
||||
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let tab_content = Some(SharedString::from(body.title.to_string()));
|
||||
let editor = cx.new(|cx| {
|
||||
Editor::for_multibuffer(buffer, Some(project), window, cx)
|
||||
});
|
||||
@@ -105,7 +104,6 @@ fn view_release_notes_locally(
|
||||
editor,
|
||||
workspace_handle,
|
||||
language_registry,
|
||||
tab_content,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -132,6 +130,11 @@ pub fn notify_if_app_was_updated(cx: &mut App) {
|
||||
let Some(updater) = AutoUpdater::get(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let ReleaseChannel::Nightly = ReleaseChannel::global(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
let should_show_notification = should_show_notification.await?;
|
||||
|
||||
@@ -1867,7 +1867,7 @@ mod tests {
|
||||
let hunk = diff.hunks(&buffer, cx).next().unwrap();
|
||||
|
||||
let new_index_text = diff
|
||||
.stage_or_unstage_hunks(true, &[hunk.clone()], &buffer, true, cx)
|
||||
.stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert_eq!(new_index_text, buffer_text);
|
||||
|
||||
@@ -734,8 +734,8 @@ impl Database {
|
||||
users.push(proto::User {
|
||||
id: user.id.to_proto(),
|
||||
avatar_url: format!(
|
||||
"https://github.com/{}.png?size=128",
|
||||
user.github_login
|
||||
"https://avatars.githubusercontent.com/u/{}?s=128&v=4",
|
||||
user.github_user_id
|
||||
),
|
||||
github_login: user.github_login,
|
||||
name: user.name,
|
||||
|
||||
@@ -76,7 +76,10 @@ async fn test_purge_old_embeddings(cx: &mut gpui::TestAppContext) {
|
||||
db.purge_old_embeddings().await.unwrap();
|
||||
|
||||
// Try to retrieve the purged embeddings
|
||||
let retrieved_embeddings = db.get_embeddings(model, &[digest.clone()]).await.unwrap();
|
||||
let retrieved_embeddings = db
|
||||
.get_embeddings(model, std::slice::from_ref(&digest))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
retrieved_embeddings.is_empty(),
|
||||
"Old embeddings should have been purged"
|
||||
|
||||
@@ -179,7 +179,7 @@ struct Session {
|
||||
}
|
||||
|
||||
impl Session {
|
||||
async fn db(&self) -> tokio::sync::MutexGuard<DbHandle> {
|
||||
async fn db(&self) -> tokio::sync::MutexGuard<'_, DbHandle> {
|
||||
#[cfg(test)]
|
||||
tokio::task::yield_now().await;
|
||||
let guard = self.db.lock().await;
|
||||
@@ -1037,7 +1037,7 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn snapshot(self: &Arc<Self>) -> ServerSnapshot {
|
||||
pub async fn snapshot(self: &Arc<Self>) -> ServerSnapshot<'_> {
|
||||
ServerSnapshot {
|
||||
connection_pool: ConnectionPoolGuard {
|
||||
guard: self.connection_pool.lock(),
|
||||
@@ -2008,6 +2008,7 @@ async fn join_project(
|
||||
session.connection_id,
|
||||
proto::UpdateLanguageServer {
|
||||
project_id: project_id.to_proto(),
|
||||
server_name: Some(language_server.name.clone()),
|
||||
language_server_id: language_server.id,
|
||||
variant: Some(
|
||||
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
|
||||
|
||||
@@ -16,9 +16,9 @@ use crate::stripe_client::{
|
||||
StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams,
|
||||
StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
|
||||
StripeCreateMeterEventPayload, StripeCreateSubscriptionItems, StripeCreateSubscriptionParams,
|
||||
StripeCustomerId, StripeMeter, StripePrice, StripePriceId, StripeSubscription,
|
||||
StripeSubscriptionId, StripeSubscriptionTrialSettings,
|
||||
StripeSubscriptionTrialSettingsEndBehavior,
|
||||
StripeCustomerId, StripeCustomerUpdate, StripeCustomerUpdateAddress, StripeCustomerUpdateName,
|
||||
StripeMeter, StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId,
|
||||
StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior,
|
||||
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionItems,
|
||||
UpdateSubscriptionParams,
|
||||
};
|
||||
@@ -247,6 +247,11 @@ impl StripeBilling {
|
||||
}]);
|
||||
params.success_url = Some(success_url);
|
||||
params.billing_address_collection = Some(StripeBillingAddressCollection::Required);
|
||||
params.customer_update = Some(StripeCustomerUpdate {
|
||||
address: Some(StripeCustomerUpdateAddress::Auto),
|
||||
name: Some(StripeCustomerUpdateName::Auto),
|
||||
shipping: None,
|
||||
});
|
||||
|
||||
let session = self.client.create_checkout_session(params).await?;
|
||||
Ok(session.url.context("no checkout session URL")?)
|
||||
@@ -301,6 +306,11 @@ impl StripeBilling {
|
||||
}]);
|
||||
params.success_url = Some(success_url);
|
||||
params.billing_address_collection = Some(StripeBillingAddressCollection::Required);
|
||||
params.customer_update = Some(StripeCustomerUpdate {
|
||||
address: Some(StripeCustomerUpdateAddress::Auto),
|
||||
name: Some(StripeCustomerUpdateName::Auto),
|
||||
shipping: None,
|
||||
});
|
||||
|
||||
let session = self.client.create_checkout_session(params).await?;
|
||||
Ok(session.url.context("no checkout session URL")?)
|
||||
|
||||
@@ -154,6 +154,31 @@ pub enum StripeBillingAddressCollection {
|
||||
Required,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct StripeCustomerUpdate {
|
||||
pub address: Option<StripeCustomerUpdateAddress>,
|
||||
pub name: Option<StripeCustomerUpdateName>,
|
||||
pub shipping: Option<StripeCustomerUpdateShipping>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum StripeCustomerUpdateAddress {
|
||||
Auto,
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum StripeCustomerUpdateName {
|
||||
Auto,
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum StripeCustomerUpdateShipping {
|
||||
Auto,
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StripeCreateCheckoutSessionParams<'a> {
|
||||
pub customer: Option<&'a StripeCustomerId>,
|
||||
@@ -164,6 +189,7 @@ pub struct StripeCreateCheckoutSessionParams<'a> {
|
||||
pub subscription_data: Option<StripeCreateCheckoutSessionSubscriptionData>,
|
||||
pub success_url: Option<&'a str>,
|
||||
pub billing_address_collection: Option<StripeBillingAddressCollection>,
|
||||
pub customer_update: Option<StripeCustomerUpdate>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
|
||||
@@ -12,9 +12,10 @@ use crate::stripe_client::{
|
||||
StripeCheckoutSessionMode, StripeCheckoutSessionPaymentMethodCollection, StripeClient,
|
||||
StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams,
|
||||
StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
|
||||
StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeMeter, StripeMeterId,
|
||||
StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId, StripeSubscriptionItem,
|
||||
StripeSubscriptionItemId, UpdateCustomerParams, UpdateSubscriptionParams,
|
||||
StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate,
|
||||
StripeMeter, StripeMeterId, StripePrice, StripePriceId, StripeSubscription,
|
||||
StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, UpdateCustomerParams,
|
||||
UpdateSubscriptionParams,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -36,6 +37,7 @@ pub struct StripeCreateCheckoutSessionCall {
|
||||
pub subscription_data: Option<StripeCreateCheckoutSessionSubscriptionData>,
|
||||
pub success_url: Option<String>,
|
||||
pub billing_address_collection: Option<StripeBillingAddressCollection>,
|
||||
pub customer_update: Option<StripeCustomerUpdate>,
|
||||
}
|
||||
|
||||
pub struct FakeStripeClient {
|
||||
@@ -233,6 +235,7 @@ impl StripeClient for FakeStripeClient {
|
||||
subscription_data: params.subscription_data,
|
||||
success_url: params.success_url.map(|url| url.to_string()),
|
||||
billing_address_collection: params.billing_address_collection,
|
||||
customer_update: params.customer_update,
|
||||
});
|
||||
|
||||
Ok(StripeCheckoutSession {
|
||||
|
||||
@@ -22,10 +22,11 @@ use crate::stripe_client::{
|
||||
StripeCheckoutSessionPaymentMethodCollection, StripeClient,
|
||||
StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams,
|
||||
StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
|
||||
StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeMeter, StripePrice,
|
||||
StripePriceId, StripePriceRecurring, StripeSubscription, StripeSubscriptionId,
|
||||
StripeSubscriptionItem, StripeSubscriptionItemId, StripeSubscriptionTrialSettings,
|
||||
StripeSubscriptionTrialSettingsEndBehavior,
|
||||
StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate,
|
||||
StripeCustomerUpdateAddress, StripeCustomerUpdateName, StripeCustomerUpdateShipping,
|
||||
StripeMeter, StripePrice, StripePriceId, StripePriceRecurring, StripeSubscription,
|
||||
StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId,
|
||||
StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior,
|
||||
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateCustomerParams,
|
||||
UpdateSubscriptionParams,
|
||||
};
|
||||
@@ -446,6 +447,7 @@ impl<'a> TryFrom<StripeCreateCheckoutSessionParams<'a>> for CreateCheckoutSessio
|
||||
subscription_data: value.subscription_data.map(Into::into),
|
||||
success_url: value.success_url,
|
||||
billing_address_collection: value.billing_address_collection.map(Into::into),
|
||||
customer_update: value.customer_update.map(Into::into),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -541,3 +543,50 @@ impl From<StripeBillingAddressCollection> for stripe::CheckoutSessionBillingAddr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StripeCustomerUpdateAddress> for stripe::CreateCheckoutSessionCustomerUpdateAddress {
|
||||
fn from(value: StripeCustomerUpdateAddress) -> Self {
|
||||
match value {
|
||||
StripeCustomerUpdateAddress::Auto => {
|
||||
stripe::CreateCheckoutSessionCustomerUpdateAddress::Auto
|
||||
}
|
||||
StripeCustomerUpdateAddress::Never => {
|
||||
stripe::CreateCheckoutSessionCustomerUpdateAddress::Never
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StripeCustomerUpdateName> for stripe::CreateCheckoutSessionCustomerUpdateName {
|
||||
fn from(value: StripeCustomerUpdateName) -> Self {
|
||||
match value {
|
||||
StripeCustomerUpdateName::Auto => stripe::CreateCheckoutSessionCustomerUpdateName::Auto,
|
||||
StripeCustomerUpdateName::Never => {
|
||||
stripe::CreateCheckoutSessionCustomerUpdateName::Never
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StripeCustomerUpdateShipping> for stripe::CreateCheckoutSessionCustomerUpdateShipping {
|
||||
fn from(value: StripeCustomerUpdateShipping) -> Self {
|
||||
match value {
|
||||
StripeCustomerUpdateShipping::Auto => {
|
||||
stripe::CreateCheckoutSessionCustomerUpdateShipping::Auto
|
||||
}
|
||||
StripeCustomerUpdateShipping::Never => {
|
||||
stripe::CreateCheckoutSessionCustomerUpdateShipping::Never
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StripeCustomerUpdate> for stripe::CreateCheckoutSessionCustomerUpdate {
|
||||
fn from(value: StripeCustomerUpdate) -> Self {
|
||||
stripe::CreateCheckoutSessionCustomerUpdate {
|
||||
address: value.address.map(Into::into),
|
||||
name: value.name.map(Into::into),
|
||||
shipping: value.shipping.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ async fn test_channel_notes_participant_indices(
|
||||
channel_view_a.update_in(cx_a, |notes, window, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
editor.insert("a", window, cx);
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||
selections.select_ranges(vec![0..1]);
|
||||
});
|
||||
});
|
||||
@@ -188,7 +188,7 @@ async fn test_channel_notes_participant_indices(
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
editor.move_down(&Default::default(), window, cx);
|
||||
editor.insert("b", window, cx);
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||
selections.select_ranges(vec![1..2]);
|
||||
});
|
||||
});
|
||||
@@ -198,7 +198,7 @@ async fn test_channel_notes_participant_indices(
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
editor.move_down(&Default::default(), window, cx);
|
||||
editor.insert("c", window, cx);
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||
selections.select_ranges(vec![2..3]);
|
||||
});
|
||||
});
|
||||
@@ -273,12 +273,12 @@ async fn test_channel_notes_participant_indices(
|
||||
.unwrap();
|
||||
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||
selections.select_ranges(vec![0..1]);
|
||||
});
|
||||
});
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||
selections.select_ranges(vec![2..3]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
};
|
||||
use call::ActiveCall;
|
||||
use editor::{
|
||||
DocumentColorsRenderMode, Editor, EditorSettings, RowInfo,
|
||||
DocumentColorsRenderMode, Editor, EditorSettings, RowInfo, SelectionEffects,
|
||||
actions::{
|
||||
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
|
||||
ExpandMacroRecursively, MoveToEnd, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
|
||||
@@ -348,7 +348,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
|
||||
// Type a completion trigger character as the guest.
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([13..13])
|
||||
});
|
||||
editor.handle_input(".", window, cx);
|
||||
});
|
||||
cx_b.focus(&editor_b);
|
||||
@@ -461,7 +463,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
// Now we do a second completion, this time to ensure that documentation/snippets are
|
||||
// resolved
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([46..46]));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([46..46])
|
||||
});
|
||||
editor.handle_input("; a", window, cx);
|
||||
editor.handle_input(".", window, cx);
|
||||
});
|
||||
@@ -613,7 +617,7 @@ async fn test_collaborating_with_code_actions(
|
||||
|
||||
// Move cursor to a location that contains code actions.
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
|
||||
});
|
||||
});
|
||||
@@ -817,7 +821,9 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
|
||||
// Move cursor to a location that can be renamed.
|
||||
let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([7..7]));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([7..7])
|
||||
});
|
||||
editor.rename(&Rename, window, cx).unwrap()
|
||||
});
|
||||
|
||||
@@ -863,7 +869,9 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
editor.cancel(&editor::actions::Cancel, window, cx);
|
||||
});
|
||||
let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([7..8]));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([7..8])
|
||||
});
|
||||
editor.rename(&Rename, window, cx).unwrap()
|
||||
});
|
||||
|
||||
@@ -1364,7 +1372,9 @@ async fn test_on_input_format_from_host_to_guest(
|
||||
// Type a on type formatting trigger character as the guest.
|
||||
cx_a.focus(&editor_a);
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([13..13])
|
||||
});
|
||||
editor.handle_input(">", window, cx);
|
||||
});
|
||||
|
||||
@@ -1460,7 +1470,9 @@ async fn test_on_input_format_from_guest_to_host(
|
||||
// Type a on type formatting trigger character as the guest.
|
||||
cx_b.focus(&editor_b);
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([13..13])
|
||||
});
|
||||
editor.handle_input(":", window, cx);
|
||||
});
|
||||
|
||||
@@ -1697,7 +1709,9 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
|
||||
let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([13..13].clone())
|
||||
});
|
||||
editor.handle_input(":", window, cx);
|
||||
});
|
||||
cx_b.focus(&editor_b);
|
||||
@@ -1718,7 +1732,9 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
|
||||
let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([13..13])
|
||||
});
|
||||
editor.handle_input("a change to increment both buffers' versions", window, cx);
|
||||
});
|
||||
cx_a.focus(&editor_a);
|
||||
@@ -2121,7 +2137,9 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
|
||||
});
|
||||
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([13..13].clone())
|
||||
});
|
||||
editor.handle_input(":", window, cx);
|
||||
});
|
||||
color_request_handle.next().await.unwrap();
|
||||
|
||||
@@ -6,7 +6,7 @@ use collab_ui::{
|
||||
channel_view::ChannelView,
|
||||
notifications::project_shared_notification::ProjectSharedNotification,
|
||||
};
|
||||
use editor::{Editor, MultiBuffer, PathKey};
|
||||
use editor::{Editor, MultiBuffer, PathKey, SelectionEffects};
|
||||
use gpui::{
|
||||
AppContext as _, BackgroundExecutor, BorrowAppContext, Entity, SharedString, TestAppContext,
|
||||
VisualContext, VisualTestContext, point,
|
||||
@@ -376,7 +376,9 @@ async fn test_basic_following(
|
||||
|
||||
// Changes to client A's editor are reflected on client B.
|
||||
editor_a1.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2]));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([1..1, 2..2])
|
||||
});
|
||||
});
|
||||
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
executor.run_until_parked();
|
||||
@@ -393,7 +395,9 @@ async fn test_basic_following(
|
||||
editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
|
||||
|
||||
editor_a1.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([3..3])
|
||||
});
|
||||
editor.set_scroll_position(point(0., 100.), window, cx);
|
||||
});
|
||||
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
@@ -1647,7 +1651,9 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
|
||||
// b should follow a to position 1
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]))
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([1..1])
|
||||
})
|
||||
});
|
||||
cx_a.executor()
|
||||
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
@@ -1667,7 +1673,9 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
|
||||
// b should not follow a to position 2
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]))
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([2..2])
|
||||
})
|
||||
});
|
||||
cx_a.executor()
|
||||
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
@@ -1968,7 +1976,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
|
||||
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
editor.insert("Hello from A.", window, cx);
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
|
||||
selections.select_ranges(vec![3..4]);
|
||||
});
|
||||
});
|
||||
@@ -2109,7 +2117,7 @@ async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
workspace.add_item_to_center(Box::new(editor.clone()) as _, window, cx)
|
||||
});
|
||||
editor.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::row_range(4..4)]);
|
||||
})
|
||||
});
|
||||
|
||||
@@ -8,8 +8,9 @@ use crate::stripe_billing::StripeBilling;
|
||||
use crate::stripe_client::{
|
||||
FakeStripeClient, StripeBillingAddressCollection, StripeCheckoutSessionMode,
|
||||
StripeCheckoutSessionPaymentMethodCollection, StripeCreateCheckoutSessionLineItems,
|
||||
StripeCreateCheckoutSessionSubscriptionData, StripeCustomerId, StripeMeter, StripeMeterId,
|
||||
StripePrice, StripePriceId, StripePriceRecurring, StripeSubscription, StripeSubscriptionId,
|
||||
StripeCreateCheckoutSessionSubscriptionData, StripeCustomerId, StripeCustomerUpdate,
|
||||
StripeCustomerUpdateAddress, StripeCustomerUpdateName, StripeMeter, StripeMeterId, StripePrice,
|
||||
StripePriceId, StripePriceRecurring, StripeSubscription, StripeSubscriptionId,
|
||||
StripeSubscriptionItem, StripeSubscriptionItemId, StripeSubscriptionTrialSettings,
|
||||
StripeSubscriptionTrialSettingsEndBehavior,
|
||||
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionItems,
|
||||
@@ -431,6 +432,14 @@ async fn test_checkout_with_zed_pro() {
|
||||
call.billing_address_collection,
|
||||
Some(StripeBillingAddressCollection::Required)
|
||||
);
|
||||
assert_eq!(
|
||||
call.customer_update,
|
||||
Some(StripeCustomerUpdate {
|
||||
address: Some(StripeCustomerUpdateAddress::Auto),
|
||||
name: Some(StripeCustomerUpdateName::Auto),
|
||||
shipping: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,6 +525,14 @@ async fn test_checkout_with_zed_pro_trial() {
|
||||
call.billing_address_collection,
|
||||
Some(StripeBillingAddressCollection::Required)
|
||||
);
|
||||
assert_eq!(
|
||||
call.customer_update,
|
||||
Some(StripeCustomerUpdate {
|
||||
address: Some(StripeCustomerUpdateAddress::Auto),
|
||||
name: Some(StripeCustomerUpdateName::Auto),
|
||||
shipping: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Successful checkout with extended trial.
|
||||
@@ -574,5 +591,13 @@ async fn test_checkout_with_zed_pro_trial() {
|
||||
call.billing_address_collection,
|
||||
Some(StripeBillingAddressCollection::Required)
|
||||
);
|
||||
assert_eq!(
|
||||
call.customer_update,
|
||||
Some(StripeCustomerUpdate {
|
||||
address: Some(StripeCustomerUpdateAddress::Auto),
|
||||
name: Some(StripeCustomerUpdateName::Auto),
|
||||
shipping: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ use client::{
|
||||
};
|
||||
use collections::HashMap;
|
||||
use editor::{
|
||||
CollaborationHub, DisplayPoint, Editor, EditorEvent, display_map::ToDisplayPoint,
|
||||
scroll::Autoscroll,
|
||||
CollaborationHub, DisplayPoint, Editor, EditorEvent, SelectionEffects,
|
||||
display_map::ToDisplayPoint, scroll::Autoscroll,
|
||||
};
|
||||
use gpui::{
|
||||
AnyView, App, ClipboardItem, Context, Entity, EventEmitter, Focusable, Pixels, Point, Render,
|
||||
@@ -260,9 +260,16 @@ impl ChannelView {
|
||||
.find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
|
||||
{
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::focused()), window, cx, |s| {
|
||||
s.replace_cursors_with(|map| vec![item.range.start.to_display_point(map)])
|
||||
})
|
||||
editor.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::focused()),
|
||||
window,
|
||||
cx,
|
||||
|s| {
|
||||
s.replace_cursors_with(|map| {
|
||||
vec![item.range.start.to_display_point(map)]
|
||||
})
|
||||
},
|
||||
)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -303,6 +303,7 @@ pub enum ComponentScope {
|
||||
Collaboration,
|
||||
#[strum(serialize = "Data Display")]
|
||||
DataDisplay,
|
||||
Debugger,
|
||||
Editor,
|
||||
#[strum(serialize = "Images & Icons")]
|
||||
Images,
|
||||
|
||||
@@ -698,16 +698,16 @@ async fn stream_completion(
|
||||
completion_url: Arc<str>,
|
||||
request: Request,
|
||||
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
|
||||
let is_vision_request = request.messages.last().map_or(false, |message| match message {
|
||||
ChatMessage::User { content }
|
||||
| ChatMessage::Assistant { content, .. }
|
||||
| ChatMessage::Tool { content, .. } => {
|
||||
matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
let is_vision_request = request.messages.iter().any(|message| match message {
|
||||
ChatMessage::User { content }
|
||||
| ChatMessage::Assistant { content, .. }
|
||||
| ChatMessage::Tool { content, .. } => {
|
||||
matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
||||
let request_builder = HttpRequest::builder()
|
||||
let mut request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(completion_url.as_ref())
|
||||
.header(
|
||||
@@ -719,8 +719,12 @@ async fn stream_completion(
|
||||
)
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Copilot-Integration-Id", "vscode-chat")
|
||||
.header("Copilot-Vision-Request", is_vision_request.to_string());
|
||||
.header("Copilot-Integration-Id", "vscode-chat");
|
||||
|
||||
if is_vision_request {
|
||||
request_builder =
|
||||
request_builder.header("Copilot-Vision-Request", is_vision_request.to_string());
|
||||
}
|
||||
|
||||
let is_streaming = request.stream;
|
||||
|
||||
|
||||
@@ -264,7 +264,8 @@ fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b:
|
||||
mod tests {
|
||||
use super::*;
|
||||
use editor::{
|
||||
Editor, ExcerptRange, MultiBuffer, test::editor_lsp_test_context::EditorLspTestContext,
|
||||
Editor, ExcerptRange, MultiBuffer, SelectionEffects,
|
||||
test::editor_lsp_test_context::EditorLspTestContext,
|
||||
};
|
||||
use fs::FakeFs;
|
||||
use futures::StreamExt;
|
||||
@@ -478,7 +479,7 @@ mod tests {
|
||||
// Reset the editor to verify how suggestions behave when tabbing on leading indentation.
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.set_text("fn foo() {\n \n}", window, cx);
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
|
||||
});
|
||||
});
|
||||
@@ -767,7 +768,7 @@ mod tests {
|
||||
);
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
// Ensure copilot suggestions are shown for the first excerpt.
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
|
||||
});
|
||||
editor.next_edit_prediction(&Default::default(), window, cx);
|
||||
@@ -793,7 +794,7 @@ mod tests {
|
||||
);
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
// Move to another excerpt, ensuring the suggestion gets cleared.
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
|
||||
});
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
@@ -1019,7 +1020,7 @@ mod tests {
|
||||
);
|
||||
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
|
||||
selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
|
||||
});
|
||||
editor.refresh_inline_completion(true, false, window, cx);
|
||||
@@ -1029,7 +1030,7 @@ mod tests {
|
||||
assert!(copilot_requests.try_next().is_err());
|
||||
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(5, 0)..Point::new(5, 0)])
|
||||
});
|
||||
editor.refresh_inline_completion(true, false, window, cx);
|
||||
|
||||
@@ -33,6 +33,7 @@ log.workspace = true
|
||||
paths.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
shlex.workspace = true
|
||||
task.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -5,7 +5,7 @@ use gpui::AsyncApp;
|
||||
use serde_json::Value;
|
||||
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
|
||||
use task::DebugRequest;
|
||||
use util::ResultExt;
|
||||
use util::{ResultExt, maybe};
|
||||
|
||||
use crate::*;
|
||||
|
||||
@@ -72,6 +72,24 @@ impl JsDebugAdapter {
|
||||
|
||||
let mut configuration = task_definition.config.clone();
|
||||
if let Some(configuration) = configuration.as_object_mut() {
|
||||
maybe!({
|
||||
configuration
|
||||
.get("type")
|
||||
.filter(|value| value == &"node-terminal")?;
|
||||
let command = configuration.get("command")?.as_str()?.to_owned();
|
||||
let mut args = shlex::split(&command)?.into_iter();
|
||||
let program = args.next()?;
|
||||
configuration.insert("program".to_owned(), program.into());
|
||||
configuration.insert(
|
||||
"args".to_owned(),
|
||||
args.map(Value::from).collect::<Vec<_>>().into(),
|
||||
);
|
||||
configuration.insert("console".to_owned(), "externalTerminal".into());
|
||||
Some(())
|
||||
});
|
||||
|
||||
configuration.entry("type").and_modify(normalize_task_type);
|
||||
|
||||
if let Some(program) = configuration
|
||||
.get("program")
|
||||
.cloned()
|
||||
@@ -96,7 +114,6 @@ impl JsDebugAdapter {
|
||||
.entry("cwd")
|
||||
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
|
||||
|
||||
configuration.entry("type").and_modify(normalize_task_type);
|
||||
configuration
|
||||
.entry("console")
|
||||
.or_insert("externalTerminal".into());
|
||||
@@ -512,7 +529,7 @@ fn normalize_task_type(task_type: &mut Value) {
|
||||
};
|
||||
|
||||
let new_name = match task_type_str {
|
||||
"node" | "pwa-node" => "pwa-node",
|
||||
"node" | "pwa-node" | "node-terminal" => "pwa-node",
|
||||
"chrome" | "pwa-chrome" => "pwa-chrome",
|
||||
"edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
|
||||
_ => task_type_str,
|
||||
|
||||
@@ -28,13 +28,18 @@ test-support = [
|
||||
[dependencies]
|
||||
alacritty_terminal.workspace = true
|
||||
anyhow.workspace = true
|
||||
bitflags.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
dap.workspace = true
|
||||
dap_adapters = { workspace = true, optional = true }
|
||||
db.workspace = true
|
||||
debugger_tools = { workspace = true, optional = true }
|
||||
editor.workspace = true
|
||||
env_logger = { workspace = true, optional = true }
|
||||
feature_flags.workspace = true
|
||||
file_icons.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
@@ -63,11 +68,10 @@ theme.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
tree-sitter-json.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
debugger_tools = { workspace = true, optional = true }
|
||||
unindent = { workspace = true, optional = true }
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -100,7 +100,13 @@ impl DebugPanel {
|
||||
sessions: vec![],
|
||||
active_session: None,
|
||||
focus_handle,
|
||||
breakpoint_list: BreakpointList::new(None, workspace.weak_handle(), &project, cx),
|
||||
breakpoint_list: BreakpointList::new(
|
||||
None,
|
||||
workspace.weak_handle(),
|
||||
&project,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
project,
|
||||
workspace: workspace.weak_handle(),
|
||||
context_menu: None,
|
||||
|
||||
@@ -697,8 +697,13 @@ impl RunningState {
|
||||
)
|
||||
});
|
||||
|
||||
let breakpoint_list =
|
||||
BreakpointList::new(Some(session.clone()), workspace.clone(), &project, cx);
|
||||
let breakpoint_list = BreakpointList::new(
|
||||
Some(session.clone()),
|
||||
workspace.clone(),
|
||||
&project,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let _subscriptions = vec![
|
||||
cx.on_app_quit(move |this, cx| {
|
||||
|
||||
@@ -5,11 +5,11 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use dap::ExceptionBreakpointsFilter;
|
||||
use dap::{Capabilities, ExceptionBreakpointsFilter};
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
Action, AppContext, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful,
|
||||
Task, UniformListScrollHandle, WeakEntity, uniform_list,
|
||||
Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
|
||||
Stateful, Task, UniformListScrollHandle, WeakEntity, actions, uniform_list,
|
||||
};
|
||||
use language::Point;
|
||||
use project::{
|
||||
@@ -21,16 +21,20 @@ use project::{
|
||||
worktree_store::WorktreeStore,
|
||||
};
|
||||
use ui::{
|
||||
AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div, FluentBuilder as _,
|
||||
Icon, IconButton, IconName, IconSize, Indicator, InteractiveElement, IntoElement, Label,
|
||||
LabelCommon, LabelSize, ListItem, ParentElement, Render, Scrollbar, ScrollbarState,
|
||||
SharedString, StatefulInteractiveElement, Styled, Toggleable, Tooltip, Window, div, h_flex, px,
|
||||
v_flex,
|
||||
ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div,
|
||||
Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, Indicator,
|
||||
InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement,
|
||||
Render, RenderOnce, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
|
||||
Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
|
||||
|
||||
actions!(
|
||||
debugger,
|
||||
[PreviousBreakpointProperty, NextBreakpointProperty]
|
||||
);
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(crate) enum SelectedBreakpointKind {
|
||||
Source,
|
||||
@@ -48,6 +52,8 @@ pub(crate) struct BreakpointList {
|
||||
focus_handle: FocusHandle,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_ix: Option<usize>,
|
||||
input: Entity<Editor>,
|
||||
strip_mode: Option<ActiveBreakpointStripMode>,
|
||||
}
|
||||
|
||||
impl Focusable for BreakpointList {
|
||||
@@ -56,11 +62,19 @@ impl Focusable for BreakpointList {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum ActiveBreakpointStripMode {
|
||||
Log,
|
||||
Condition,
|
||||
HitCondition,
|
||||
}
|
||||
|
||||
impl BreakpointList {
|
||||
pub(crate) fn new(
|
||||
session: Option<Entity<Session>>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: &Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let project = project.read(cx);
|
||||
@@ -70,7 +84,7 @@ impl BreakpointList {
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
cx.new(|_| Self {
|
||||
cx.new(|cx| Self {
|
||||
breakpoint_store,
|
||||
worktree_store,
|
||||
scrollbar_state,
|
||||
@@ -82,17 +96,28 @@ impl BreakpointList {
|
||||
focus_handle,
|
||||
scroll_handle,
|
||||
selected_ix: None,
|
||||
input: cx.new(|cx| Editor::single_line(window, cx)),
|
||||
strip_mode: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn edit_line_breakpoint(
|
||||
&mut self,
|
||||
&self,
|
||||
path: Arc<Path>,
|
||||
row: u32,
|
||||
action: BreakpointEditAction,
|
||||
cx: &mut Context<Self>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.breakpoint_store.update(cx, |breakpoint_store, cx| {
|
||||
Self::edit_line_breakpoint_inner(&self.breakpoint_store, path, row, action, cx);
|
||||
}
|
||||
fn edit_line_breakpoint_inner(
|
||||
breakpoint_store: &Entity<BreakpointStore>,
|
||||
path: Arc<Path>,
|
||||
row: u32,
|
||||
action: BreakpointEditAction,
|
||||
cx: &mut App,
|
||||
) {
|
||||
breakpoint_store.update(cx, |breakpoint_store, cx| {
|
||||
if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) {
|
||||
breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx);
|
||||
} else {
|
||||
@@ -148,16 +173,63 @@ impl BreakpointList {
|
||||
})
|
||||
}
|
||||
|
||||
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
|
||||
fn set_active_breakpoint_property(
|
||||
&mut self,
|
||||
prop: ActiveBreakpointStripMode,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.strip_mode = Some(prop);
|
||||
let placeholder = match prop {
|
||||
ActiveBreakpointStripMode::Log => "Set Log Message",
|
||||
ActiveBreakpointStripMode::Condition => "Set Condition",
|
||||
ActiveBreakpointStripMode::HitCondition => "Set Hit Condition",
|
||||
};
|
||||
let mut is_exception_breakpoint = true;
|
||||
let active_value = self.selected_ix.and_then(|ix| {
|
||||
self.breakpoints.get(ix).and_then(|bp| {
|
||||
if let BreakpointEntryKind::LineBreakpoint(bp) = &bp.kind {
|
||||
is_exception_breakpoint = false;
|
||||
match prop {
|
||||
ActiveBreakpointStripMode::Log => bp.breakpoint.message.clone(),
|
||||
ActiveBreakpointStripMode::Condition => bp.breakpoint.condition.clone(),
|
||||
ActiveBreakpointStripMode::HitCondition => {
|
||||
bp.breakpoint.hit_condition.clone()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
self.input.update(cx, |this, cx| {
|
||||
this.set_placeholder_text(placeholder, cx);
|
||||
this.set_read_only(is_exception_breakpoint);
|
||||
this.set_text(active_value.as_deref().unwrap_or(""), window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn select_ix(&mut self, ix: Option<usize>, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.selected_ix = ix;
|
||||
if let Some(ix) = ix {
|
||||
self.scroll_handle
|
||||
.scroll_to_item(ix, ScrollStrategy::Center);
|
||||
}
|
||||
if let Some(mode) = self.strip_mode {
|
||||
self.set_active_breakpoint_property(mode, window, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.strip_mode.is_some() {
|
||||
if self.input.focus_handle(cx).contains_focused(window, cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
let ix = match self.selected_ix {
|
||||
_ if self.breakpoints.len() == 0 => None,
|
||||
None => Some(0),
|
||||
@@ -169,15 +241,21 @@ impl BreakpointList {
|
||||
}
|
||||
}
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
self.select_ix(ix, window, cx);
|
||||
}
|
||||
|
||||
fn select_previous(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
_window: &mut Window,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.strip_mode.is_some() {
|
||||
if self.input.focus_handle(cx).contains_focused(window, cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
let ix = match self.selected_ix {
|
||||
_ if self.breakpoints.len() == 0 => None,
|
||||
None => Some(self.breakpoints.len() - 1),
|
||||
@@ -189,37 +267,105 @@ impl BreakpointList {
|
||||
}
|
||||
}
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
self.select_ix(ix, window, cx);
|
||||
}
|
||||
|
||||
fn select_first(
|
||||
&mut self,
|
||||
_: &menu::SelectFirst,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.strip_mode.is_some() {
|
||||
if self.input.focus_handle(cx).contains_focused(window, cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
let ix = if self.breakpoints.len() > 0 {
|
||||
Some(0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
self.select_ix(ix, window, cx);
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.strip_mode.is_some() {
|
||||
if self.input.focus_handle(cx).contains_focused(window, cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
let ix = if self.breakpoints.len() > 0 {
|
||||
Some(self.breakpoints.len() - 1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
self.select_ix(ix, window, cx);
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.input.focus_handle(cx).contains_focused(window, cx) {
|
||||
self.focus_handle.focus(window);
|
||||
} else if self.strip_mode.is_some() {
|
||||
self.strip_mode.take();
|
||||
cx.notify();
|
||||
} else {
|
||||
cx.propagate();
|
||||
}
|
||||
}
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(mode) = self.strip_mode {
|
||||
let handle = self.input.focus_handle(cx);
|
||||
if handle.is_focused(window) {
|
||||
// Go back to the main strip. Save the result as well.
|
||||
let text = self.input.read(cx).text(cx);
|
||||
|
||||
match mode {
|
||||
ActiveBreakpointStripMode::Log => match &entry.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
Self::edit_line_breakpoint_inner(
|
||||
&self.breakpoint_store,
|
||||
line_breakpoint.breakpoint.path.clone(),
|
||||
line_breakpoint.breakpoint.row,
|
||||
BreakpointEditAction::EditLogMessage(Arc::from(text)),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ActiveBreakpointStripMode::Condition => match &entry.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
Self::edit_line_breakpoint_inner(
|
||||
&self.breakpoint_store,
|
||||
line_breakpoint.breakpoint.path.clone(),
|
||||
line_breakpoint.breakpoint.row,
|
||||
BreakpointEditAction::EditCondition(Arc::from(text)),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ActiveBreakpointStripMode::HitCondition => match &entry.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
Self::edit_line_breakpoint_inner(
|
||||
&self.breakpoint_store,
|
||||
line_breakpoint.breakpoint.path.clone(),
|
||||
line_breakpoint.breakpoint.row,
|
||||
BreakpointEditAction::EditHitCondition(Arc::from(text)),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
self.focus_handle.focus(window);
|
||||
} else {
|
||||
handle.focus(window);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
match &mut entry.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
let path = line_breakpoint.breakpoint.path.clone();
|
||||
@@ -233,12 +379,18 @@ impl BreakpointList {
|
||||
fn toggle_enable_breakpoint(
|
||||
&mut self,
|
||||
_: &ToggleEnableBreakpoint,
|
||||
_window: &mut Window,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
|
||||
return;
|
||||
};
|
||||
if self.strip_mode.is_some() {
|
||||
if self.input.focus_handle(cx).contains_focused(window, cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
match &mut entry.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
@@ -279,6 +431,50 @@ impl BreakpointList {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn previous_breakpoint_property(
|
||||
&mut self,
|
||||
_: &PreviousBreakpointProperty,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let next_mode = match self.strip_mode {
|
||||
Some(ActiveBreakpointStripMode::Log) => None,
|
||||
Some(ActiveBreakpointStripMode::Condition) => Some(ActiveBreakpointStripMode::Log),
|
||||
Some(ActiveBreakpointStripMode::HitCondition) => {
|
||||
Some(ActiveBreakpointStripMode::Condition)
|
||||
}
|
||||
None => Some(ActiveBreakpointStripMode::HitCondition),
|
||||
};
|
||||
if let Some(mode) = next_mode {
|
||||
self.set_active_breakpoint_property(mode, window, cx);
|
||||
} else {
|
||||
self.strip_mode.take();
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
fn next_breakpoint_property(
|
||||
&mut self,
|
||||
_: &NextBreakpointProperty,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let next_mode = match self.strip_mode {
|
||||
Some(ActiveBreakpointStripMode::Log) => Some(ActiveBreakpointStripMode::Condition),
|
||||
Some(ActiveBreakpointStripMode::Condition) => {
|
||||
Some(ActiveBreakpointStripMode::HitCondition)
|
||||
}
|
||||
Some(ActiveBreakpointStripMode::HitCondition) => None,
|
||||
None => Some(ActiveBreakpointStripMode::Log),
|
||||
};
|
||||
if let Some(mode) = next_mode {
|
||||
self.set_active_breakpoint_property(mode, window, cx);
|
||||
} else {
|
||||
self.strip_mode.take();
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
|
||||
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
|
||||
@@ -294,20 +490,31 @@ impl BreakpointList {
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render_list(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let selected_ix = self.selected_ix;
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let supported_breakpoint_properties = self
|
||||
.session
|
||||
.as_ref()
|
||||
.map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities()))
|
||||
.unwrap_or_else(SupportedBreakpointProperties::empty);
|
||||
let strip_mode = self.strip_mode;
|
||||
uniform_list(
|
||||
"breakpoint-list",
|
||||
self.breakpoints.len(),
|
||||
cx.processor(move |this, range: Range<usize>, window, cx| {
|
||||
cx.processor(move |this, range: Range<usize>, _, _| {
|
||||
range
|
||||
.clone()
|
||||
.zip(&mut this.breakpoints[range])
|
||||
.map(|(ix, breakpoint)| {
|
||||
breakpoint
|
||||
.render(ix, focus_handle.clone(), window, cx)
|
||||
.toggle_state(Some(ix) == selected_ix)
|
||||
.render(
|
||||
strip_mode,
|
||||
supported_breakpoint_properties,
|
||||
ix,
|
||||
Some(ix) == selected_ix,
|
||||
focus_handle.clone(),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.collect()
|
||||
@@ -443,7 +650,6 @@ impl BreakpointList {
|
||||
|
||||
impl Render for BreakpointList {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||
// let old_len = self.breakpoints.len();
|
||||
let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
|
||||
self.breakpoints.clear();
|
||||
let weak = cx.weak_entity();
|
||||
@@ -523,15 +729,46 @@ impl Render for BreakpointList {
|
||||
.on_action(cx.listener(Self::select_previous))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::toggle_enable_breakpoint))
|
||||
.on_action(cx.listener(Self::unset_breakpoint))
|
||||
.on_action(cx.listener(Self::next_breakpoint_property))
|
||||
.on_action(cx.listener(Self::previous_breakpoint_property))
|
||||
.size_full()
|
||||
.m_0p5()
|
||||
.child(self.render_list(window, cx))
|
||||
.children(self.render_vertical_scrollbar(cx))
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.child(self.render_list(cx))
|
||||
.children(self.render_vertical_scrollbar(cx)),
|
||||
)
|
||||
.when_some(self.strip_mode, |this, _| {
|
||||
this.child(Divider::horizontal()).child(
|
||||
h_flex()
|
||||
// .w_full()
|
||||
.m_0p5()
|
||||
.p_0p5()
|
||||
.border_1()
|
||||
.rounded_sm()
|
||||
.when(
|
||||
self.input.focus_handle(cx).contains_focused(window, cx),
|
||||
|this| {
|
||||
let colors = cx.theme().colors();
|
||||
let border = if self.input.read(cx).read_only(cx) {
|
||||
colors.border_disabled
|
||||
} else {
|
||||
colors.border_focused
|
||||
};
|
||||
this.border_color(border)
|
||||
},
|
||||
)
|
||||
.child(self.input.clone()),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct LineBreakpoint {
|
||||
name: SharedString,
|
||||
@@ -543,7 +780,10 @@ struct LineBreakpoint {
|
||||
impl LineBreakpoint {
|
||||
fn render(
|
||||
&mut self,
|
||||
props: SupportedBreakpointProperties,
|
||||
strip_mode: Option<ActiveBreakpointStripMode>,
|
||||
ix: usize,
|
||||
is_selected: bool,
|
||||
focus_handle: FocusHandle,
|
||||
weak: WeakEntity<BreakpointList>,
|
||||
) -> ListItem {
|
||||
@@ -594,15 +834,16 @@ impl LineBreakpoint {
|
||||
})
|
||||
.child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger))
|
||||
.on_mouse_down(MouseButton::Left, move |_, _, _| {});
|
||||
|
||||
ListItem::new(SharedString::from(format!(
|
||||
"breakpoint-ui-item-{:?}/{}:{}",
|
||||
self.dir, self.name, self.line
|
||||
)))
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
move |_, _, cx| {
|
||||
move |_, window, cx| {
|
||||
weak.update(cx, |breakpoint_list, cx| {
|
||||
breakpoint_list.select_ix(Some(ix), cx);
|
||||
breakpoint_list.select_ix(Some(ix), window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -613,21 +854,26 @@ impl LineBreakpoint {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.py_1()
|
||||
h_flex()
|
||||
.w_full()
|
||||
.mr_4()
|
||||
.py_0p5()
|
||||
.gap_1()
|
||||
.min_h(px(26.))
|
||||
.justify_center()
|
||||
.justify_between()
|
||||
.id(SharedString::from(format!(
|
||||
"breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
|
||||
self.dir, self.name, self.line
|
||||
)))
|
||||
.on_click(move |_, window, cx| {
|
||||
weak.update(cx, |breakpoint_list, cx| {
|
||||
breakpoint_list.select_ix(Some(ix), cx);
|
||||
breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
|
||||
})
|
||||
.ok();
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
move |_, window, cx| {
|
||||
weak.update(cx, |breakpoint_list, cx| {
|
||||
breakpoint_list.select_ix(Some(ix), window, cx);
|
||||
breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
@@ -644,8 +890,20 @@ impl LineBreakpoint {
|
||||
.size(LabelSize::Small)
|
||||
.line_height_style(ui::LineHeightStyle::UiLabel)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(BreakpointOptionsStrip {
|
||||
props,
|
||||
breakpoint: BreakpointEntry {
|
||||
kind: BreakpointEntryKind::LineBreakpoint(self.clone()),
|
||||
weak: weak,
|
||||
},
|
||||
is_selected,
|
||||
focus_handle,
|
||||
strip_mode,
|
||||
index: ix,
|
||||
}),
|
||||
)
|
||||
.toggle_state(is_selected)
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -658,7 +916,10 @@ struct ExceptionBreakpoint {
|
||||
impl ExceptionBreakpoint {
|
||||
fn render(
|
||||
&mut self,
|
||||
props: SupportedBreakpointProperties,
|
||||
strip_mode: Option<ActiveBreakpointStripMode>,
|
||||
ix: usize,
|
||||
is_selected: bool,
|
||||
focus_handle: FocusHandle,
|
||||
list: WeakEntity<BreakpointList>,
|
||||
) -> ListItem {
|
||||
@@ -669,15 +930,15 @@ impl ExceptionBreakpoint {
|
||||
};
|
||||
let id = SharedString::from(&self.id);
|
||||
let is_enabled = self.is_enabled;
|
||||
|
||||
let weak = list.clone();
|
||||
ListItem::new(SharedString::from(format!(
|
||||
"exception-breakpoint-ui-item-{}",
|
||||
self.id
|
||||
)))
|
||||
.on_click({
|
||||
let list = list.clone();
|
||||
move |_, _, cx| {
|
||||
list.update(cx, |list, cx| list.select_ix(Some(ix), cx))
|
||||
move |_, window, cx| {
|
||||
list.update(cx, |list, cx| list.select_ix(Some(ix), window, cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
@@ -691,18 +952,21 @@ impl ExceptionBreakpoint {
|
||||
"exception-breakpoint-ui-item-{}-click-handler",
|
||||
self.id
|
||||
)))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
if is_enabled {
|
||||
"Disable Exception Breakpoint"
|
||||
} else {
|
||||
"Enable Exception Breakpoint"
|
||||
},
|
||||
&ToggleEnableBreakpoint,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
if is_enabled {
|
||||
"Disable Exception Breakpoint"
|
||||
} else {
|
||||
"Enable Exception Breakpoint"
|
||||
},
|
||||
&ToggleEnableBreakpoint,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let list = list.clone();
|
||||
@@ -722,21 +986,40 @@ impl ExceptionBreakpoint {
|
||||
.child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.py_1()
|
||||
.gap_1()
|
||||
.min_h(px(26.))
|
||||
.justify_center()
|
||||
.id(("exception-breakpoint-label", ix))
|
||||
h_flex()
|
||||
.w_full()
|
||||
.mr_4()
|
||||
.py_0p5()
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new(self.data.label.clone())
|
||||
.size(LabelSize::Small)
|
||||
.line_height_style(ui::LineHeightStyle::UiLabel),
|
||||
v_flex()
|
||||
.py_1()
|
||||
.gap_1()
|
||||
.min_h(px(26.))
|
||||
.justify_center()
|
||||
.id(("exception-breakpoint-label", ix))
|
||||
.child(
|
||||
Label::new(self.data.label.clone())
|
||||
.size(LabelSize::Small)
|
||||
.line_height_style(ui::LineHeightStyle::UiLabel),
|
||||
)
|
||||
.when_some(self.data.description.clone(), |el, description| {
|
||||
el.tooltip(Tooltip::text(description))
|
||||
}),
|
||||
)
|
||||
.when_some(self.data.description.clone(), |el, description| {
|
||||
el.tooltip(Tooltip::text(description))
|
||||
.child(BreakpointOptionsStrip {
|
||||
props,
|
||||
breakpoint: BreakpointEntry {
|
||||
kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()),
|
||||
weak: weak,
|
||||
},
|
||||
is_selected,
|
||||
focus_handle,
|
||||
strip_mode,
|
||||
index: ix,
|
||||
}),
|
||||
)
|
||||
.toggle_state(is_selected)
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -754,18 +1037,264 @@ struct BreakpointEntry {
|
||||
impl BreakpointEntry {
|
||||
fn render(
|
||||
&mut self,
|
||||
strip_mode: Option<ActiveBreakpointStripMode>,
|
||||
props: SupportedBreakpointProperties,
|
||||
ix: usize,
|
||||
is_selected: bool,
|
||||
focus_handle: FocusHandle,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) -> ListItem {
|
||||
match &mut self.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => line_breakpoint.render(
|
||||
props,
|
||||
strip_mode,
|
||||
ix,
|
||||
is_selected,
|
||||
focus_handle,
|
||||
self.weak.clone(),
|
||||
),
|
||||
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => exception_breakpoint
|
||||
.render(
|
||||
props.for_exception_breakpoints(),
|
||||
strip_mode,
|
||||
ix,
|
||||
is_selected,
|
||||
focus_handle,
|
||||
self.weak.clone(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn id(&self) -> SharedString {
|
||||
match &self.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => format!(
|
||||
"source-breakpoint-control-strip-{:?}:{}",
|
||||
line_breakpoint.breakpoint.path, line_breakpoint.breakpoint.row
|
||||
)
|
||||
.into(),
|
||||
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => format!(
|
||||
"exception-breakpoint-control-strip--{}",
|
||||
exception_breakpoint.id
|
||||
)
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn has_log(&self) -> bool {
|
||||
match &self.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
line_breakpoint.render(ix, focus_handle, self.weak.clone())
|
||||
line_breakpoint.breakpoint.message.is_some()
|
||||
}
|
||||
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
|
||||
exception_breakpoint.render(ix, focus_handle, self.weak.clone())
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_condition(&self) -> bool {
|
||||
match &self.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
line_breakpoint.breakpoint.condition.is_some()
|
||||
}
|
||||
// We don't support conditions on exception breakpoints
|
||||
BreakpointEntryKind::ExceptionBreakpoint(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_hit_condition(&self) -> bool {
|
||||
match &self.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
line_breakpoint.breakpoint.hit_condition.is_some()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
bitflags::bitflags! {
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SupportedBreakpointProperties: u32 {
|
||||
const LOG = 1 << 0;
|
||||
const CONDITION = 1 << 1;
|
||||
const HIT_CONDITION = 1 << 2;
|
||||
// Conditions for exceptions can be set only when exception filters are supported.
|
||||
const EXCEPTION_FILTER_OPTIONS = 1 << 3;
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Capabilities> for SupportedBreakpointProperties {
|
||||
fn from(caps: &Capabilities) -> Self {
|
||||
let mut this = Self::empty();
|
||||
for (prop, offset) in [
|
||||
(caps.supports_log_points, Self::LOG),
|
||||
(caps.supports_conditional_breakpoints, Self::CONDITION),
|
||||
(
|
||||
caps.supports_hit_conditional_breakpoints,
|
||||
Self::HIT_CONDITION,
|
||||
),
|
||||
(
|
||||
caps.supports_exception_options,
|
||||
Self::EXCEPTION_FILTER_OPTIONS,
|
||||
),
|
||||
] {
|
||||
if prop.unwrap_or_default() {
|
||||
this.insert(offset);
|
||||
}
|
||||
}
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl SupportedBreakpointProperties {
|
||||
fn for_exception_breakpoints(self) -> Self {
|
||||
// TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
#[derive(IntoElement)]
|
||||
struct BreakpointOptionsStrip {
|
||||
props: SupportedBreakpointProperties,
|
||||
breakpoint: BreakpointEntry,
|
||||
is_selected: bool,
|
||||
focus_handle: FocusHandle,
|
||||
strip_mode: Option<ActiveBreakpointStripMode>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl BreakpointOptionsStrip {
|
||||
fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool {
|
||||
self.is_selected && self.strip_mode == Some(expected_mode)
|
||||
}
|
||||
fn on_click_callback(
|
||||
&self,
|
||||
mode: ActiveBreakpointStripMode,
|
||||
) -> impl for<'a> Fn(&ClickEvent, &mut Window, &'a mut App) + use<> {
|
||||
let list = self.breakpoint.weak.clone();
|
||||
let ix = self.index;
|
||||
move |_, window, cx| {
|
||||
list.update(cx, |this, cx| {
|
||||
if this.strip_mode != Some(mode) {
|
||||
this.set_active_breakpoint_property(mode, window, cx);
|
||||
} else if this.selected_ix == Some(ix) {
|
||||
this.strip_mode.take();
|
||||
} else {
|
||||
cx.propagate();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
fn add_border(
|
||||
&self,
|
||||
kind: ActiveBreakpointStripMode,
|
||||
available: bool,
|
||||
window: &Window,
|
||||
cx: &App,
|
||||
) -> impl Fn(Div) -> Div {
|
||||
move |this: Div| {
|
||||
// Avoid layout shifts in case there's no colored border
|
||||
let this = this.border_2().rounded_sm();
|
||||
if self.is_selected && self.strip_mode == Some(kind) {
|
||||
let theme = cx.theme().colors();
|
||||
if self.focus_handle.is_focused(window) {
|
||||
this.border_color(theme.border_selected)
|
||||
} else {
|
||||
this.border_color(theme.border_disabled)
|
||||
}
|
||||
} else if !available {
|
||||
this.border_color(cx.theme().colors().border_disabled)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl RenderOnce for BreakpointOptionsStrip {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let id = self.breakpoint.id();
|
||||
let supports_logs = self.props.contains(SupportedBreakpointProperties::LOG);
|
||||
let supports_condition = self
|
||||
.props
|
||||
.contains(SupportedBreakpointProperties::CONDITION);
|
||||
let supports_hit_condition = self
|
||||
.props
|
||||
.contains(SupportedBreakpointProperties::HIT_CONDITION);
|
||||
let has_logs = self.breakpoint.has_log();
|
||||
let has_condition = self.breakpoint.has_condition();
|
||||
let has_hit_condition = self.breakpoint.has_hit_condition();
|
||||
let style_for_toggle = |mode, is_enabled| {
|
||||
if is_enabled && self.strip_mode == Some(mode) && self.is_selected {
|
||||
ui::ButtonStyle::Filled
|
||||
} else {
|
||||
ui::ButtonStyle::Subtle
|
||||
}
|
||||
};
|
||||
let color_for_toggle = |is_enabled| {
|
||||
if is_enabled {
|
||||
ui::Color::Default
|
||||
} else {
|
||||
ui::Color::Muted
|
||||
}
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div() .map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx))
|
||||
.child(
|
||||
IconButton::new(
|
||||
SharedString::from(format!("{id}-log-toggle")),
|
||||
IconName::ScrollText,
|
||||
)
|
||||
.style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
|
||||
.icon_color(color_for_toggle(has_logs))
|
||||
.disabled(!supports_logs)
|
||||
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log))
|
||||
.on_click(self.on_click_callback(ActiveBreakpointStripMode::Log)).tooltip(|window, cx| Tooltip::with_meta("Set Log Message", None, "Set log message to display (instead of stopping) when a breakpoint is hit", window, cx))
|
||||
)
|
||||
.when(!has_logs && !self.is_selected, |this| this.invisible()),
|
||||
)
|
||||
.child(
|
||||
div().map(self.add_border(
|
||||
ActiveBreakpointStripMode::Condition,
|
||||
supports_condition,
|
||||
window, cx
|
||||
))
|
||||
.child(
|
||||
IconButton::new(
|
||||
SharedString::from(format!("{id}-condition-toggle")),
|
||||
IconName::SplitAlt,
|
||||
)
|
||||
.style(style_for_toggle(
|
||||
ActiveBreakpointStripMode::Condition,
|
||||
has_condition
|
||||
))
|
||||
.icon_color(color_for_toggle(has_condition))
|
||||
.disabled(!supports_condition)
|
||||
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
|
||||
.on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
|
||||
.tooltip(|window, cx| Tooltip::with_meta("Set Condition", None, "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met", window, cx))
|
||||
)
|
||||
.when(!has_condition && !self.is_selected, |this| this.invisible()),
|
||||
)
|
||||
.child(
|
||||
div() .map(self.add_border(
|
||||
ActiveBreakpointStripMode::HitCondition,
|
||||
supports_hit_condition,window, cx
|
||||
))
|
||||
.child(
|
||||
IconButton::new(
|
||||
SharedString::from(format!("{id}-hit-condition-toggle")),
|
||||
IconName::ArrowDown10,
|
||||
)
|
||||
.style(style_for_toggle(
|
||||
ActiveBreakpointStripMode::HitCondition,
|
||||
has_hit_condition,
|
||||
))
|
||||
.icon_color(color_for_toggle(has_hit_condition))
|
||||
.disabled(!supports_hit_condition)
|
||||
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition))
|
||||
.on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition)).tooltip(|window, cx| Tooltip::with_meta("Set Hit Condition", None, "Set expression that controls how many hits of the breakpoint are ignored.", window, cx))
|
||||
)
|
||||
.when(!has_hit_condition && !self.is_selected, |this| {
|
||||
this.invisible()
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use collections::HashMap;
|
||||
use dap::StackFrameId;
|
||||
use editor::{
|
||||
Anchor, Bias, DebugStackFrameLine, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer,
|
||||
RowHighlightOptions, ToPoint, scroll::Autoscroll,
|
||||
RowHighlightOptions, SelectionEffects, ToPoint, scroll::Autoscroll,
|
||||
};
|
||||
use gpui::{
|
||||
AnyView, App, AppContext, Entity, EventEmitter, Focusable, IntoElement, Render, SharedString,
|
||||
@@ -99,10 +99,11 @@ impl StackTraceView {
|
||||
if frame_anchor.excerpt_id
|
||||
!= editor.selections.newest_anchor().head().excerpt_id
|
||||
{
|
||||
let auto_scroll =
|
||||
Some(Autoscroll::center().for_anchor(frame_anchor));
|
||||
let effects = SelectionEffects::scroll(
|
||||
Autoscroll::center().for_anchor(frame_anchor),
|
||||
);
|
||||
|
||||
editor.change_selections(auto_scroll, window, cx, |selections| {
|
||||
editor.change_selections(effects, window, cx, |selections| {
|
||||
let selection_id = selections.new_selection_id();
|
||||
|
||||
let selection = Selection {
|
||||
|
||||
@@ -4,7 +4,6 @@ use editor::{
|
||||
Anchor, Editor, EditorSnapshot, ToOffset,
|
||||
display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle},
|
||||
hover_popover::diagnostics_markdown_style,
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
use gpui::{AppContext, Entity, Focusable, WeakEntity};
|
||||
use language::{BufferId, Diagnostic, DiagnosticEntry};
|
||||
@@ -311,7 +310,7 @@ impl DiagnosticBlock {
|
||||
let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
|
||||
|
||||
editor.unfold_ranges(&[range.start..range.end], true, false, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_ranges([range.start..range.start]);
|
||||
});
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
|
||||
@@ -12,7 +12,6 @@ use diagnostic_renderer::DiagnosticBlock;
|
||||
use editor::{
|
||||
DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
|
||||
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use gpui::{
|
||||
@@ -626,7 +625,7 @@ impl ProjectDiagnosticsEditor {
|
||||
if let Some(anchor_range) = anchor_ranges.first() {
|
||||
let range_to_select = anchor_range.start..anchor_range.start;
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_anchor_ranges([range_to_select]);
|
||||
})
|
||||
});
|
||||
|
||||
@@ -388,7 +388,6 @@ actions!(
|
||||
RestartLanguageServer,
|
||||
RevealInFileManager,
|
||||
ReverseLines,
|
||||
RevertFile,
|
||||
ReloadFile,
|
||||
Rewrap,
|
||||
RunFlycheck,
|
||||
|
||||
@@ -1205,7 +1205,7 @@ impl CodeActionContents {
|
||||
tasks_len + code_actions_len + self.debug_scenarios.len()
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
|
||||
@@ -966,10 +966,22 @@ impl DisplaySnapshot {
|
||||
.and_then(|id| id.style(&editor_style.syntax));
|
||||
|
||||
if let Some(chunk_highlight) = chunk.highlight_style {
|
||||
// For color inlays, blend the color with the editor background
|
||||
let mut processed_highlight = chunk_highlight;
|
||||
if chunk.is_inlay {
|
||||
if let Some(inlay_color) = chunk_highlight.color {
|
||||
// Only blend if the color has transparency (alpha < 1.0)
|
||||
if inlay_color.a < 1.0 {
|
||||
let blended_color = editor_style.background.blend(inlay_color);
|
||||
processed_highlight.color = Some(blended_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(highlight_style) = highlight_style.as_mut() {
|
||||
highlight_style.highlight(chunk_highlight);
|
||||
highlight_style.highlight(processed_highlight);
|
||||
} else {
|
||||
highlight_style = Some(chunk_highlight);
|
||||
highlight_style = Some(processed_highlight);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -327,9 +327,9 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint,
|
||||
InlayId::Color(_) => match inlay.color {
|
||||
Some(color) => {
|
||||
let style = self.highlight_styles.inlay_hint.get_or_insert_default();
|
||||
let mut style = self.highlight_styles.inlay_hint.unwrap_or_default();
|
||||
style.color = Some(color);
|
||||
Some(*style)
|
||||
Some(style)
|
||||
}
|
||||
None => self.highlight_styles.inlay_hint,
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::{
|
||||
ScrollbarDiagnostics, ShowMinimap, ShowScrollbar,
|
||||
},
|
||||
git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer},
|
||||
gutter::breakpoint_indicator::breakpoint_indicator_path,
|
||||
hover_popover::{
|
||||
self, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
|
||||
POPOVER_RIGHT_OFFSET, hover_at,
|
||||
@@ -30,6 +31,7 @@ use crate::{
|
||||
mouse_context_menu::{self, MenuPosition},
|
||||
scroll::{ActiveScrollbarState, ScrollbarThumbState, scroll_amount::ScrollAmount},
|
||||
};
|
||||
|
||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use file_icons::FileIcons;
|
||||
@@ -42,12 +44,12 @@ use gpui::{
|
||||
Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
|
||||
Bounds, ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges,
|
||||
Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
|
||||
HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Keystroke, Length,
|
||||
HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Keystroke, Length, Modifiers,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString,
|
||||
Size, StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, WeakEntity,
|
||||
Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px,
|
||||
quad, relative, size, solid_background, transparent_black,
|
||||
Window, anchored, canvas, deferred, div, fill, linear_color_stop, linear_gradient, outline,
|
||||
point, px, quad, relative, size, solid_background, transparent_black,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::language_settings::{
|
||||
@@ -61,7 +63,7 @@ use multi_buffer::{
|
||||
|
||||
use project::{
|
||||
ProjectPath,
|
||||
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
|
||||
debugger::breakpoint_store::{Breakpoint, BreakpointEditAction, BreakpointSessionState},
|
||||
project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings},
|
||||
};
|
||||
use settings::Settings;
|
||||
@@ -2757,7 +2759,16 @@ impl EditorElement {
|
||||
return None;
|
||||
}
|
||||
|
||||
let button = editor.render_breakpoint(text_anchor, display_row, &bp, state, cx);
|
||||
let button = self.render_breakpoint(
|
||||
editor,
|
||||
snapshot,
|
||||
text_anchor,
|
||||
display_row,
|
||||
row,
|
||||
&bp,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let button = prepaint_gutter_button(
|
||||
button,
|
||||
@@ -2776,6 +2787,184 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_breakpoint(
|
||||
&self,
|
||||
editor: &Editor,
|
||||
snapshot: &EditorSnapshot,
|
||||
position: Anchor,
|
||||
row: DisplayRow,
|
||||
multibuffer_row: MultiBufferRow,
|
||||
breakpoint: &Breakpoint,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let element_id =
|
||||
ElementId::Name(format!("breakpoint_indicator_{}", multibuffer_row.0).into());
|
||||
|
||||
// === Extract text style and calculate dimensions ===
|
||||
let text_style = self.style.text.clone();
|
||||
let font_size = text_style.font_size;
|
||||
let font_size_px = font_size.to_pixels(window.rem_size());
|
||||
let rem_size = window.rem_size();
|
||||
|
||||
// Calculate font scale relative to a baseline font size
|
||||
const BASELINE_FONT_SIZE: f32 = 14.0; // Default editor font size
|
||||
let font_scale = font_size_px / px(BASELINE_FONT_SIZE);
|
||||
|
||||
let line_height: Pixels = text_style.line_height.to_pixels(font_size, rem_size);
|
||||
|
||||
// Debug font metrics
|
||||
dbg!(font_size);
|
||||
dbg!(font_size_px);
|
||||
dbg!(rem_size);
|
||||
dbg!(BASELINE_FONT_SIZE);
|
||||
dbg!(font_scale);
|
||||
dbg!(line_height);
|
||||
|
||||
// Helper to scale pixel values based on font size
|
||||
let scale_px = |value: f32| px(value) * font_scale;
|
||||
|
||||
const HORIZONTAL_OFFSET: f32 = 40.0;
|
||||
const VERTICAL_OFFSET: f32 = 4.0;
|
||||
|
||||
let horizontal_offset = scale_px(HORIZONTAL_OFFSET);
|
||||
let vertical_offset = px(VERTICAL_OFFSET);
|
||||
let indicator_height = line_height - vertical_offset;
|
||||
|
||||
let line_number_width = self.max_line_number_width(snapshot, window, cx);
|
||||
let indicator_width = dbg!(line_number_width) + scale_px(HORIZONTAL_OFFSET);
|
||||
|
||||
// Debug indicator dimensions
|
||||
dbg!(horizontal_offset);
|
||||
dbg!(vertical_offset);
|
||||
dbg!(indicator_height);
|
||||
dbg!(indicator_width);
|
||||
|
||||
let is_disabled = breakpoint.is_disabled();
|
||||
let breakpoint_arc = Arc::from(breakpoint.clone());
|
||||
|
||||
let (is_hovered, collides_with_existing) = editor.gutter_breakpoint_indicator.0.map_or(
|
||||
(false, false),
|
||||
|PhantomBreakpointIndicator {
|
||||
is_active,
|
||||
display_row,
|
||||
collides_with_existing_breakpoint,
|
||||
}| {
|
||||
(
|
||||
is_active && display_row == row,
|
||||
collides_with_existing_breakpoint,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let indicator_color = if is_hovered {
|
||||
cx.theme().colors().ghost_element_hover
|
||||
} else if is_disabled {
|
||||
cx.theme().status().info.alpha(0.64)
|
||||
} else {
|
||||
cx.theme().status().info.alpha(0.48)
|
||||
};
|
||||
|
||||
let primary_action = if is_disabled {
|
||||
"enable"
|
||||
} else if is_hovered && !collides_with_existing {
|
||||
"set"
|
||||
} else {
|
||||
"unset"
|
||||
};
|
||||
|
||||
let mut tooltip_text = format!("Click to {primary_action}");
|
||||
|
||||
if collides_with_existing && !is_disabled {
|
||||
use std::fmt::Write;
|
||||
let modifier_key = gpui::Keystroke {
|
||||
modifiers: Modifiers::secondary_key(),
|
||||
..Default::default()
|
||||
};
|
||||
write!(tooltip_text, ", {modifier_key}-click to disable").ok();
|
||||
}
|
||||
|
||||
div()
|
||||
.id(element_id)
|
||||
.cursor_pointer()
|
||||
.absolute()
|
||||
.left_0()
|
||||
.w(indicator_width)
|
||||
.h(indicator_height)
|
||||
.child(
|
||||
canvas(
|
||||
|_bounds, _cx, _style| {},
|
||||
move |bounds, _cx, window, _style| {
|
||||
// Debug canvas bounds
|
||||
dbg!(&bounds);
|
||||
|
||||
// Adjust bounds to account for horizontal offset
|
||||
let adjusted_bounds = Bounds {
|
||||
origin: point(bounds.origin.x + horizontal_offset, bounds.origin.y),
|
||||
size: size(bounds.size.width, indicator_height),
|
||||
};
|
||||
|
||||
// Debug adjusted bounds
|
||||
dbg!(&adjusted_bounds);
|
||||
dbg!(font_scale);
|
||||
|
||||
// Generate the breakpoint indicator path
|
||||
let path =
|
||||
breakpoint_indicator_path(adjusted_bounds, font_scale, is_disabled);
|
||||
|
||||
// Paint the path with the calculated color
|
||||
window.paint_path(path, indicator_color);
|
||||
},
|
||||
)
|
||||
.size_full(),
|
||||
)
|
||||
.on_click({
|
||||
let editor_weak = self.editor.downgrade();
|
||||
let breakpoint = breakpoint_arc.clone();
|
||||
move |event, window, cx| {
|
||||
let action = if event.modifiers().platform || breakpoint.is_disabled() {
|
||||
BreakpointEditAction::InvertState
|
||||
} else {
|
||||
BreakpointEditAction::Toggle
|
||||
};
|
||||
|
||||
let Some(editor_strong) = editor_weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
window.focus(&editor_strong.focus_handle(cx));
|
||||
editor_strong.update(cx, |editor, cx| {
|
||||
editor.edit_breakpoint_at_anchor(
|
||||
position,
|
||||
breakpoint.as_ref().clone(),
|
||||
action,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.on_mouse_down(gpui::MouseButton::Right, {
|
||||
let editor_weak = self.editor.downgrade();
|
||||
let anchor_position = position.clone();
|
||||
move |event, window, cx| {
|
||||
let Some(editor_strong) = editor_weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
editor_strong.update(cx, |editor, cx| {
|
||||
editor.set_breakpoint_context_menu(
|
||||
row,
|
||||
Some(anchor_position.clone()),
|
||||
event.position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_run_indicators(
|
||||
&self,
|
||||
@@ -3034,7 +3223,7 @@ impl EditorElement {
|
||||
scroll_position: gpui::Point<f32>,
|
||||
rows: Range<DisplayRow>,
|
||||
buffer_rows: &[RowInfo],
|
||||
active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
|
||||
_active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
|
||||
newest_selection_head: Option<DisplayPoint>,
|
||||
snapshot: &EditorSnapshot,
|
||||
window: &mut Window,
|
||||
@@ -3090,16 +3279,7 @@ impl EditorElement {
|
||||
return None;
|
||||
}
|
||||
|
||||
let color = active_rows
|
||||
.get(&display_row)
|
||||
.map(|spec| {
|
||||
if spec.breakpoint {
|
||||
cx.theme().colors().debugger_accent
|
||||
} else {
|
||||
cx.theme().colors().editor_active_line_number
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| cx.theme().colors().editor_line_number);
|
||||
let color = cx.theme().colors().editor_active_line_number;
|
||||
let shaped_line =
|
||||
self.shape_line_number(SharedString::from(&line_number), color, window);
|
||||
let scroll_top = scroll_position.y * line_height;
|
||||
@@ -5577,6 +5757,9 @@ impl EditorElement {
|
||||
cx: &mut App,
|
||||
) {
|
||||
window.paint_layer(layout.gutter_hitbox.bounds, |window| {
|
||||
for breakpoint in layout.breakpoints.iter_mut() {
|
||||
breakpoint.paint(window, cx);
|
||||
}
|
||||
window.with_element_namespace("crease_toggles", |window| {
|
||||
for crease_toggle in layout.crease_toggles.iter_mut().flatten() {
|
||||
crease_toggle.paint(window, cx);
|
||||
@@ -5589,13 +5772,24 @@ impl EditorElement {
|
||||
}
|
||||
});
|
||||
|
||||
for breakpoint in layout.breakpoints.iter_mut() {
|
||||
breakpoint.paint(window, cx);
|
||||
}
|
||||
|
||||
for test_indicator in layout.test_indicators.iter_mut() {
|
||||
test_indicator.paint(window, cx);
|
||||
}
|
||||
|
||||
let show_git_gutter = layout
|
||||
.position_map
|
||||
.snapshot
|
||||
.show_git_diff_gutter
|
||||
.unwrap_or_else(|| {
|
||||
matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
Some(GitGutterSetting::TrackedFiles)
|
||||
)
|
||||
});
|
||||
|
||||
if show_git_gutter {
|
||||
Self::paint_gutter_diff_hunks(layout, window, cx)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5619,20 +5813,6 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
let show_git_gutter = layout
|
||||
.position_map
|
||||
.snapshot
|
||||
.show_git_diff_gutter
|
||||
.unwrap_or_else(|| {
|
||||
matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
Some(GitGutterSetting::TrackedFiles)
|
||||
)
|
||||
});
|
||||
if show_git_gutter {
|
||||
Self::paint_gutter_diff_hunks(layout, window, cx)
|
||||
}
|
||||
|
||||
let highlight_width = 0.275 * layout.position_map.line_height;
|
||||
let highlight_corner_radii = Corners::all(0.05 * layout.position_map.line_height);
|
||||
window.paint_layer(layout.gutter_hitbox.bounds, |window| {
|
||||
@@ -6879,7 +7059,8 @@ impl EditorElement {
|
||||
layout.width
|
||||
}
|
||||
|
||||
fn max_line_number_width(
|
||||
/// Get the width of the longest line number in the current editor in Pixels
|
||||
pub(crate) fn max_line_number_width(
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
window: &mut Window,
|
||||
@@ -6974,7 +7155,7 @@ impl AcceptEditPredictionBinding {
|
||||
}
|
||||
|
||||
fn prepaint_gutter_button(
|
||||
button: IconButton,
|
||||
button: impl IntoElement,
|
||||
row: DisplayRow,
|
||||
line_height: Pixels,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
@@ -8959,6 +9140,10 @@ impl Element for EditorElement {
|
||||
self.paint_background(layout, window, cx);
|
||||
self.paint_indent_guides(layout, window, cx);
|
||||
|
||||
if layout.gutter_hitbox.size.width > Pixels::ZERO {
|
||||
self.paint_gutter_highlights(layout, window, cx);
|
||||
self.paint_gutter_indicators(layout, window, cx);
|
||||
}
|
||||
if layout.gutter_hitbox.size.width > Pixels::ZERO {
|
||||
self.paint_blamed_display_rows(layout, window, cx);
|
||||
self.paint_line_numbers(layout, window, cx);
|
||||
@@ -8966,11 +9151,6 @@ impl Element for EditorElement {
|
||||
|
||||
self.paint_text(layout, window, cx);
|
||||
|
||||
if layout.gutter_hitbox.size.width > Pixels::ZERO {
|
||||
self.paint_gutter_highlights(layout, window, cx);
|
||||
self.paint_gutter_indicators(layout, window, cx);
|
||||
}
|
||||
|
||||
if !layout.blocks.is_empty() {
|
||||
window.with_element_namespace("blocks", |window| {
|
||||
self.paint_blocks(layout, window, cx);
|
||||
@@ -10051,7 +10231,7 @@ fn compute_auto_height_layout(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
Editor, MultiBuffer,
|
||||
Editor, MultiBuffer, SelectionEffects,
|
||||
display_map::{BlockPlacement, BlockProperties},
|
||||
editor_tests::{init_test, update_test_language_settings},
|
||||
};
|
||||
@@ -10176,7 +10356,7 @@ mod tests {
|
||||
window
|
||||
.update(cx, |editor, window, cx| {
|
||||
editor.cursor_shape = CursorShape::Block;
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([
|
||||
Point::new(0, 0)..Point::new(1, 0),
|
||||
Point::new(3, 2)..Point::new(3, 3),
|
||||
|
||||
208
crates/editor/src/gutter/breakpoint_indicator.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
use gpui::{Bounds, Path, PathBuilder, PathStyle, StrokeOptions, point};
|
||||
use ui::{Pixels, px};
|
||||
|
||||
/// Draw the path for the breakpoint indicator.
|
||||
///
|
||||
/// Note: The indicator needs to be a minimum of MIN_WIDTH px wide.
|
||||
/// wide to draw without graphical issues, so it will ignore narrower width.
|
||||
pub(crate) fn breakpoint_indicator_path(
|
||||
bounds: Bounds<Pixels>,
|
||||
scale: f32,
|
||||
stroke: bool,
|
||||
) -> Path<Pixels> {
|
||||
// Constants for the breakpoint shape dimensions
|
||||
// The shape is designed based on a 50px wide by 15px high template
|
||||
// and uses 9-slice style scaling to allow the shape to be stretched
|
||||
// vertically and horizontally.
|
||||
const SHAPE_BASE_HEIGHT: f32 = 15.0;
|
||||
const SHAPE_FIXED_WIDTH: f32 = 32.0; // Width of non-stretchable parts (corners)
|
||||
const SHAPE_MIN_WIDTH: f32 = 34.0; // Minimum width to render properly
|
||||
const PIXEL_ROUNDING_FACTOR: f32 = 8.0; // Round to nearest 1/8 pixel
|
||||
|
||||
// Key points in the shape (in base coordinates)
|
||||
const CORNER_RADIUS: f32 = 5.0;
|
||||
const CENTER_Y: f32 = 7.5;
|
||||
const TOP_Y: f32 = 0.0;
|
||||
const BOTTOM_Y: f32 = 15.0;
|
||||
const CURVE_CONTROL_OFFSET: f32 = 1.5;
|
||||
const RIGHT_CORNER_START: f32 = 4.0;
|
||||
const RIGHT_CORNER_WIDTH: f32 = 13.0;
|
||||
|
||||
// Helper function to round pixels to nearest 1/8
|
||||
let round_to_pixel_grid = |value: Pixels| -> Pixels {
|
||||
let value_f32: f32 = value.into();
|
||||
px((value_f32 * PIXEL_ROUNDING_FACTOR).round() / PIXEL_ROUNDING_FACTOR)
|
||||
};
|
||||
|
||||
// Calculate actual dimensions with scaling
|
||||
let min_allowed_width = px(SHAPE_MIN_WIDTH * scale);
|
||||
let actual_width = if bounds.size.width < min_allowed_width {
|
||||
min_allowed_width
|
||||
} else {
|
||||
bounds.size.width
|
||||
};
|
||||
let actual_height = bounds.size.height;
|
||||
|
||||
// Debug input parameters and initial calculations
|
||||
dbg!(&bounds);
|
||||
dbg!(scale);
|
||||
dbg!(stroke);
|
||||
dbg!(min_allowed_width);
|
||||
dbg!(actual_width);
|
||||
dbg!(actual_height);
|
||||
|
||||
// Origin point for positioning
|
||||
let origin_x = bounds.origin.x;
|
||||
let origin_y = bounds.origin.y;
|
||||
|
||||
// Calculate the scale factor based on height and user scale
|
||||
let shape_scale = (actual_height / px(SHAPE_BASE_HEIGHT)) * scale;
|
||||
|
||||
// Calculate the width of fixed and stretchable sections
|
||||
let fixed_sections_width = px(SHAPE_FIXED_WIDTH) * shape_scale;
|
||||
let stretchable_middle_width = actual_width - fixed_sections_width;
|
||||
|
||||
// Debug scaling calculations
|
||||
dbg!(shape_scale);
|
||||
dbg!(fixed_sections_width);
|
||||
dbg!(stretchable_middle_width);
|
||||
|
||||
// Pre-calculate all the key x-coordinates
|
||||
let left_edge_x = round_to_pixel_grid(origin_x);
|
||||
let left_corner_end_x = round_to_pixel_grid(origin_x + px(CORNER_RADIUS) * shape_scale);
|
||||
let middle_section_end_x =
|
||||
round_to_pixel_grid(origin_x + px(CORNER_RADIUS) * shape_scale + stretchable_middle_width);
|
||||
let right_corner_start_x = round_to_pixel_grid(
|
||||
origin_x
|
||||
+ px(CORNER_RADIUS) * shape_scale
|
||||
+ stretchable_middle_width
|
||||
+ px(RIGHT_CORNER_START) * shape_scale,
|
||||
);
|
||||
let right_edge_x = round_to_pixel_grid(
|
||||
origin_x
|
||||
+ px(CORNER_RADIUS) * shape_scale
|
||||
+ stretchable_middle_width
|
||||
+ px(RIGHT_CORNER_WIDTH) * shape_scale,
|
||||
);
|
||||
|
||||
// Debug x-coordinates
|
||||
dbg!(origin_x);
|
||||
dbg!(left_edge_x);
|
||||
dbg!(left_corner_end_x);
|
||||
dbg!(middle_section_end_x);
|
||||
dbg!(right_corner_start_x);
|
||||
dbg!(right_edge_x);
|
||||
|
||||
// Pre-calculate all the key y-coordinates
|
||||
let top_edge_y = round_to_pixel_grid(origin_y);
|
||||
let center_y = round_to_pixel_grid(origin_y + px(CENTER_Y) * shape_scale);
|
||||
let bottom_edge_y = round_to_pixel_grid(origin_y + px(BOTTOM_Y) * shape_scale);
|
||||
|
||||
// Y-coordinates for the left side curves
|
||||
let left_upper_curve_start_y = round_to_pixel_grid(origin_y + px(CORNER_RADIUS) * shape_scale);
|
||||
let left_lower_curve_end_y = round_to_pixel_grid(origin_y + px(10.0) * shape_scale);
|
||||
|
||||
// Y-coordinates for the right side curves
|
||||
let right_upper_curve_control_y = round_to_pixel_grid(origin_y + px(6.0) * shape_scale);
|
||||
let right_lower_curve_control_y = round_to_pixel_grid(origin_y + px(9.0) * shape_scale);
|
||||
|
||||
// Control point offsets
|
||||
let control_offset = px(CURVE_CONTROL_OFFSET) * shape_scale;
|
||||
let right_control_offset = px(9.0) * shape_scale;
|
||||
|
||||
// Debug y-coordinates
|
||||
dbg!(origin_y);
|
||||
dbg!(top_edge_y);
|
||||
dbg!(center_y);
|
||||
dbg!(bottom_edge_y);
|
||||
dbg!(left_upper_curve_start_y);
|
||||
dbg!(left_lower_curve_end_y);
|
||||
dbg!(right_upper_curve_control_y);
|
||||
dbg!(right_lower_curve_control_y);
|
||||
|
||||
// Create the path builder
|
||||
let mut builder = if stroke {
|
||||
let stroke_width = px(1.0 * scale);
|
||||
let options = StrokeOptions::default().with_line_width(stroke_width.0);
|
||||
PathBuilder::stroke(stroke_width).with_style(PathStyle::Stroke(options))
|
||||
} else {
|
||||
PathBuilder::fill()
|
||||
};
|
||||
|
||||
// Build the path - starting from left center
|
||||
builder.move_to(point(left_edge_x, center_y));
|
||||
|
||||
// === Upper half of the shape ===
|
||||
|
||||
// Move up to start of left upper curve
|
||||
builder.line_to(point(left_edge_x, left_upper_curve_start_y));
|
||||
|
||||
// Top-left corner curve
|
||||
builder.cubic_bezier_to(
|
||||
point(left_corner_end_x, top_edge_y),
|
||||
point(left_edge_x, round_to_pixel_grid(origin_y + control_offset)),
|
||||
point(round_to_pixel_grid(origin_x + control_offset), top_edge_y),
|
||||
);
|
||||
|
||||
// Top edge - stretchable middle section
|
||||
builder.line_to(point(middle_section_end_x, top_edge_y));
|
||||
|
||||
// Top edge - right corner start
|
||||
builder.line_to(point(right_corner_start_x, top_edge_y));
|
||||
|
||||
// Top-right corner curve
|
||||
builder.cubic_bezier_to(
|
||||
point(right_edge_x, center_y),
|
||||
point(
|
||||
round_to_pixel_grid(
|
||||
origin_x
|
||||
+ px(CORNER_RADIUS) * shape_scale
|
||||
+ stretchable_middle_width
|
||||
+ right_control_offset,
|
||||
),
|
||||
top_edge_y,
|
||||
),
|
||||
point(right_edge_x, right_upper_curve_control_y),
|
||||
);
|
||||
|
||||
// === Lower half of the shape (mirrored) ===
|
||||
|
||||
// Bottom-right corner curve
|
||||
builder.cubic_bezier_to(
|
||||
point(right_corner_start_x, bottom_edge_y),
|
||||
point(right_edge_x, right_lower_curve_control_y),
|
||||
point(
|
||||
round_to_pixel_grid(
|
||||
origin_x
|
||||
+ px(CORNER_RADIUS) * shape_scale
|
||||
+ stretchable_middle_width
|
||||
+ right_control_offset,
|
||||
),
|
||||
bottom_edge_y,
|
||||
),
|
||||
);
|
||||
|
||||
// Bottom edge - right corner to middle
|
||||
builder.line_to(point(middle_section_end_x, bottom_edge_y));
|
||||
|
||||
// Bottom edge - stretchable middle section
|
||||
builder.line_to(point(left_corner_end_x, bottom_edge_y));
|
||||
|
||||
// Bottom-left corner curve
|
||||
builder.cubic_bezier_to(
|
||||
point(left_edge_x, left_lower_curve_end_y),
|
||||
point(
|
||||
round_to_pixel_grid(origin_x + control_offset),
|
||||
bottom_edge_y,
|
||||
),
|
||||
point(
|
||||
left_edge_x,
|
||||
round_to_pixel_grid(origin_y + px(13.5) * shape_scale),
|
||||
),
|
||||
);
|
||||
|
||||
// Close the path by returning to start
|
||||
builder.line_to(point(left_edge_x, center_y));
|
||||
|
||||
builder.build().unwrap()
|
||||
}
|
||||
1
crates/editor/src/gutter/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod breakpoint_indicator;
|
||||
@@ -1257,7 +1257,7 @@ mod tests {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let anchor_range = snapshot.anchor_before(selection_range.start)
|
||||
..snapshot.anchor_after(selection_range.end);
|
||||
editor.change_selections(Some(crate::Autoscroll::fit()), window, cx, |s| {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
EditorSnapshot, GlobalDiagnosticRenderer, Hover,
|
||||
display_map::{InlayOffset, ToDisplayPoint, invisibles::is_invisible},
|
||||
hover_links::{InlayHighlight, RangeInEditor},
|
||||
scroll::{Autoscroll, ScrollAmount},
|
||||
scroll::ScrollAmount,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use gpui::{
|
||||
@@ -648,7 +648,7 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
||||
..Default::default()
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: { cx.theme().players().local().selection },
|
||||
selection_background_color: cx.theme().colors().element_selection_background,
|
||||
heading: StyleRefinement::default()
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.text_base()
|
||||
@@ -697,7 +697,7 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
||||
..Default::default()
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: { cx.theme().players().local().selection },
|
||||
selection_background_color: cx.theme().colors().element_selection_background,
|
||||
height_is_multiple_of_line_height: true,
|
||||
heading: StyleRefinement::default()
|
||||
.font_weight(FontWeight::BOLD)
|
||||
@@ -746,7 +746,7 @@ pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App)
|
||||
};
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(
|
||||
Some(Autoscroll::fit()),
|
||||
Default::default(),
|
||||
window,
|
||||
cx,
|
||||
|selections| {
|
||||
|
||||
@@ -956,7 +956,7 @@ fn fetch_and_update_hints(
|
||||
.update(cx, |editor, cx| {
|
||||
if got_throttled {
|
||||
let query_not_around_visible_range = match editor
|
||||
.excerpts_for_inlay_hints_query(None, cx)
|
||||
.visible_excerpts(None, cx)
|
||||
.remove(&query.excerpt_id)
|
||||
{
|
||||
Some((_, _, current_visible_range)) => {
|
||||
@@ -1302,6 +1302,7 @@ fn apply_hint_update(
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::SelectionEffects;
|
||||
use crate::editor_tests::update_test_language_settings;
|
||||
use crate::scroll::ScrollAmount;
|
||||
use crate::{ExcerptRange, scroll::Autoscroll, test::editor_lsp_test_context::rust_lang};
|
||||
@@ -1384,7 +1385,9 @@ pub mod tests {
|
||||
|
||||
editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([13..13])
|
||||
});
|
||||
editor.handle_input("some change", window, cx);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -1698,7 +1701,9 @@ pub mod tests {
|
||||
|
||||
rs_editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([13..13])
|
||||
});
|
||||
editor.handle_input("some rs change", window, cx);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -1733,7 +1738,9 @@ pub mod tests {
|
||||
|
||||
md_editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([13..13])
|
||||
});
|
||||
editor.handle_input("some md change", window, cx);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -2155,7 +2162,9 @@ pub mod tests {
|
||||
] {
|
||||
editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([13..13])
|
||||
});
|
||||
editor.handle_input(change_after_opening, window, cx);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -2199,7 +2208,9 @@ pub mod tests {
|
||||
edits.push(cx.spawn(|mut cx| async move {
|
||||
task_editor
|
||||
.update(&mut cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([13..13])
|
||||
});
|
||||
editor.handle_input(async_later_change, window, cx);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -2447,9 +2458,12 @@ pub mod tests {
|
||||
|
||||
editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
|
||||
s.select_ranges([selection_in_cached_range..selection_in_cached_range])
|
||||
});
|
||||
editor.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::center()),
|
||||
window,
|
||||
cx,
|
||||
|s| s.select_ranges([selection_in_cached_range..selection_in_cached_range]),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
cx.executor().advance_clock(Duration::from_millis(
|
||||
@@ -2511,9 +2525,7 @@ pub mod tests {
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> Range<Point> {
|
||||
let ranges = editor
|
||||
.update(cx, |editor, _window, cx| {
|
||||
editor.excerpts_for_inlay_hints_query(None, cx)
|
||||
})
|
||||
.update(cx, |editor, _window, cx| editor.visible_excerpts(None, cx))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
ranges.len(),
|
||||
@@ -2712,15 +2724,24 @@ pub mod tests {
|
||||
|
||||
editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
|
||||
s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
|
||||
});
|
||||
editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
|
||||
s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
|
||||
});
|
||||
editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
|
||||
s.select_ranges([Point::new(50, 0)..Point::new(50, 0)])
|
||||
});
|
||||
editor.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::Next),
|
||||
window,
|
||||
cx,
|
||||
|s| s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]),
|
||||
);
|
||||
editor.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::Next),
|
||||
window,
|
||||
cx,
|
||||
|s| s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]),
|
||||
);
|
||||
editor.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::Next),
|
||||
window,
|
||||
cx,
|
||||
|s| s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
@@ -2745,9 +2766,12 @@ pub mod tests {
|
||||
|
||||
editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
|
||||
s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
|
||||
});
|
||||
editor.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::Next),
|
||||
window,
|
||||
cx,
|
||||
|s| s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
cx.executor().advance_clock(Duration::from_millis(
|
||||
@@ -2778,9 +2802,12 @@ pub mod tests {
|
||||
|
||||
editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
|
||||
s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
|
||||
});
|
||||
editor.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::Next),
|
||||
window,
|
||||
cx,
|
||||
|s| s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
cx.executor().advance_clock(Duration::from_millis(
|
||||
@@ -2812,7 +2839,7 @@ pub mod tests {
|
||||
editor_edited.store(true, Ordering::Release);
|
||||
editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
|
||||
});
|
||||
editor.handle_input("++++more text++++", window, cx);
|
||||
@@ -3130,7 +3157,7 @@ pub mod tests {
|
||||
cx.executor().run_until_parked();
|
||||
editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
|
||||
})
|
||||
})
|
||||
@@ -3412,7 +3439,7 @@ pub mod tests {
|
||||
cx.executor().run_until_parked();
|
||||
editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -778,7 +778,7 @@ impl Item for Editor {
|
||||
|
||||
fn deactivated(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let selection = self.selections.newest_anchor();
|
||||
self.push_to_nav_history(selection.head(), None, true, cx);
|
||||
self.push_to_nav_history(selection.head(), None, true, false, cx);
|
||||
}
|
||||
|
||||
fn workspace_deactivated(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -1352,7 +1352,7 @@ impl ProjectItem for Editor {
|
||||
cx,
|
||||
);
|
||||
if !restoration_data.selections.is_empty() {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges(clip_ranges(&restoration_data.selections, &snapshot));
|
||||
});
|
||||
}
|
||||
@@ -1521,7 +1521,7 @@ impl SearchableItem for Editor {
|
||||
fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
|
||||
let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
|
||||
let snapshot = &self.snapshot(window, cx).buffer_snapshot;
|
||||
let selection = self.selections.newest::<usize>(cx);
|
||||
let selection = self.selections.newest_adjusted(cx);
|
||||
|
||||
match setting {
|
||||
SeedQuerySetting::Never => String::new(),
|
||||
@@ -1558,7 +1558,7 @@ impl SearchableItem for Editor {
|
||||
) {
|
||||
self.unfold_ranges(&[matches[index].clone()], false, true, cx);
|
||||
let range = self.range_for_match(&matches[index]);
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
self.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_ranges([range]);
|
||||
})
|
||||
}
|
||||
@@ -1570,7 +1570,7 @@ impl SearchableItem for Editor {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.unfold_ranges(matches, false, false, cx);
|
||||
self.change_selections(None, window, cx, |s| {
|
||||
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges(matches.iter().cloned())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -843,7 +843,7 @@ mod jsx_tag_autoclose_tests {
|
||||
let mut cx = EditorTestContext::for_editor(editor, cx).await;
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
|
||||
selections.select(vec![
|
||||
Selection::from_offset(4),
|
||||
Selection::from_offset(9),
|
||||
|
||||
@@ -3,10 +3,10 @@ use std::{cmp, ops::Range};
|
||||
use collections::HashMap;
|
||||
use futures::future::join_all;
|
||||
use gpui::{Hsla, Rgba};
|
||||
use itertools::Itertools;
|
||||
use language::point_from_lsp;
|
||||
use lsp::LanguageServerId;
|
||||
use multi_buffer::Anchor;
|
||||
use project::DocumentColor;
|
||||
use project::{DocumentColor, lsp_store::ColorFetchStrategy};
|
||||
use settings::Settings as _;
|
||||
use text::{Bias, BufferId, OffsetRangeExt as _};
|
||||
use ui::{App, Context, Window};
|
||||
@@ -19,6 +19,7 @@ use crate::{
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct LspColorData {
|
||||
cache_version_used: usize,
|
||||
colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
|
||||
inlay_colors: HashMap<InlayId, usize>,
|
||||
render_mode: DocumentColorsRenderMode,
|
||||
@@ -27,6 +28,7 @@ pub(super) struct LspColorData {
|
||||
impl LspColorData {
|
||||
pub fn new(cx: &App) -> Self {
|
||||
Self {
|
||||
cache_version_used: 0,
|
||||
colors: Vec::new(),
|
||||
inlay_colors: HashMap::default(),
|
||||
render_mode: EditorSettings::get_global(cx).lsp_document_colors,
|
||||
@@ -122,7 +124,7 @@ impl LspColorData {
|
||||
impl Editor {
|
||||
pub(super) fn refresh_colors(
|
||||
&mut self,
|
||||
for_server_id: Option<LanguageServerId>,
|
||||
ignore_cache: bool,
|
||||
buffer_id: Option<BufferId>,
|
||||
_: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -141,29 +143,41 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
let visible_buffers = self
|
||||
.visible_excerpts(None, cx)
|
||||
.into_values()
|
||||
.map(|(buffer, ..)| buffer)
|
||||
.filter(|editor_buffer| {
|
||||
buffer_id.is_none_or(|buffer_id| buffer_id == editor_buffer.read(cx).remote_id())
|
||||
})
|
||||
.unique_by(|buffer| buffer.read(cx).remote_id())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let all_colors_task = project.read(cx).lsp_store().update(cx, |lsp_store, cx| {
|
||||
self.buffer()
|
||||
.update(cx, |multi_buffer, cx| {
|
||||
multi_buffer
|
||||
.all_buffers()
|
||||
.into_iter()
|
||||
.filter(|editor_buffer| {
|
||||
buffer_id.is_none_or(|buffer_id| {
|
||||
buffer_id == editor_buffer.read(cx).remote_id()
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
visible_buffers
|
||||
.into_iter()
|
||||
.filter_map(|buffer| {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let colors_task = lsp_store.document_colors(for_server_id, buffer, cx)?;
|
||||
let fetch_strategy = if ignore_cache {
|
||||
ColorFetchStrategy::IgnoreCache
|
||||
} else {
|
||||
ColorFetchStrategy::UseCache {
|
||||
known_cache_version: self
|
||||
.colors
|
||||
.as_ref()
|
||||
.map(|colors| colors.cache_version_used),
|
||||
}
|
||||
};
|
||||
let colors_task = lsp_store.document_colors(fetch_strategy, buffer, cx)?;
|
||||
Some(async move { (buffer_id, colors_task.await) })
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
cx.spawn(async move |editor, cx| {
|
||||
let all_colors = join_all(all_colors_task).await;
|
||||
if all_colors.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Ok((multi_buffer_snapshot, editor_excerpts)) = editor.update(cx, |editor, cx| {
|
||||
let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let editor_excerpts = multi_buffer_snapshot.excerpts().fold(
|
||||
@@ -187,6 +201,7 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut cache_version = None;
|
||||
let mut new_editor_colors = Vec::<(Range<Anchor>, DocumentColor)>::new();
|
||||
for (buffer_id, colors) in all_colors {
|
||||
let Some(excerpts) = editor_excerpts.get(&buffer_id) else {
|
||||
@@ -194,7 +209,8 @@ impl Editor {
|
||||
};
|
||||
match colors {
|
||||
Ok(colors) => {
|
||||
for color in colors {
|
||||
cache_version = colors.cache_version;
|
||||
for color in colors.colors {
|
||||
let color_start = point_from_lsp(color.lsp_range.start);
|
||||
let color_end = point_from_lsp(color.lsp_range.end);
|
||||
|
||||
@@ -337,6 +353,9 @@ impl Editor {
|
||||
}
|
||||
|
||||
let mut updated = colors.set_colors(new_color_inlays);
|
||||
if let Some(cache_version) = cache_version {
|
||||
colors.cache_version_used = cache_version;
|
||||
}
|
||||
if colors.render_mode == DocumentColorsRenderMode::Inlay
|
||||
&& (!colors_splice.to_insert.is_empty()
|
||||
|| !colors_splice.to_remove.is_empty())
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor,
|
||||
EvaluateSelectedText, FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation,
|
||||
GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionExt,
|
||||
ToDisplayPoint, ToggleCodeActions,
|
||||
GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionEffects,
|
||||
SelectionExt, ToDisplayPoint, ToggleCodeActions,
|
||||
actions::{Format, FormatSelections},
|
||||
selections_collection::SelectionsCollection,
|
||||
};
|
||||
@@ -177,7 +177,7 @@ pub fn deploy_context_menu(
|
||||
let anchor = buffer.anchor_before(point.to_point(&display_map));
|
||||
if !display_ranges(&display_map, &editor.selections).any(|r| r.contains(&point)) {
|
||||
// Move the cursor to the clicked location so that dispatched actions make sense
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.clear_disjoint();
|
||||
s.set_pending_anchor_range(anchor..anchor, SelectMode::Character);
|
||||
});
|
||||
@@ -275,10 +275,10 @@ pub fn deploy_context_menu(
|
||||
cx,
|
||||
),
|
||||
None => {
|
||||
let character_size = editor.character_size(window);
|
||||
let character_size = editor.character_dimensions(window);
|
||||
let menu_position = MenuPosition::PinnedToEditor {
|
||||
source: source_anchor,
|
||||
offset: gpui::point(character_size.width, character_size.height),
|
||||
offset: gpui::point(character_size.em_width, character_size.line_height),
|
||||
};
|
||||
Some(MouseContextMenu::new(
|
||||
editor,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SemanticsProvider};
|
||||
use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SelectionEffects, SemanticsProvider};
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::HashSet;
|
||||
use futures::{channel::mpsc, future::join_all};
|
||||
@@ -213,7 +213,9 @@ impl ProposedChangesEditor {
|
||||
|
||||
self.buffer_entries = buffer_entries;
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, window, cx, |selections| selections.refresh());
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
|
||||
selections.refresh()
|
||||
});
|
||||
editor.buffer.update(cx, |buffer, cx| {
|
||||
for diff in new_diffs {
|
||||
buffer.add_diff(diff, cx)
|
||||
|
||||
@@ -487,8 +487,9 @@ impl Editor {
|
||||
if opened_first_time {
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
||||
editor.refresh_colors(false, None, window, cx);
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
@@ -599,6 +600,7 @@ impl Editor {
|
||||
);
|
||||
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
||||
self.refresh_colors(false, None, window, cx);
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<f32> {
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{rc::Rc, sync::LazyLock};
|
||||
|
||||
pub use crate::rust_analyzer_ext::expand_macro_recursively;
|
||||
use crate::{
|
||||
DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer,
|
||||
DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer, SelectionEffects,
|
||||
display_map::{
|
||||
Block, BlockPlacement, CustomBlockId, DisplayMap, DisplayRow, DisplaySnapshot,
|
||||
ToDisplayPoint,
|
||||
@@ -93,7 +93,9 @@ pub fn select_ranges(
|
||||
) {
|
||||
let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
|
||||
assert_eq!(editor.text(cx), unmarked_text);
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges(text_ranges));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges(text_ranges)
|
||||
});
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, RowExt,
|
||||
AnchorRangeExt, DisplayPoint, Editor, MultiBuffer, RowExt,
|
||||
display_map::{HighlightKey, ToDisplayPoint},
|
||||
};
|
||||
use buffer_diff::DiffHunkStatusKind;
|
||||
@@ -362,7 +362,7 @@ impl EditorTestContext {
|
||||
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
|
||||
self.editor.update_in(&mut self.cx, |editor, window, cx| {
|
||||
editor.set_text(unmarked_text, window, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_ranges(selection_ranges)
|
||||
})
|
||||
});
|
||||
@@ -379,7 +379,7 @@ impl EditorTestContext {
|
||||
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
|
||||
self.editor.update_in(&mut self.cx, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), unmarked_text);
|
||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_ranges(selection_ranges)
|
||||
})
|
||||
});
|
||||
|
||||
@@ -417,7 +417,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
|
||||
debug_adapter_extension::init(extension_host_proxy.clone(), cx);
|
||||
language_extension::init(extension_host_proxy.clone(), languages.clone());
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), cx);
|
||||
languages::init(languages.clone(), node_runtime.clone(), cx);
|
||||
prompt_store::init(cx);
|
||||
terminal_view::init(cx);
|
||||
|
||||
@@ -221,6 +221,9 @@ impl ExampleContext {
|
||||
ThreadEvent::ShowError(thread_error) => {
|
||||
tx.try_send(Err(anyhow!(thread_error.clone()))).ok();
|
||||
}
|
||||
ThreadEvent::RetriesFailed { .. } => {
|
||||
// Ignore retries failed events
|
||||
}
|
||||
ThreadEvent::Stopped(reason) => match reason {
|
||||
Ok(StopReason::EndTurn) => {
|
||||
tx.close_channel();
|
||||
|
||||
@@ -259,6 +259,36 @@ async fn copy_extension_resources(
|
||||
}
|
||||
}
|
||||
|
||||
if !manifest.debug_adapters.is_empty() {
|
||||
for (debug_adapter, entry) in &manifest.debug_adapters {
|
||||
let schema_path = entry.schema_path.clone().unwrap_or_else(|| {
|
||||
PathBuf::from("debug_adapter_schemas".to_owned())
|
||||
.join(debug_adapter.as_ref())
|
||||
.with_extension("json")
|
||||
});
|
||||
let parent = schema_path
|
||||
.parent()
|
||||
.with_context(|| format!("invalid empty schema path for {debug_adapter}"))?;
|
||||
fs::create_dir_all(output_dir.join(parent))?;
|
||||
copy_recursive(
|
||||
fs.as_ref(),
|
||||
&extension_path.join(&schema_path),
|
||||
&output_dir.join(&schema_path),
|
||||
CopyOptions {
|
||||
overwrite: true,
|
||||
ignore_if_exists: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to copy debug adapter schema '{}'",
|
||||
schema_path.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ use crate::{
|
||||
GrammarManifestEntry, RELOAD_DEBOUNCE_DURATION, SchemaVersion,
|
||||
};
|
||||
use async_compression::futures::bufread::GzipEncoder;
|
||||
use collections::BTreeMap;
|
||||
use collections::{BTreeMap, HashSet};
|
||||
use extension::ExtensionHostProxy;
|
||||
use fs::{FakeFs, Fs, RealFs};
|
||||
use futures::{AsyncReadExt, StreamExt, io::BufReader};
|
||||
use gpui::{AppContext as _, SemanticVersion, TestAppContext};
|
||||
use http_client::{FakeHttpClient, Response};
|
||||
use language::{BinaryStatus, LanguageMatcher, LanguageRegistry, LanguageServerStatusUpdate};
|
||||
use language::{BinaryStatus, LanguageMatcher, LanguageRegistry};
|
||||
use lsp::LanguageServerName;
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
@@ -720,20 +720,22 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
status_updates.next().await.unwrap(),
|
||||
status_updates.next().await.unwrap(),
|
||||
status_updates.next().await.unwrap(),
|
||||
status_updates.next().await.unwrap(),
|
||||
],
|
||||
[
|
||||
(
|
||||
LanguageServerName::new_static("gleam"),
|
||||
LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate)
|
||||
BinaryStatus::Starting
|
||||
),
|
||||
(
|
||||
LanguageServerName::new_static("gleam"),
|
||||
LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading)
|
||||
BinaryStatus::CheckingForUpdate
|
||||
),
|
||||
(
|
||||
LanguageServerName::new_static("gleam"),
|
||||
LanguageServerStatusUpdate::Binary(BinaryStatus::None)
|
||||
)
|
||||
BinaryStatus::Downloading
|
||||
),
|
||||
(LanguageServerName::new_static("gleam"), BinaryStatus::None)
|
||||
]
|
||||
);
|
||||
|
||||
@@ -794,7 +796,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
|
||||
// Start a new instance of the language server.
|
||||
project.update(cx, |project, cx| {
|
||||
project.restart_language_servers_for_buffers(vec![buffer.clone()], cx)
|
||||
project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
@@ -816,7 +818,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
project.update(cx, |project, cx| {
|
||||
project.restart_language_servers_for_buffers(vec![buffer.clone()], cx)
|
||||
project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)
|
||||
});
|
||||
|
||||
// The extension re-fetches the latest version of the language server.
|
||||
|
||||