Compare commits
99 Commits
hide-root
...
fix-launch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8308caf16c | ||
|
|
93632fbaf4 | ||
|
|
efa5f4c96a | ||
|
|
0d1d8a1437 | ||
|
|
6c4728f00f | ||
|
|
a3cc063107 | ||
|
|
7d5a5d0984 | ||
|
|
4c3ada5753 | ||
|
|
b3a8816c0e | ||
|
|
6d9bcdb2af | ||
|
|
0852912fd6 | ||
|
|
47ac01842b | ||
|
|
5b22994d9f | ||
|
|
6c0ea88f5b | ||
|
|
fc4ca346be | ||
|
|
e9570eefbf | ||
|
|
72de3143c8 | ||
|
|
ad206a6a97 | ||
|
|
1e1bc7c373 | ||
|
|
84eca53319 | ||
|
|
b4e558ce3d | ||
|
|
00a8101016 | ||
|
|
444f797827 | ||
|
|
7a14987c02 | ||
|
|
5eb68f0ea4 | ||
|
|
9c513223c4 | ||
|
|
0c0933d1c0 | ||
|
|
a4c5a2d4d3 | ||
|
|
311e136e30 | ||
|
|
4f5433a180 | ||
|
|
295db79c47 | ||
|
|
71d5c57119 | ||
|
|
dd17fd3d5a | ||
|
|
e4f8c4fb4c | ||
|
|
e62e9facf0 | ||
|
|
3f419b32f8 | ||
|
|
5270844b42 | ||
|
|
f567bb52ff | ||
|
|
c55630889a | ||
|
|
e0ca4270b4 | ||
|
|
02dfaf7799 | ||
|
|
c9972ca532 | ||
|
|
9334e152b4 | ||
|
|
9c47c52de5 | ||
|
|
286b97c0de | ||
|
|
415d482395 | ||
|
|
a9d0eee2a9 | ||
|
|
e4e3409952 | ||
|
|
46f98b6001 | ||
|
|
c1a4a24bce | ||
|
|
eb5f59577d | ||
|
|
2dad48d8d9 | ||
|
|
16853acbb1 | ||
|
|
64d649245c | ||
|
|
08210b512d | ||
|
|
6070aea6c0 | ||
|
|
16b44d53f9 | ||
|
|
3bed830a1f | ||
|
|
ee2a329981 | ||
|
|
6d64058fc6 | ||
|
|
7c2822a020 | ||
|
|
3db00384f4 | ||
|
|
3dfbd9e57c | ||
|
|
b103d7621b | ||
|
|
ab70e524c8 | ||
|
|
f0ce62ead8 | ||
|
|
f0345df479 | ||
|
|
bbd2262a93 | ||
|
|
c4fd9e1a6b | ||
|
|
0b7583bae5 | ||
|
|
e4bd115a63 | ||
|
|
fa54fa80d0 | ||
|
|
de16f2bbe6 | ||
|
|
e3b13b54c9 | ||
|
|
2c5d2a58d8 | ||
|
|
3485b7704b | ||
|
|
6801b9137f | ||
|
|
3853e83da7 | ||
|
|
047a7f5d29 | ||
|
|
8332e60ca9 | ||
|
|
afab4b522e | ||
|
|
0cb7dd2972 | ||
|
|
387281fa5b | ||
|
|
72bcb0beb7 | ||
|
|
4ff41ba62e | ||
|
|
16e901fb8f | ||
|
|
54b4587f9a | ||
|
|
1fe10117b7 | ||
|
|
78fd2685d5 | ||
|
|
da9e958b15 | ||
|
|
3908ca9744 | ||
|
|
6fe58a2c4e | ||
|
|
79e7ccc1fe | ||
|
|
0bc9478b46 | ||
|
|
4ac7935589 | ||
|
|
c75ad2fd11 | ||
|
|
365997d79d | ||
|
|
c57a6263aa | ||
|
|
ebea734515 |
9
.github/ISSUE_TEMPLATE/01_bug_ai.yml
vendored
9
.github/ISSUE_TEMPLATE/01_bug_ai.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Bug Report (AI Related)
|
||||
name: Bug Report (AI)
|
||||
description: Zed Agent Panel Bugs
|
||||
type: "Bug"
|
||||
labels: ["ai"]
|
||||
@@ -19,15 +19,14 @@ body:
|
||||
2.
|
||||
3.
|
||||
|
||||
Actual Behavior:
|
||||
Expected Behavior:
|
||||
**Expected Behavior**:
|
||||
**Actual Behavior**:
|
||||
|
||||
### Model Provider Details
|
||||
- Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc)
|
||||
- Model Name:
|
||||
- Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
|
||||
- MCP Servers in-use:
|
||||
- Other Details:
|
||||
- Other Details (MCPs, other settings, etc):
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
name: Bug Report (Edit Predictions)
|
||||
description: Zed Edit Predictions bugs
|
||||
type: "Bug"
|
||||
labels: ["ai", "inline completion", "zeta"]
|
||||
title: "Edit Predictions: <a short description of the Edit Prediction bug>"
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Describe the bug with a one line summary, and provide detailed reproduction steps
|
||||
value: |
|
||||
<!-- Please insert a one line summary of the issue below -->
|
||||
SUMMARY_SENTENCE_HERE
|
||||
|
||||
### Description
|
||||
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
|
||||
<!-- Please include the LLM provider and model name you are using -->
|
||||
Steps to trigger the problem:
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
Actual Behavior:
|
||||
Expected Behavior:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Zed Version and System Specs
|
||||
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
|
||||
placeholder: |
|
||||
Output of "zed: copy system specs into clipboard"
|
||||
validations:
|
||||
required: true
|
||||
35
.github/ISSUE_TEMPLATE/03_bug_git.yml
vendored
35
.github/ISSUE_TEMPLATE/03_bug_git.yml
vendored
@@ -1,35 +0,0 @@
|
||||
name: Bug Report (Git)
|
||||
description: Zed Git-Related Bugs
|
||||
type: "Bug"
|
||||
labels: ["git"]
|
||||
title: "Git: <a short description of the Git bug>"
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Describe the bug with a one line summary, and provide detailed reproduction steps
|
||||
value: |
|
||||
<!-- Please insert a one line summary of the issue below -->
|
||||
SUMMARY_SENTENCE_HERE
|
||||
|
||||
### Description
|
||||
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
|
||||
Steps to trigger the problem:
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
Actual Behavior:
|
||||
Expected Behavior:
|
||||
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Zed Version and System Specs
|
||||
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
|
||||
placeholder: |
|
||||
Output of "zed: copy system specs into clipboard"
|
||||
validations:
|
||||
required: true
|
||||
4
.github/ISSUE_TEMPLATE/04_bug_debugger.yml
vendored
4
.github/ISSUE_TEMPLATE/04_bug_debugger.yml
vendored
@@ -19,8 +19,8 @@ body:
|
||||
2.
|
||||
3.
|
||||
|
||||
Actual Behavior:
|
||||
Expected Behavior:
|
||||
**Expected Behavior**:
|
||||
**Actual Behavior**:
|
||||
|
||||
validations:
|
||||
required: true
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
@@ -18,14 +18,16 @@ body:
|
||||
- Issues with insufficient detail may be summarily closed.
|
||||
-->
|
||||
|
||||
DESCRIPTION_HERE
|
||||
|
||||
Steps to reproduce:
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
Expected Behavior:
|
||||
Actual Behavior:
|
||||
**Expected Behavior**:
|
||||
**Actual Behavior**:
|
||||
|
||||
<!-- Before Submitting, did you:
|
||||
1. Include settings.json, keymap.json, .editorconfig if relevant?
|
||||
|
||||
2
.github/actions/build_docs/action.yml
vendored
2
.github/actions/build_docs/action.yml
vendored
@@ -22,7 +22,7 @@ runs:
|
||||
- name: Check for broken links
|
||||
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
|
||||
with:
|
||||
args: --no-progress './docs/src/**/*'
|
||||
args: --no-progress --exclude '^http' './docs/src/**/*'
|
||||
fail: true
|
||||
|
||||
- name: Build book
|
||||
|
||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -801,6 +801,7 @@ jobs:
|
||||
name: Build with Nix
|
||||
uses: ./.github/workflows/nix.yml
|
||||
if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix')
|
||||
secrets: inherit
|
||||
with:
|
||||
flake-output: debug
|
||||
# excludes the final package to only cache dependencies
|
||||
|
||||
1
.github/workflows/eval.yml
vendored
1
.github/workflows/eval.yml
vendored
@@ -30,6 +30,7 @@ jobs:
|
||||
noop:
|
||||
name: No-op
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- name: No-op
|
||||
run: echo "Nothing to do"
|
||||
|
||||
1
.github/workflows/release_nightly.yml
vendored
1
.github/workflows/release_nightly.yml
vendored
@@ -214,6 +214,7 @@ jobs:
|
||||
bundle-nix:
|
||||
name: Build and cache Nix package
|
||||
needs: tests
|
||||
secrets: inherit
|
||||
uses: ./.github/workflows/nix.yml
|
||||
|
||||
update-nightly-tag:
|
||||
|
||||
3
.github/workflows/unit_evals.yml
vendored
3
.github/workflows/unit_evals.yml
vendored
@@ -19,6 +19,7 @@ env:
|
||||
|
||||
jobs:
|
||||
unit_evals:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
timeout-minutes: 60
|
||||
name: Run unit evals
|
||||
runs-on:
|
||||
@@ -62,7 +63,7 @@ jobs:
|
||||
|
||||
- name: Run unit evals
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo nextest run --workspace --no-fail-fast --features eval --no-capture -E 'test(::eval_)' --test-threads 1
|
||||
run: cargo nextest run --workspace --no-fail-fast --features eval --no-capture -E 'test(::eval_)'
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
|
||||
32
Cargo.lock
generated
32
Cargo.lock
generated
@@ -99,6 +99,7 @@ dependencies = [
|
||||
"paths",
|
||||
"picker",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"proto",
|
||||
@@ -704,6 +705,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"streaming_diff",
|
||||
"strsim",
|
||||
"task",
|
||||
@@ -3159,6 +3161,16 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "command-fds"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ec1052629a80c28594777d1252efc8a6b005d13f9edfd8c3fc0f44d5b32489a"
|
||||
dependencies = [
|
||||
"nix 0.30.1",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "command_palette"
|
||||
version = "0.1.0"
|
||||
@@ -4015,6 +4027,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"http_client",
|
||||
"language",
|
||||
"libc",
|
||||
"log",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
@@ -4038,7 +4051,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "dap-types"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/zed-industries/dap-types?rev=68516de327fa1be15214133a0a2e52a12982ce75#68516de327fa1be15214133a0a2e52a12982ce75"
|
||||
source = "git+https://github.com/zed-industries/dap-types?rev=b40956a7f4d1939da67429d941389ee306a3a308#b40956a7f4d1939da67429d941389ee306a3a308"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -4051,6 +4064,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"collections",
|
||||
"dap",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
@@ -4682,7 +4696,6 @@ dependencies = [
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"convert_case 0.8.0",
|
||||
"ctor",
|
||||
"dap",
|
||||
@@ -8990,7 +9003,6 @@ dependencies = [
|
||||
"tree-sitter-yaml",
|
||||
"unindent",
|
||||
"util",
|
||||
"which 6.0.3",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -10129,6 +10141,18 @@ dependencies = [
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases 0.2.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "node_runtime"
|
||||
version = "0.1.0"
|
||||
@@ -12109,7 +12133,6 @@ dependencies = [
|
||||
"unindent",
|
||||
"url",
|
||||
"util",
|
||||
"uuid",
|
||||
"which 6.0.3",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
@@ -17121,6 +17144,7 @@ dependencies = [
|
||||
"async-fs",
|
||||
"async_zip",
|
||||
"collections",
|
||||
"command-fds",
|
||||
"dirs 4.0.0",
|
||||
"dunce",
|
||||
"futures 0.3.31",
|
||||
|
||||
@@ -435,7 +435,7 @@ core-foundation-sys = "0.8.6"
|
||||
core-video = { version = "0.4.3", features = ["metal"] }
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
ctor = "0.4.0"
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "68516de327fa1be15214133a0a2e52a12982ce75" }
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b40956a7f4d1939da67429d941389ee306a3a308" }
|
||||
dashmap = "6.0"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
@@ -698,6 +698,8 @@ codegen-units = 16
|
||||
[profile.dev.package]
|
||||
taffy = { opt-level = 3 }
|
||||
cranelift-codegen = { opt-level = 3 }
|
||||
cranelift-codegen-meta = { opt-level = 3 }
|
||||
cranelift-codegen-shared = { opt-level = 3 }
|
||||
resvg = { opt-level = 3 }
|
||||
rustybuzz = { opt-level = 3 }
|
||||
ttf-parser = { opt-level = 3 }
|
||||
|
||||
1
assets/icons/circle_help.svg
Normal file
1
assets/icons/circle_help.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-circle-help-icon lucide-circle-help"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
|
||||
|
After Width: | Height: | Size: 348 B |
@@ -395,6 +395,8 @@
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePreviousItem",
|
||||
"insert": "vim::InsertBefore",
|
||||
".": "vim::Repeat",
|
||||
"alt-.": "vim::RepeatFind",
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode",
|
||||
@@ -421,6 +423,7 @@
|
||||
|
||||
"x": "editor::SelectLine",
|
||||
"shift-x": "editor::SelectLine",
|
||||
"%": "editor::SelectAll",
|
||||
// Window mode
|
||||
"space w h": "workspace::ActivatePaneLeft",
|
||||
"space w l": "workspace::ActivatePaneRight",
|
||||
@@ -450,7 +453,8 @@
|
||||
"ctrl-c": "editor::ToggleComments",
|
||||
"d": "vim::HelixDelete",
|
||||
"c": "vim::Substitute",
|
||||
"shift-c": "editor::AddSelectionBelow"
|
||||
"shift-c": "editor::AddSelectionBelow",
|
||||
"alt-shift-c": "editor::AddSelectionAbove"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -445,7 +445,9 @@
|
||||
// Whether to show breakpoints in the gutter.
|
||||
"breakpoints": true,
|
||||
// Whether to show fold buttons in the gutter.
|
||||
"folds": true
|
||||
"folds": true,
|
||||
// Minimum number of characters to reserve space for in the gutter.
|
||||
"min_line_number_digits": 4
|
||||
},
|
||||
"indent_guides": {
|
||||
// Whether to show indent guides in the editor.
|
||||
@@ -605,7 +607,7 @@
|
||||
// "never"
|
||||
"show": "always"
|
||||
},
|
||||
/// Hide main root dir
|
||||
// Whether to hide the root entry when only one folder is open in the window.
|
||||
"hide_root": false
|
||||
},
|
||||
"outline_panel": {
|
||||
@@ -1478,7 +1480,8 @@
|
||||
"Go": {
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
"debuggers": ["Delve"]
|
||||
},
|
||||
"GraphQL": {
|
||||
"prettier": {
|
||||
@@ -1543,9 +1546,15 @@
|
||||
"Plain Text": {
|
||||
"allow_rewrap": "anywhere"
|
||||
},
|
||||
"Python": {
|
||||
"debuggers": ["Debugpy"]
|
||||
},
|
||||
"Ruby": {
|
||||
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."]
|
||||
},
|
||||
"Rust": {
|
||||
"debuggers": ["CodeLLDB"]
|
||||
},
|
||||
"SCSS": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Static tasks configuration.
|
||||
// Project tasks configuration. See https://zed.dev/docs/tasks for documentation.
|
||||
//
|
||||
// Example:
|
||||
[
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"version_control.added": "#27a657ff",
|
||||
"version_control.modified": "#d3b020ff",
|
||||
"version_control.deleted": "#e06c76ff",
|
||||
"version_control.conflict_marker.ours": "#a1c1811a",
|
||||
"version_control.conflict_marker.theirs": "#74ade81a",
|
||||
"conflict": "#dec184ff",
|
||||
"conflict.background": "#dec1841a",
|
||||
"conflict.border": "#5d4c2fff",
|
||||
|
||||
@@ -109,5 +109,6 @@ gpui = { workspace = true, "features" = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, "features" = ["test-support"] }
|
||||
language_model = { workspace = true, "features" = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
|
||||
@@ -1605,6 +1605,7 @@ impl ActiveThread {
|
||||
|
||||
this.thread.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.cancel_last_completion(Some(window.window_handle()), cx);
|
||||
thread.send_to_model(
|
||||
model.model,
|
||||
CompletionIntent::UserPrompt,
|
||||
@@ -1788,12 +1789,31 @@ impl ActiveThread {
|
||||
|
||||
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let message_id = self.messages[ix];
|
||||
let Some(message) = self.thread.read(cx).message(message_id) else {
|
||||
let workspace = self.workspace.clone();
|
||||
let thread = self.thread.read(cx);
|
||||
|
||||
let is_first_message = ix == 0;
|
||||
let is_last_message = ix == self.messages.len() - 1;
|
||||
|
||||
let Some(message) = thread.message(message_id) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let is_generating = thread.is_generating();
|
||||
let is_generating_stale = thread.is_generation_stale().unwrap_or(false);
|
||||
|
||||
let loading_dots = (is_generating && is_last_message).then(|| {
|
||||
h_flex()
|
||||
.h_8()
|
||||
.my_3()
|
||||
.mx_5()
|
||||
.when(is_generating_stale || message.is_hidden, |this| {
|
||||
this.child(AnimatedLabel::new("").size(LabelSize::Small))
|
||||
})
|
||||
});
|
||||
|
||||
if message.is_hidden {
|
||||
return Empty.into_any();
|
||||
return div().children(loading_dots).into_any();
|
||||
}
|
||||
|
||||
let message_creases = message.creases.clone();
|
||||
@@ -1802,9 +1822,6 @@ impl ActiveThread {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let thread = self.thread.read(cx);
|
||||
|
||||
// Get all the data we need from thread before we start using it in closures
|
||||
let checkpoint = thread.checkpoint_for_message(message_id);
|
||||
let configured_model = thread.configured_model().map(|m| m.model);
|
||||
@@ -1815,14 +1832,6 @@ impl ActiveThread {
|
||||
|
||||
let tool_uses = thread.tool_uses_for_message(message_id, cx);
|
||||
let has_tool_uses = !tool_uses.is_empty();
|
||||
let is_generating = thread.is_generating();
|
||||
let is_generating_stale = thread.is_generation_stale().unwrap_or(false);
|
||||
|
||||
let is_first_message = ix == 0;
|
||||
let is_last_message = ix == self.messages.len() - 1;
|
||||
|
||||
let loading_dots = (is_generating_stale && is_last_message)
|
||||
.then(|| AnimatedLabel::new("").size(LabelSize::Small));
|
||||
|
||||
let editing_message_state = self
|
||||
.editing_message
|
||||
@@ -2238,17 +2247,7 @@ impl ActiveThread {
|
||||
parent.child(self.render_rules_item(cx))
|
||||
})
|
||||
.child(styled_message)
|
||||
.when(is_generating && is_last_message, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.h_8()
|
||||
.mt_2()
|
||||
.mb_4()
|
||||
.ml_4()
|
||||
.py_1p5()
|
||||
.when_some(loading_dots, |this, loading_dots| this.child(loading_dots)),
|
||||
)
|
||||
})
|
||||
.children(loading_dots)
|
||||
.when(show_feedback, move |parent| {
|
||||
parent.child(feedback_items).when_some(
|
||||
self.open_feedback_editors.get(&message_id),
|
||||
@@ -3708,7 +3707,7 @@ mod tests {
|
||||
use util::path;
|
||||
use workspace::CollaboratorId;
|
||||
|
||||
use crate::{ContextLoadResult, thread_store};
|
||||
use crate::{ContextLoadResult, thread::MessageSegment, thread_store};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -3842,6 +3841,114 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_editing_message_cancels_previous_completion(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
|
||||
let (cx, active_thread, _, thread, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
cx.update(|_, cx| {
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.set_default_model(
|
||||
Some(ConfiguredModel {
|
||||
provider: Arc::new(FakeLanguageModelProvider),
|
||||
model: model.clone(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Track thread events to verify cancellation
|
||||
let cancellation_events = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let new_request_events = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
|
||||
let _subscription = cx.update(|_, cx| {
|
||||
let cancellation_events = cancellation_events.clone();
|
||||
let new_request_events = new_request_events.clone();
|
||||
cx.subscribe(
|
||||
&thread,
|
||||
move |_thread, event: &ThreadEvent, _cx| match event {
|
||||
ThreadEvent::CompletionCanceled => {
|
||||
cancellation_events.lock().unwrap().push(());
|
||||
}
|
||||
ThreadEvent::NewRequest => {
|
||||
new_request_events.lock().unwrap().push(());
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
// Insert a user message and start streaming a response
|
||||
let message = thread.update(cx, |thread, cx| {
|
||||
let message_id = thread.insert_user_message(
|
||||
"Hello, how are you?",
|
||||
ContextLoadResult::default(),
|
||||
None,
|
||||
vec![],
|
||||
cx,
|
||||
);
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(
|
||||
model.clone(),
|
||||
CompletionIntent::UserPrompt,
|
||||
cx.active_window(),
|
||||
cx,
|
||||
);
|
||||
thread.message(message_id).cloned().unwrap()
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify that a completion is in progress
|
||||
assert!(cx.read(|cx| thread.read(cx).is_generating()));
|
||||
assert_eq!(new_request_events.lock().unwrap().len(), 1);
|
||||
|
||||
// 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,
|
||||
);
|
||||
let editor = active_thread
|
||||
.editing_message
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.1
|
||||
.editor
|
||||
.clone();
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_text("What is the weather like?", window, cx);
|
||||
});
|
||||
active_thread.confirm_editing_message(&Default::default(), window, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify that the previous completion was cancelled
|
||||
assert_eq!(cancellation_events.lock().unwrap().len(), 1);
|
||||
|
||||
// Verify that a new request was started after cancellation
|
||||
assert_eq!(new_request_events.lock().unwrap().len(), 2);
|
||||
|
||||
// Verify that the edited message contains the new text
|
||||
let edited_message =
|
||||
thread.update(cx, |thread, _| thread.message(message.id).cloned().unwrap());
|
||||
match &edited_message.segments[0] {
|
||||
MessageSegment::Text(text) => {
|
||||
assert_eq!(text, "What is the weather like?");
|
||||
}
|
||||
_ => panic!("Expected text segment"),
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test_settings(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
|
||||
@@ -12,7 +12,7 @@ use context_server::ContextServerId;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, ScrollHandle, Subscription, pulsating_between,
|
||||
Focusable, ScrollHandle, Subscription, Transformation, percentage,
|
||||
};
|
||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use project::context_server_store::{ContextServerStatus, ContextServerStore};
|
||||
@@ -475,7 +475,6 @@ impl AgentConfiguration {
|
||||
.get(&context_server_id)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
|
||||
let tools = tools_by_source
|
||||
.get(&ToolSource::ContextServer {
|
||||
id: context_server_id.0.clone().into(),
|
||||
@@ -484,25 +483,23 @@ impl AgentConfiguration {
|
||||
let tool_count = tools.len();
|
||||
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
let success_color = Color::Success.color(cx);
|
||||
|
||||
let (status_indicator, tooltip_text) = match server_status {
|
||||
ContextServerStatus::Starting => (
|
||||
Indicator::dot()
|
||||
.color(Color::Success)
|
||||
Icon::new(IconName::LoadCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 1.)),
|
||||
move |this, delta| this.color(success_color.alpha(delta).into()),
|
||||
Animation::new(Duration::from_secs(3)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element(),
|
||||
"Server is starting.",
|
||||
),
|
||||
ContextServerStatus::Running => (
|
||||
Indicator::dot().color(Color::Success).into_any_element(),
|
||||
"Server is running.",
|
||||
"Server is active.",
|
||||
),
|
||||
ContextServerStatus::Error(_) => (
|
||||
Indicator::dot().color(Color::Error).into_any_element(),
|
||||
@@ -526,12 +523,11 @@ impl AgentConfiguration {
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.when(
|
||||
error.is_some() || are_tools_expanded && tool_count > 1,
|
||||
error.is_some() || are_tools_expanded && tool_count >= 1,
|
||||
|element| element.border_b_1().border_color(border_color),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Disclosure::new(
|
||||
"tool-list-disclosure",
|
||||
@@ -551,12 +547,16 @@ impl AgentConfiguration {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id(item_id.clone())
|
||||
h_flex()
|
||||
.id(SharedString::from(format!("tooltip-{}", item_id)))
|
||||
.h_full()
|
||||
.w_3()
|
||||
.mx_1()
|
||||
.justify_center()
|
||||
.tooltip(Tooltip::text(tooltip_text))
|
||||
.child(status_indicator),
|
||||
)
|
||||
.child(Label::new(context_server_id.0.clone()).ml_0p5())
|
||||
.child(Label::new(item_id).ml_0p5().mr_1p5())
|
||||
.when(is_running, |this| {
|
||||
this.child(
|
||||
Label::new(if tool_count == 1 {
|
||||
|
||||
@@ -91,12 +91,13 @@ impl AgentModelSelector {
|
||||
|
||||
impl Render for AgentModelSelector {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let model = self.selector.read(cx).delegate.active_model(cx);
|
||||
let model_name = model
|
||||
.map(|model| model.model.name().0)
|
||||
.unwrap_or_else(|| SharedString::from("No model selected"));
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
Button::new("active-model", model_name)
|
||||
|
||||
@@ -386,8 +386,10 @@ impl CodegenAlternative {
|
||||
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
||||
} else {
|
||||
let request = self.build_request(&model, user_prompt, cx)?;
|
||||
cx.spawn(async move |_, cx| model.stream_completion_text(request.await, &cx).await)
|
||||
.boxed_local()
|
||||
cx.spawn(async move |_, cx| {
|
||||
Ok(model.stream_completion_text(request.await, &cx).await?)
|
||||
})
|
||||
.boxed_local()
|
||||
};
|
||||
self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
|
||||
Ok(())
|
||||
|
||||
@@ -71,6 +71,10 @@ fn show_configure_mcp_modal(
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, Workspace>,
|
||||
) {
|
||||
if !window.is_window_active() {
|
||||
return;
|
||||
}
|
||||
|
||||
let context_server_store = workspace.project().read(cx).context_server_store();
|
||||
let repository: Option<SharedString> = manifest.repository.as_ref().map(|s| s.clone().into());
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ impl Tool for ContextServerTool {
|
||||
arguments
|
||||
);
|
||||
let response = protocol
|
||||
.request::<context_server::types::request::CallTool>(
|
||||
.request::<context_server::types::requests::CallTool>(
|
||||
context_server::types::CallToolParams {
|
||||
name: tool_name,
|
||||
arguments,
|
||||
@@ -123,6 +123,9 @@ impl Tool for ContextServerTool {
|
||||
types::ToolResponseContent::Image { .. } => {
|
||||
log::warn!("Ignoring image content from tool response");
|
||||
}
|
||||
types::ToolResponseContent::Audio { .. } => {
|
||||
log::warn!("Ignoring audio content from tool response");
|
||||
}
|
||||
types::ToolResponseContent::Resource { .. } => {
|
||||
log::warn!("Ignoring resource content from tool response");
|
||||
}
|
||||
|
||||
@@ -38,8 +38,7 @@ use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use text::{OffsetRangeExt, ToPoint as _};
|
||||
use ui::prelude::*;
|
||||
use util::RangeExt;
|
||||
use util::ResultExt;
|
||||
use util::{RangeExt, ResultExt, maybe};
|
||||
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
|
||||
use zed_actions::agent::OpenConfiguration;
|
||||
|
||||
@@ -1171,27 +1170,31 @@ impl InlineAssistant {
|
||||
selections.select_anchor_ranges([position..position])
|
||||
});
|
||||
|
||||
let mut scroll_target_top;
|
||||
let mut scroll_target_bottom;
|
||||
let mut scroll_target_range = None;
|
||||
if let Some(decorations) = assist.decorations.as_ref() {
|
||||
scroll_target_top = editor
|
||||
.row_for_block(decorations.prompt_block_id, cx)
|
||||
.unwrap()
|
||||
.0 as f32;
|
||||
scroll_target_bottom = editor
|
||||
.row_for_block(decorations.end_block_id, cx)
|
||||
.unwrap()
|
||||
.0 as f32;
|
||||
} else {
|
||||
scroll_target_range = maybe!({
|
||||
let top = editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f32;
|
||||
let bottom = editor.row_for_block(decorations.end_block_id, cx)?.0 as f32;
|
||||
Some((top, bottom))
|
||||
});
|
||||
if scroll_target_range.is_none() {
|
||||
log::error!("bug: failed to find blocks for scrolling to inline assist");
|
||||
}
|
||||
}
|
||||
let scroll_target_range = scroll_target_range.unwrap_or_else(|| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let start_row = assist
|
||||
.range
|
||||
.start
|
||||
.to_display_point(&snapshot.display_snapshot)
|
||||
.row();
|
||||
scroll_target_top = start_row.0 as f32;
|
||||
scroll_target_bottom = scroll_target_top + 1.;
|
||||
}
|
||||
let top = start_row.0 as f32;
|
||||
let bottom = top + 1.0;
|
||||
(top, bottom)
|
||||
});
|
||||
let mut scroll_target_top = scroll_target_range.0;
|
||||
let mut scroll_target_bottom = scroll_target_range.1;
|
||||
|
||||
scroll_target_top -= editor.vertical_scroll_margin() as f32;
|
||||
scroll_target_bottom += editor.vertical_scroll_margin() as f32;
|
||||
|
||||
@@ -1331,7 +1334,7 @@ impl InlineAssistant {
|
||||
editor.clear_gutter_highlights::<GutterPendingRange>(cx);
|
||||
} else {
|
||||
editor.highlight_gutter::<GutterPendingRange>(
|
||||
&gutter_pending_ranges,
|
||||
gutter_pending_ranges,
|
||||
|cx| cx.theme().status().info_background,
|
||||
cx,
|
||||
)
|
||||
@@ -1342,7 +1345,7 @@ impl InlineAssistant {
|
||||
editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
|
||||
} else {
|
||||
editor.highlight_gutter::<GutterTransformedRange>(
|
||||
&gutter_transformed_ranges,
|
||||
gutter_transformed_ranges,
|
||||
|cx| cx.theme().status().info,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -722,6 +722,7 @@ impl MessageEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.flex_wrap()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -731,6 +732,7 @@ impl MessageEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.flex_wrap()
|
||||
.when(!incompatible_tools.is_empty(), |this| {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
|
||||
@@ -195,20 +195,20 @@ impl MessageSegment {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ProjectSnapshot {
|
||||
pub worktree_snapshots: Vec<WorktreeSnapshot>,
|
||||
pub unsaved_buffer_paths: Vec<String>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct WorktreeSnapshot {
|
||||
pub worktree_path: String,
|
||||
pub git_state: Option<GitState>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct GitState {
|
||||
pub remote_url: Option<String>,
|
||||
pub head_sha: Option<String>,
|
||||
@@ -247,7 +247,7 @@ impl LastRestoreCheckpoint {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
pub enum DetailedSummaryState {
|
||||
#[default]
|
||||
NotGenerated,
|
||||
@@ -391,7 +391,7 @@ impl ThreadSummary {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ExceededWindowError {
|
||||
/// Model used when last message exceeded context window
|
||||
model_id: LanguageModelId,
|
||||
@@ -1563,6 +1563,9 @@ impl Thread {
|
||||
Err(LanguageModelCompletionError::Other(error)) => {
|
||||
return Err(error);
|
||||
}
|
||||
Err(err @ LanguageModelCompletionError::RateLimit(..)) => {
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
|
||||
@@ -594,10 +594,11 @@ impl Render for ThreadHistory {
|
||||
view.pr_5()
|
||||
.child(
|
||||
uniform_list(
|
||||
cx.entity().clone(),
|
||||
"thread-history",
|
||||
self.list_item_count(),
|
||||
Self::list_items,
|
||||
cx.processor(|this, range: Range<usize>, window, cx| {
|
||||
this.list_items(range, window, cx)
|
||||
}),
|
||||
)
|
||||
.p_1()
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
|
||||
@@ -89,7 +89,7 @@ pub fn init(cx: &mut App) {
|
||||
pub struct SharedProjectContext(Rc<RefCell<Option<ProjectContext>>>);
|
||||
|
||||
impl SharedProjectContext {
|
||||
pub fn borrow(&self) -> Ref<Option<ProjectContext>> {
|
||||
pub fn borrow(&self) -> Ref<'_, Option<ProjectContext>> {
|
||||
self.0.borrow()
|
||||
}
|
||||
}
|
||||
@@ -562,7 +562,7 @@ impl ThreadStore {
|
||||
|
||||
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
|
||||
if let Some(response) = protocol
|
||||
.request::<context_server::types::request::ListTools>(())
|
||||
.request::<context_server::types::requests::ListTools>(())
|
||||
.await
|
||||
.log_err()
|
||||
{
|
||||
@@ -603,7 +603,7 @@ pub struct SerializedThreadMetadata {
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct SerializedThread {
|
||||
pub version: String,
|
||||
pub summary: SharedString,
|
||||
@@ -629,7 +629,7 @@ pub struct SerializedThread {
|
||||
pub profile: Option<AgentProfileId>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct SerializedLanguageModel {
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
@@ -690,11 +690,15 @@ impl SerializedThreadV0_1_0 {
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
SerializedThread { messages, ..self.0 }
|
||||
SerializedThread {
|
||||
messages,
|
||||
version: SerializedThread::VERSION.to_string(),
|
||||
..self.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SerializedMessage {
|
||||
pub id: MessageId,
|
||||
pub role: Role,
|
||||
@@ -712,7 +716,7 @@ pub struct SerializedMessage {
|
||||
pub is_hidden: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum SerializedMessageSegment {
|
||||
#[serde(rename = "text")]
|
||||
@@ -730,14 +734,14 @@ pub enum SerializedMessageSegment {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SerializedToolUse {
|
||||
pub id: LanguageModelToolUseId,
|
||||
pub name: SharedString,
|
||||
pub input: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SerializedToolResult {
|
||||
pub tool_use_id: LanguageModelToolUseId,
|
||||
pub is_error: bool,
|
||||
@@ -800,7 +804,7 @@ impl LegacySerializedMessage {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SerializedCrease {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
@@ -919,7 +923,7 @@ impl ThreadsDatabase {
|
||||
|
||||
fn bytes_encode(
|
||||
item: &Self::EItem,
|
||||
) -> Result<std::borrow::Cow<[u8]>, heed::BoxedError> {
|
||||
) -> Result<std::borrow::Cow<'_, [u8]>, heed::BoxedError> {
|
||||
serde_json::to_vec(&item.0)
|
||||
.map(std::borrow::Cow::Owned)
|
||||
.map_err(Into::into)
|
||||
@@ -1057,3 +1061,181 @@ impl ThreadsDatabase {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::thread::{DetailedSummaryState, MessageId};
|
||||
use chrono::Utc;
|
||||
use language_model::{Role, TokenUsage};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_legacy_serialized_thread_upgrade() {
|
||||
let updated_at = Utc::now();
|
||||
let legacy_thread = LegacySerializedThread {
|
||||
summary: "Test conversation".into(),
|
||||
updated_at,
|
||||
messages: vec![LegacySerializedMessage {
|
||||
id: MessageId(1),
|
||||
role: Role::User,
|
||||
text: "Hello, world!".to_string(),
|
||||
tool_uses: vec![],
|
||||
tool_results: vec![],
|
||||
}],
|
||||
initial_project_snapshot: None,
|
||||
};
|
||||
|
||||
let upgraded = legacy_thread.upgrade();
|
||||
|
||||
assert_eq!(
|
||||
upgraded,
|
||||
SerializedThread {
|
||||
summary: "Test conversation".into(),
|
||||
updated_at,
|
||||
messages: vec![SerializedMessage {
|
||||
id: MessageId(1),
|
||||
role: Role::User,
|
||||
segments: vec![SerializedMessageSegment::Text {
|
||||
text: "Hello, world!".to_string()
|
||||
}],
|
||||
tool_uses: vec![],
|
||||
tool_results: vec![],
|
||||
context: "".to_string(),
|
||||
creases: vec![],
|
||||
is_hidden: false
|
||||
}],
|
||||
version: SerializedThread::VERSION.to_string(),
|
||||
initial_project_snapshot: None,
|
||||
cumulative_token_usage: TokenUsage::default(),
|
||||
request_token_usage: vec![],
|
||||
detailed_summary_state: DetailedSummaryState::default(),
|
||||
exceeded_window_error: None,
|
||||
model: None,
|
||||
completion_mode: None,
|
||||
tool_use_limit_reached: false,
|
||||
profile: None
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialized_threadv0_1_0_upgrade() {
|
||||
let updated_at = Utc::now();
|
||||
let thread_v0_1_0 = SerializedThreadV0_1_0(SerializedThread {
|
||||
summary: "Test conversation".into(),
|
||||
updated_at,
|
||||
messages: vec![
|
||||
SerializedMessage {
|
||||
id: MessageId(1),
|
||||
role: Role::User,
|
||||
segments: vec![SerializedMessageSegment::Text {
|
||||
text: "Use tool_1".to_string(),
|
||||
}],
|
||||
tool_uses: vec![],
|
||||
tool_results: vec![],
|
||||
context: "".to_string(),
|
||||
creases: vec![],
|
||||
is_hidden: false,
|
||||
},
|
||||
SerializedMessage {
|
||||
id: MessageId(2),
|
||||
role: Role::Assistant,
|
||||
segments: vec![SerializedMessageSegment::Text {
|
||||
text: "I want to use a tool".to_string(),
|
||||
}],
|
||||
tool_uses: vec![SerializedToolUse {
|
||||
id: "abc".into(),
|
||||
name: "tool_1".into(),
|
||||
input: serde_json::Value::Null,
|
||||
}],
|
||||
tool_results: vec![],
|
||||
context: "".to_string(),
|
||||
creases: vec![],
|
||||
is_hidden: false,
|
||||
},
|
||||
SerializedMessage {
|
||||
id: MessageId(1),
|
||||
role: Role::User,
|
||||
segments: vec![SerializedMessageSegment::Text {
|
||||
text: "Here is the tool result".to_string(),
|
||||
}],
|
||||
tool_uses: vec![],
|
||||
tool_results: vec![SerializedToolResult {
|
||||
tool_use_id: "abc".into(),
|
||||
is_error: false,
|
||||
content: LanguageModelToolResultContent::Text("abcdef".into()),
|
||||
output: Some(serde_json::Value::Null),
|
||||
}],
|
||||
context: "".to_string(),
|
||||
creases: vec![],
|
||||
is_hidden: false,
|
||||
},
|
||||
],
|
||||
version: SerializedThreadV0_1_0::VERSION.to_string(),
|
||||
initial_project_snapshot: None,
|
||||
cumulative_token_usage: TokenUsage::default(),
|
||||
request_token_usage: vec![],
|
||||
detailed_summary_state: DetailedSummaryState::default(),
|
||||
exceeded_window_error: None,
|
||||
model: None,
|
||||
completion_mode: None,
|
||||
tool_use_limit_reached: false,
|
||||
profile: None,
|
||||
});
|
||||
let upgraded = thread_v0_1_0.upgrade();
|
||||
|
||||
assert_eq!(
|
||||
upgraded,
|
||||
SerializedThread {
|
||||
summary: "Test conversation".into(),
|
||||
updated_at,
|
||||
messages: vec![
|
||||
SerializedMessage {
|
||||
id: MessageId(1),
|
||||
role: Role::User,
|
||||
segments: vec![SerializedMessageSegment::Text {
|
||||
text: "Use tool_1".to_string()
|
||||
}],
|
||||
tool_uses: vec![],
|
||||
tool_results: vec![],
|
||||
context: "".to_string(),
|
||||
creases: vec![],
|
||||
is_hidden: false
|
||||
},
|
||||
SerializedMessage {
|
||||
id: MessageId(2),
|
||||
role: Role::Assistant,
|
||||
segments: vec![SerializedMessageSegment::Text {
|
||||
text: "I want to use a tool".to_string(),
|
||||
}],
|
||||
tool_uses: vec![SerializedToolUse {
|
||||
id: "abc".into(),
|
||||
name: "tool_1".into(),
|
||||
input: serde_json::Value::Null,
|
||||
}],
|
||||
tool_results: vec![SerializedToolResult {
|
||||
tool_use_id: "abc".into(),
|
||||
is_error: false,
|
||||
content: LanguageModelToolResultContent::Text("abcdef".into()),
|
||||
output: Some(serde_json::Value::Null),
|
||||
}],
|
||||
context: "".to_string(),
|
||||
creases: vec![],
|
||||
is_hidden: false,
|
||||
},
|
||||
],
|
||||
version: SerializedThread::VERSION.to_string(),
|
||||
initial_project_snapshot: None,
|
||||
cumulative_token_usage: TokenUsage::default(),
|
||||
request_token_usage: vec![],
|
||||
detailed_summary_state: DetailedSummaryState::default(),
|
||||
exceeded_window_error: None,
|
||||
model: None,
|
||||
completion_mode: None,
|
||||
tool_use_limit_reached: false,
|
||||
profile: None
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -406,6 +407,7 @@ impl RateLimit {
|
||||
/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
|
||||
#[derive(Debug)]
|
||||
pub struct RateLimitInfo {
|
||||
pub retry_after: Option<Duration>,
|
||||
pub requests: Option<RateLimit>,
|
||||
pub tokens: Option<RateLimit>,
|
||||
pub input_tokens: Option<RateLimit>,
|
||||
@@ -417,10 +419,11 @@ impl RateLimitInfo {
|
||||
// Check if any rate limit headers exist
|
||||
let has_rate_limit_headers = headers
|
||||
.keys()
|
||||
.any(|k| k.as_str().starts_with("anthropic-ratelimit-"));
|
||||
.any(|k| k == "retry-after" || k.as_str().starts_with("anthropic-ratelimit-"));
|
||||
|
||||
if !has_rate_limit_headers {
|
||||
return Self {
|
||||
retry_after: None,
|
||||
requests: None,
|
||||
tokens: None,
|
||||
input_tokens: None,
|
||||
@@ -429,6 +432,11 @@ impl RateLimitInfo {
|
||||
}
|
||||
|
||||
Self {
|
||||
retry_after: headers
|
||||
.get("retry-after")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.map(Duration::from_secs),
|
||||
requests: RateLimit::from_headers("requests", headers).ok(),
|
||||
tokens: RateLimit::from_headers("tokens", headers).ok(),
|
||||
input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
|
||||
@@ -481,8 +489,8 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
.send(request)
|
||||
.await
|
||||
.context("failed to send request to Anthropic")?;
|
||||
let rate_limits = RateLimitInfo::from_headers(response.headers());
|
||||
if response.status().is_success() {
|
||||
let rate_limits = RateLimitInfo::from_headers(response.headers());
|
||||
let reader = BufReader::new(response.into_body());
|
||||
let stream = reader
|
||||
.lines()
|
||||
@@ -500,6 +508,8 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
})
|
||||
.boxed();
|
||||
Ok((stream, Some(rate_limits)))
|
||||
} else if let Some(retry_after) = rate_limits.retry_after {
|
||||
Err(AnthropicError::RateLimit(retry_after))
|
||||
} else {
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
@@ -769,6 +779,8 @@ pub struct MessageDelta {
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AnthropicError {
|
||||
#[error("rate limit exceeded, retry after {0:?}")]
|
||||
RateLimit(Duration),
|
||||
#[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)]
|
||||
ApiError(ApiError),
|
||||
#[error("{0}")]
|
||||
|
||||
@@ -869,7 +869,7 @@ impl ContextStore {
|
||||
|
||||
if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
|
||||
if let Some(response) = protocol
|
||||
.request::<context_server::types::request::PromptsList>(())
|
||||
.request::<context_server::types::requests::PromptsList>(())
|
||||
.await
|
||||
.log_err()
|
||||
{
|
||||
|
||||
@@ -682,11 +682,12 @@ mod tests {
|
||||
_: &AsyncApp,
|
||||
) -> BoxFuture<
|
||||
'static,
|
||||
http_client::Result<
|
||||
Result<
|
||||
BoxStream<
|
||||
'static,
|
||||
http_client::Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||
>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
unimplemented!()
|
||||
|
||||
@@ -87,7 +87,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
let protocol = server.client().context("Context server not initialized")?;
|
||||
|
||||
let response = protocol
|
||||
.request::<context_server::types::request::CompletionComplete>(
|
||||
.request::<context_server::types::requests::CompletionComplete>(
|
||||
context_server::types::CompletionCompleteParams {
|
||||
reference: context_server::types::CompletionReference::Prompt(
|
||||
context_server::types::PromptReference {
|
||||
@@ -145,7 +145,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let protocol = server.client().context("Context server not initialized")?;
|
||||
let response = protocol
|
||||
.request::<context_server::types::request::PromptsGet>(
|
||||
.request::<context_server::types::requests::PromptsGet>(
|
||||
context_server::types::PromptsGetParams {
|
||||
name: prompt_name.clone(),
|
||||
arguments: Some(prompt_args),
|
||||
|
||||
@@ -46,15 +46,19 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
const KEYS_TO_REMOVE: [&str; 5] = [
|
||||
"format",
|
||||
"additionalProperties",
|
||||
"exclusiveMinimum",
|
||||
"exclusiveMaximum",
|
||||
"optional",
|
||||
const KEYS_TO_REMOVE: [(&str, fn(&Value) -> bool); 5] = [
|
||||
("format", |value| value.is_string()),
|
||||
("additionalProperties", |value| value.is_boolean()),
|
||||
("exclusiveMinimum", |value| value.is_number()),
|
||||
("exclusiveMaximum", |value| value.is_number()),
|
||||
("optional", |value| value.is_boolean()),
|
||||
];
|
||||
for key in KEYS_TO_REMOVE {
|
||||
obj.remove(key);
|
||||
for (key, predicate) in KEYS_TO_REMOVE {
|
||||
if let Some(value) = obj.get(key) {
|
||||
if predicate(value) {
|
||||
obj.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a type is not specified for an input parameter, add a default type
|
||||
@@ -153,6 +157,24 @@ mod tests {
|
||||
"type": "integer"
|
||||
})
|
||||
);
|
||||
|
||||
// Ensure that we do not remove keys that are actually supported (e.g. "format" can just be used as another property)
|
||||
let mut json = json!({
|
||||
"description": "A test field",
|
||||
"type": "integer",
|
||||
"format": {},
|
||||
});
|
||||
|
||||
adapt_to_json_schema_subset(&mut json).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
json,
|
||||
json!({
|
||||
"description": "A test field",
|
||||
"type": "integer",
|
||||
"format": {},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -80,6 +80,7 @@ rand.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
smol.workspace = true
|
||||
task = { workspace = true, features = ["test-support"]}
|
||||
tempfile.workspace = true
|
||||
theme.workspace = true
|
||||
|
||||
@@ -11,7 +11,7 @@ use client::{Client, UserStore};
|
||||
use collections::HashMap;
|
||||
use fs::FakeFs;
|
||||
use futures::{FutureExt, future::LocalBoxFuture};
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use gpui::{AppContext, TestAppContext, Timer};
|
||||
use indoc::{formatdoc, indoc};
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
@@ -1255,9 +1255,12 @@ impl EvalAssertion {
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let mut response = judge
|
||||
.stream_completion_text(request, &cx.to_async())
|
||||
.await?;
|
||||
let mut response = retry_on_rate_limit(async || {
|
||||
Ok(judge
|
||||
.stream_completion_text(request.clone(), &cx.to_async())
|
||||
.await?)
|
||||
})
|
||||
.await?;
|
||||
let mut output = String::new();
|
||||
while let Some(chunk) = response.stream.next().await {
|
||||
let chunk = chunk?;
|
||||
@@ -1308,10 +1311,17 @@ fn eval(
|
||||
run_eval(eval.clone(), tx.clone());
|
||||
|
||||
let executor = gpui::background_executor();
|
||||
let semaphore = Arc::new(smol::lock::Semaphore::new(32));
|
||||
for _ in 1..iterations {
|
||||
let eval = eval.clone();
|
||||
let tx = tx.clone();
|
||||
executor.spawn(async move { run_eval(eval, tx) }).detach();
|
||||
let semaphore = semaphore.clone();
|
||||
executor
|
||||
.spawn(async move {
|
||||
let _guard = semaphore.acquire().await;
|
||||
run_eval(eval, tx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
drop(tx);
|
||||
|
||||
@@ -1577,21 +1587,31 @@ impl EditAgentTest {
|
||||
if let Some(input_content) = eval.input_content.as_deref() {
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
|
||||
}
|
||||
let (edit_output, _) = self.agent.edit(
|
||||
buffer.clone(),
|
||||
eval.edit_file_input.display_description,
|
||||
&conversation,
|
||||
&mut cx.to_async(),
|
||||
);
|
||||
edit_output.await?
|
||||
retry_on_rate_limit(async || {
|
||||
self.agent
|
||||
.edit(
|
||||
buffer.clone(),
|
||||
eval.edit_file_input.display_description.clone(),
|
||||
&conversation,
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.0
|
||||
.await
|
||||
})
|
||||
.await?
|
||||
} else {
|
||||
let (edit_output, _) = self.agent.overwrite(
|
||||
buffer.clone(),
|
||||
eval.edit_file_input.display_description,
|
||||
&conversation,
|
||||
&mut cx.to_async(),
|
||||
);
|
||||
edit_output.await?
|
||||
retry_on_rate_limit(async || {
|
||||
self.agent
|
||||
.overwrite(
|
||||
buffer.clone(),
|
||||
eval.edit_file_input.display_description.clone(),
|
||||
&conversation,
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.0
|
||||
.await
|
||||
})
|
||||
.await?
|
||||
};
|
||||
|
||||
let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text());
|
||||
@@ -1613,6 +1633,26 @@ impl EditAgentTest {
|
||||
}
|
||||
}
|
||||
|
||||
async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) -> Result<R> {
|
||||
loop {
|
||||
match request().await {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(err) => match err.downcast::<LanguageModelCompletionError>() {
|
||||
Ok(err) => match err {
|
||||
LanguageModelCompletionError::RateLimit(duration) => {
|
||||
// Wait until after we are allowed to try again
|
||||
eprintln!("Rate limit exceeded. Waiting for {duration:?}...",);
|
||||
Timer::after(duration).await;
|
||||
continue;
|
||||
}
|
||||
_ => return Err(err.into()),
|
||||
},
|
||||
Err(err) => return Err(err),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
struct EvalAssertionOutcome {
|
||||
score: usize,
|
||||
|
||||
@@ -638,29 +638,36 @@ impl ToolCard for TerminalToolCard {
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_b_md()
|
||||
.text_ui_sm(cx)
|
||||
.child(
|
||||
ToolOutputPreview::new(
|
||||
terminal.clone().into_any_element(),
|
||||
terminal.entity_id(),
|
||||
)
|
||||
.with_total_lines(self.content_line_count)
|
||||
.toggle_state(!terminal.read(cx).is_content_limited(window))
|
||||
.on_toggle({
|
||||
let terminal = terminal.clone();
|
||||
move |is_expanded, _, cx| {
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
terminal.set_embedded_mode(
|
||||
if is_expanded {
|
||||
None
|
||||
} else {
|
||||
Some(COLLAPSED_LINES)
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
.child({
|
||||
let content_mode = terminal.read(cx).content_mode(window, cx);
|
||||
|
||||
if content_mode.is_scrollable() {
|
||||
div().h_72().child(terminal.clone()).into_any_element()
|
||||
} else {
|
||||
ToolOutputPreview::new(
|
||||
terminal.clone().into_any_element(),
|
||||
terminal.entity_id(),
|
||||
)
|
||||
.with_total_lines(self.content_line_count)
|
||||
.toggle_state(!content_mode.is_limited())
|
||||
.on_toggle({
|
||||
let terminal = terminal.clone();
|
||||
move |is_expanded, _, cx| {
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
terminal.set_embedded_mode(
|
||||
if is_expanded {
|
||||
None
|
||||
} else {
|
||||
Some(COLLAPSED_LINES)
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -452,6 +452,10 @@ impl Model {
|
||||
| Model::Claude3_5SonnetV2
|
||||
| Model::Claude3_7Sonnet
|
||||
| Model::Claude3_7SonnetThinking
|
||||
| Model::ClaudeSonnet4
|
||||
| Model::ClaudeSonnet4Thinking
|
||||
| Model::ClaudeOpus4
|
||||
| Model::ClaudeOpus4Thinking
|
||||
| Model::Claude3Haiku
|
||||
| Model::Claude3Opus
|
||||
| Model::Claude3Sonnet
|
||||
|
||||
@@ -111,7 +111,7 @@ pub struct ChannelMembership {
|
||||
pub role: proto::ChannelRole,
|
||||
}
|
||||
impl ChannelMembership {
|
||||
pub fn sort_key(&self) -> MembershipSortKey {
|
||||
pub fn sort_key(&self) -> MembershipSortKey<'_> {
|
||||
MembershipSortKey {
|
||||
role_order: match self.role {
|
||||
proto::ChannelRole::Admin => 0,
|
||||
|
||||
@@ -32,7 +32,7 @@ impl ChannelIndex {
|
||||
.retain(|channel_id| !channels.contains(channel_id));
|
||||
}
|
||||
|
||||
pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard {
|
||||
pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard<'_> {
|
||||
ChannelPathsInsertGuard {
|
||||
channels_ordered: &mut self.channels_ordered,
|
||||
channels_by_id: &mut self.channels_by_id,
|
||||
|
||||
@@ -39,7 +39,7 @@ enum ProxyType<'t> {
|
||||
HttpProxy(HttpProxyType<'t>),
|
||||
}
|
||||
|
||||
fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType)> {
|
||||
fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType<'_>)> {
|
||||
let scheme = proxy.scheme();
|
||||
let host = proxy.host()?.to_string();
|
||||
let port = proxy.port_or_known_default()?;
|
||||
|
||||
@@ -501,8 +501,10 @@ impl Database {
|
||||
|
||||
/// Returns all channels for the user with the given ID.
|
||||
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
|
||||
self.transaction(|tx| async move { self.get_user_channels(user_id, None, true, &tx).await })
|
||||
.await
|
||||
self.weak_transaction(
|
||||
|tx| async move { self.get_user_channels(user_id, None, true, &tx).await },
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all channels for the user with the given ID that are descendants
|
||||
|
||||
@@ -15,7 +15,7 @@ impl Database {
|
||||
user_b_busy: bool,
|
||||
}
|
||||
|
||||
self.transaction(|tx| async move {
|
||||
self.weak_transaction(|tx| async move {
|
||||
let user_a_participant = Alias::new("user_a_participant");
|
||||
let user_b_participant = Alias::new("user_b_participant");
|
||||
let mut db_contacts = contact::Entity::find()
|
||||
@@ -91,7 +91,7 @@ impl Database {
|
||||
|
||||
/// Returns whether the given user is a busy (on a call).
|
||||
pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> {
|
||||
self.transaction(|tx| async move {
|
||||
self.weak_transaction(|tx| async move {
|
||||
let participant = room_participant::Entity::find()
|
||||
.filter(room_participant::Column::UserId.eq(user_id))
|
||||
.one(&*tx)
|
||||
|
||||
@@ -80,7 +80,7 @@ impl Database {
|
||||
&self,
|
||||
user_id: UserId,
|
||||
) -> Result<Option<proto::IncomingCall>> {
|
||||
self.transaction(|tx| async move {
|
||||
self.weak_transaction(|tx| async move {
|
||||
let pending_participant = room_participant::Entity::find()
|
||||
.filter(
|
||||
room_participant::Column::UserId
|
||||
|
||||
@@ -7,6 +7,12 @@ pub use token::*;
|
||||
|
||||
pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial";
|
||||
|
||||
/// The name of the feature flag that bypasses the account age check.
|
||||
pub const BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG: &str = "bypass-account-age-check";
|
||||
|
||||
/// The minimum account age an account must have in order to use the LLM service.
|
||||
pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30);
|
||||
|
||||
/// The default value to use for maximum spend per month if the user did not
|
||||
/// explicitly set a maximum spend.
|
||||
///
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use crate::db::{billing_customer, billing_subscription, user};
|
||||
use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG;
|
||||
use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG};
|
||||
use crate::{Config, db::billing_preference};
|
||||
use anyhow::{Context as _, Result};
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
@@ -84,7 +84,7 @@ impl LlmTokenClaims {
|
||||
.any(|flag| flag == "llm-closed-beta"),
|
||||
bypass_account_age_check: feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == "bypass-account-age-check"),
|
||||
.any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG),
|
||||
can_use_web_search_tool: true,
|
||||
use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"),
|
||||
plan,
|
||||
|
||||
@@ -4,7 +4,10 @@ use crate::api::billing::find_or_create_billing_customer;
|
||||
use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use crate::llm::db::LlmDatabase;
|
||||
use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, LlmTokenClaims};
|
||||
use crate::llm::{
|
||||
AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG, LlmTokenClaims,
|
||||
MIN_ACCOUNT_AGE_FOR_LLM_USE,
|
||||
};
|
||||
use crate::stripe_client::StripeCustomerId;
|
||||
use crate::{
|
||||
AppState, Error, Result, auth,
|
||||
@@ -65,7 +68,7 @@ use std::{
|
||||
rc::Rc,
|
||||
sync::{
|
||||
Arc, OnceLock,
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -86,10 +89,36 @@ pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
const MESSAGE_COUNT_PER_PAGE: usize = 100;
|
||||
const MAX_MESSAGE_LEN: usize = 1024;
|
||||
const NOTIFICATION_COUNT_PER_PAGE: usize = 50;
|
||||
const MAX_CONCURRENT_CONNECTIONS: usize = 512;
|
||||
|
||||
static CONCURRENT_CONNECTIONS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
type MessageHandler =
|
||||
Box<dyn Send + Sync + Fn(Box<dyn AnyTypedEnvelope>, Session) -> BoxFuture<'static, ()>>;
|
||||
|
||||
pub struct ConnectionGuard;
|
||||
|
||||
impl ConnectionGuard {
|
||||
pub fn try_acquire() -> Result<Self, ()> {
|
||||
let current_connections = CONCURRENT_CONNECTIONS.fetch_add(1, SeqCst);
|
||||
if current_connections >= MAX_CONCURRENT_CONNECTIONS {
|
||||
CONCURRENT_CONNECTIONS.fetch_sub(1, SeqCst);
|
||||
tracing::error!(
|
||||
"too many concurrent connections: {}",
|
||||
current_connections + 1
|
||||
);
|
||||
return Err(());
|
||||
}
|
||||
Ok(ConnectionGuard)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConnectionGuard {
|
||||
fn drop(&mut self) {
|
||||
CONCURRENT_CONNECTIONS.fetch_sub(1, SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
struct Response<R> {
|
||||
peer: Arc<Peer>,
|
||||
receipt: Receipt<R>,
|
||||
@@ -722,6 +751,7 @@ impl Server {
|
||||
system_id: Option<String>,
|
||||
send_connection_id: Option<oneshot::Sender<ConnectionId>>,
|
||||
executor: Executor,
|
||||
connection_guard: Option<ConnectionGuard>,
|
||||
) -> impl Future<Output = ()> + use<> {
|
||||
let this = self.clone();
|
||||
let span = info_span!("handle connection", %address,
|
||||
@@ -742,6 +772,7 @@ impl Server {
|
||||
tracing::error!("server is tearing down");
|
||||
return
|
||||
}
|
||||
|
||||
let (connection_id, handle_io, mut incoming_rx) = this
|
||||
.peer
|
||||
.add_connection(connection, {
|
||||
@@ -783,6 +814,7 @@ impl Server {
|
||||
tracing::error!(?error, "failed to send initial client update");
|
||||
return;
|
||||
}
|
||||
drop(connection_guard);
|
||||
|
||||
let handle_io = handle_io.fuse();
|
||||
futures::pin_mut!(handle_io);
|
||||
@@ -1154,6 +1186,19 @@ pub async fn handle_websocket_request(
|
||||
}
|
||||
|
||||
let socket_address = socket_address.to_string();
|
||||
|
||||
// Acquire connection guard before WebSocket upgrade
|
||||
let connection_guard = match ConnectionGuard::try_acquire() {
|
||||
Ok(guard) => guard,
|
||||
Err(()) => {
|
||||
return (
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"Too many concurrent connections",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
ws.on_upgrade(move |socket| {
|
||||
let socket = socket
|
||||
.map_ok(to_tungstenite_message)
|
||||
@@ -1171,6 +1216,7 @@ pub async fn handle_websocket_request(
|
||||
system_id_header.map(|header| header.to_string()),
|
||||
None,
|
||||
Executor::Production,
|
||||
Some(connection_guard),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -2773,8 +2819,12 @@ async fn make_update_user_plan_message(
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let account_too_young =
|
||||
!matches!(plan, proto::Plan::ZedPro) && user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE;
|
||||
let bypass_account_age_check = feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG);
|
||||
let account_too_young = !matches!(plan, proto::Plan::ZedPro)
|
||||
&& !bypass_account_age_check
|
||||
&& user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE;
|
||||
|
||||
Ok(proto::UpdateUserPlan {
|
||||
plan: plan.into(),
|
||||
@@ -4075,9 +4125,6 @@ async fn accept_terms_of_service(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The minimum account age an account must have in order to use the LLM service.
|
||||
pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30);
|
||||
|
||||
async fn get_llm_api_token(
|
||||
_request: proto::GetLlmToken,
|
||||
response: Response<proto::GetLlmToken>,
|
||||
|
||||
@@ -671,7 +671,7 @@ async fn test_remote_server_debugger(
|
||||
});
|
||||
|
||||
session.update(cx_a, |session, _| {
|
||||
assert_eq!(session.binary().command, "ssh");
|
||||
assert_eq!(session.binary().command.as_deref(), Some("ssh"));
|
||||
});
|
||||
|
||||
let shutdown_session = workspace.update(cx_a, |workspace, cx| {
|
||||
|
||||
@@ -258,6 +258,7 @@ impl TestServer {
|
||||
None,
|
||||
Some(connection_id_tx),
|
||||
Executor::Deterministic(cx.background_executor().clone()),
|
||||
None,
|
||||
))
|
||||
.detach();
|
||||
let connection_id = connection_id_rx.await.map_err(|e| {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::client::Client;
|
||||
use crate::types::{self, Request};
|
||||
use crate::types::{self, Notification, Request};
|
||||
|
||||
pub struct ModelContextProtocol {
|
||||
inner: Client,
|
||||
@@ -20,9 +20,10 @@ impl ModelContextProtocol {
|
||||
}
|
||||
|
||||
fn supported_protocols() -> Vec<types::ProtocolVersion> {
|
||||
vec![types::ProtocolVersion(
|
||||
types::LATEST_PROTOCOL_VERSION.to_string(),
|
||||
)]
|
||||
vec![
|
||||
types::ProtocolVersion(types::LATEST_PROTOCOL_VERSION.to_string()),
|
||||
types::ProtocolVersion(types::VERSION_2024_11_05.to_string()),
|
||||
]
|
||||
}
|
||||
|
||||
pub async fn initialize(
|
||||
@@ -42,7 +43,7 @@ impl ModelContextProtocol {
|
||||
|
||||
let response: types::InitializeResponse = self
|
||||
.inner
|
||||
.request(types::request::Initialize::METHOD, params)
|
||||
.request(types::requests::Initialize::METHOD, params)
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
@@ -53,16 +54,13 @@ impl ModelContextProtocol {
|
||||
|
||||
log::trace!("mcp server info {:?}", response.server_info);
|
||||
|
||||
self.inner.notify(
|
||||
types::NotificationType::Initialized.as_str(),
|
||||
serde_json::json!({}),
|
||||
)?;
|
||||
|
||||
let initialized_protocol = InitializedContextServerProtocol {
|
||||
inner: self.inner,
|
||||
initialize: response,
|
||||
};
|
||||
|
||||
initialized_protocol.notify::<types::notifications::Initialized>(())?;
|
||||
|
||||
Ok(initialized_protocol)
|
||||
}
|
||||
}
|
||||
@@ -96,4 +94,8 @@ impl InitializedContextServerProtocol {
|
||||
pub async fn request<T: Request>(&self, params: T::Params) -> Result<T::Response> {
|
||||
self.inner.request(T::METHOD, params).await
|
||||
}
|
||||
|
||||
pub fn notify<T: Notification>(&self, params: T::Params) -> Result<()> {
|
||||
self.inner.notify(T::METHOD, params)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ pub fn create_fake_transport(
|
||||
executor: BackgroundExecutor,
|
||||
) -> FakeTransport {
|
||||
let name = name.into();
|
||||
FakeTransport::new(executor).on_request::<crate::types::request::Initialize>(move |_params| {
|
||||
FakeTransport::new(executor).on_request::<crate::types::requests::Initialize>(move |_params| {
|
||||
create_initialize_response(name.clone())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
pub const LATEST_PROTOCOL_VERSION: &str = "2024-11-05";
|
||||
pub const LATEST_PROTOCOL_VERSION: &str = "2025-03-26";
|
||||
pub const VERSION_2024_11_05: &str = "2024-11-05";
|
||||
|
||||
pub mod request {
|
||||
pub mod requests {
|
||||
use super::*;
|
||||
|
||||
macro_rules! request {
|
||||
@@ -82,6 +83,57 @@ pub trait Request {
|
||||
const METHOD: &'static str;
|
||||
}
|
||||
|
||||
pub mod notifications {
|
||||
use super::*;
|
||||
|
||||
macro_rules! notification {
|
||||
($method:expr, $name:ident, $params:ty) => {
|
||||
pub struct $name;
|
||||
|
||||
impl Notification for $name {
|
||||
type Params = $params;
|
||||
const METHOD: &'static str = $method;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
notification!("notifications/initialized", Initialized, ());
|
||||
notification!("notifications/progress", Progress, ProgressParams);
|
||||
notification!("notifications/message", Message, MessageParams);
|
||||
notification!(
|
||||
"notifications/resources/updated",
|
||||
ResourcesUpdated,
|
||||
ResourcesUpdatedParams
|
||||
);
|
||||
notification!(
|
||||
"notifications/resources/list_changed",
|
||||
ResourcesListChanged,
|
||||
()
|
||||
);
|
||||
notification!("notifications/tools/list_changed", ToolsListChanged, ());
|
||||
notification!("notifications/prompts/list_changed", PromptsListChanged, ());
|
||||
notification!("notifications/roots/list_changed", RootsListChanged, ());
|
||||
}
|
||||
|
||||
pub trait Notification {
|
||||
type Params: DeserializeOwned + Serialize + Send + Sync + 'static;
|
||||
const METHOD: &'static str;
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageParams {
|
||||
pub level: LoggingLevel,
|
||||
pub logger: Option<String>,
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResourcesUpdatedParams {
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ProtocolVersion(pub String);
|
||||
@@ -291,13 +343,20 @@ pub enum MessageContent {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
annotations: Option<MessageAnnotations>,
|
||||
},
|
||||
#[serde(rename = "image")]
|
||||
#[serde(rename = "image", rename_all = "camelCase")]
|
||||
Image {
|
||||
data: String,
|
||||
mime_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
annotations: Option<MessageAnnotations>,
|
||||
},
|
||||
#[serde(rename = "audio", rename_all = "camelCase")]
|
||||
Audio {
|
||||
data: String,
|
||||
mime_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
annotations: Option<MessageAnnotations>,
|
||||
},
|
||||
#[serde(rename = "resource")]
|
||||
Resource {
|
||||
resource: ResourceContents,
|
||||
@@ -394,6 +453,8 @@ pub struct ServerCapabilities {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logging: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub completions: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prompts: Option<PromptsCapabilities>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resources: Option<ResourcesCapabilities>,
|
||||
@@ -438,6 +499,28 @@ pub struct Tool {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub input_schema: serde_json::Value,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<ToolAnnotations>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolAnnotations {
|
||||
/// A human-readable title for the tool.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
/// If true, the tool does not modify its environment.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub read_only_hint: Option<bool>,
|
||||
/// If true, the tool may perform destructive updates to its environment.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub destructive_hint: Option<bool>,
|
||||
/// If true, calling the tool repeatedly with the same arguments will have no additional effect on its environment.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub idempotent_hint: Option<bool>,
|
||||
/// If true, this tool may interact with an "open world" of external entities.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub open_world_hint: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -528,34 +611,6 @@ pub struct ModelHint {
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum NotificationType {
|
||||
Initialized,
|
||||
Progress,
|
||||
Message,
|
||||
ResourcesUpdated,
|
||||
ResourcesListChanged,
|
||||
ToolsListChanged,
|
||||
PromptsListChanged,
|
||||
RootsListChanged,
|
||||
}
|
||||
|
||||
impl NotificationType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
NotificationType::Initialized => "notifications/initialized",
|
||||
NotificationType::Progress => "notifications/progress",
|
||||
NotificationType::Message => "notifications/message",
|
||||
NotificationType::ResourcesUpdated => "notifications/resources/updated",
|
||||
NotificationType::ResourcesListChanged => "notifications/resources/list_changed",
|
||||
NotificationType::ToolsListChanged => "notifications/tools/list_changed",
|
||||
NotificationType::PromptsListChanged => "notifications/prompts/list_changed",
|
||||
NotificationType::RootsListChanged => "notifications/roots/list_changed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ClientNotification {
|
||||
@@ -576,12 +631,14 @@ pub enum ProgressToken {
|
||||
Number(f64),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProgressParams {
|
||||
pub progress_token: ProgressToken,
|
||||
pub progress: f64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub total: Option<f64>,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<HashMap<String, serde_json::Value>>,
|
||||
@@ -625,6 +682,8 @@ pub enum ToolResponseContent {
|
||||
Text { text: String },
|
||||
#[serde(rename = "image", rename_all = "camelCase")]
|
||||
Image { data: String, mime_type: String },
|
||||
#[serde(rename = "audio", rename_all = "camelCase")]
|
||||
Audio { data: String, mime_type: String },
|
||||
#[serde(rename = "resource")]
|
||||
Resource { resource: ResourceContents },
|
||||
}
|
||||
|
||||
@@ -408,24 +408,30 @@ impl Copilot {
|
||||
let proxy_url = copilot_settings.proxy.clone()?;
|
||||
let no_verify = copilot_settings.proxy_no_verify;
|
||||
let http_or_https_proxy = if proxy_url.starts_with("http:") {
|
||||
"HTTP_PROXY"
|
||||
Some("HTTP_PROXY")
|
||||
} else if proxy_url.starts_with("https:") {
|
||||
"HTTPS_PROXY"
|
||||
Some("HTTPS_PROXY")
|
||||
} else {
|
||||
log::error!(
|
||||
"Unsupported protocol scheme for language server proxy (must be http or https)"
|
||||
);
|
||||
return None;
|
||||
None
|
||||
};
|
||||
|
||||
let mut env = HashMap::default();
|
||||
env.insert(http_or_https_proxy.to_string(), proxy_url);
|
||||
|
||||
if let Some(true) = no_verify {
|
||||
env.insert("NODE_TLS_REJECT_UNAUTHORIZED".to_string(), "0".to_string());
|
||||
};
|
||||
if let Some(proxy_type) = http_or_https_proxy {
|
||||
env.insert(proxy_type.to_string(), proxy_url);
|
||||
if let Some(true) = no_verify {
|
||||
env.insert("NODE_TLS_REJECT_UNAUTHORIZED".to_string(), "0".to_string());
|
||||
};
|
||||
}
|
||||
|
||||
Some(env)
|
||||
if let Ok(oauth_token) = env::var(copilot_chat::COPILOT_OAUTH_ENV_VAR) {
|
||||
env.insert(copilot_chat::COPILOT_OAUTH_ENV_VAR.to_string(), oauth_token);
|
||||
}
|
||||
|
||||
if env.is_empty() { None } else { Some(env) }
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
||||
@@ -16,6 +16,8 @@ use paths::home_dir;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::watch_config_dir;
|
||||
|
||||
pub const COPILOT_OAUTH_ENV_VAR: &str = "GH_COPILOT_TOKEN";
|
||||
|
||||
#[derive(Default, Clone, Debug, PartialEq)]
|
||||
pub struct CopilotChatSettings {
|
||||
pub api_url: Arc<str>,
|
||||
@@ -405,13 +407,19 @@ impl CopilotChat {
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Self {
|
||||
oauth_token: None,
|
||||
let this = Self {
|
||||
oauth_token: std::env::var(COPILOT_OAUTH_ENV_VAR).ok(),
|
||||
api_token: None,
|
||||
models: None,
|
||||
settings,
|
||||
client,
|
||||
};
|
||||
if this.oauth_token.is_some() {
|
||||
cx.spawn(async move |this, mut cx| Self::update_models(&this, &mut cx).await)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
async fn update_models(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
|
||||
|
||||
@@ -51,6 +51,9 @@ telemetry.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
libc.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
async-pipe.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -181,7 +181,7 @@ impl DebugTaskDefinition {
|
||||
/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct DebugAdapterBinary {
|
||||
pub command: String,
|
||||
pub command: Option<String>,
|
||||
pub arguments: Vec<String>,
|
||||
pub envs: HashMap<String, String>,
|
||||
pub cwd: Option<PathBuf>,
|
||||
@@ -369,6 +369,10 @@ pub trait DebugAdapter: 'static + Send + Sync {
|
||||
}
|
||||
|
||||
async fn dap_schema(&self) -> serde_json::Value;
|
||||
|
||||
fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -433,7 +437,7 @@ impl DebugAdapter for FakeAdapter {
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
Ok(DebugAdapterBinary {
|
||||
command: "command".into(),
|
||||
command: Some("command".into()),
|
||||
arguments: vec![],
|
||||
connection: None,
|
||||
envs: HashMap::default(),
|
||||
|
||||
@@ -297,7 +297,7 @@ mod tests {
|
||||
let client = DebugAdapterClient::start(
|
||||
crate::client::SessionId(1),
|
||||
DebugAdapterBinary {
|
||||
command: "command".into(),
|
||||
command: Some("command".into()),
|
||||
arguments: Default::default(),
|
||||
envs: Default::default(),
|
||||
connection: None,
|
||||
@@ -367,7 +367,7 @@ mod tests {
|
||||
let client = DebugAdapterClient::start(
|
||||
crate::client::SessionId(1),
|
||||
DebugAdapterBinary {
|
||||
command: "command".into(),
|
||||
command: Some("command".into()),
|
||||
arguments: Default::default(),
|
||||
envs: Default::default(),
|
||||
connection: None,
|
||||
@@ -420,7 +420,7 @@ mod tests {
|
||||
let client = DebugAdapterClient::start(
|
||||
crate::client::SessionId(1),
|
||||
DebugAdapterBinary {
|
||||
command: "command".into(),
|
||||
command: Some("command".into()),
|
||||
arguments: Default::default(),
|
||||
envs: Default::default(),
|
||||
connection: None,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use dap_types::{
|
||||
ErrorResponse,
|
||||
messages::{Message, Response},
|
||||
@@ -12,7 +12,6 @@ use smol::{
|
||||
io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader},
|
||||
lock::Mutex,
|
||||
net::{TcpListener, TcpStream},
|
||||
process::Child,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@@ -22,7 +21,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use util::{ConnectionResult, ResultExt as _};
|
||||
use util::ConnectionResult;
|
||||
|
||||
use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
|
||||
|
||||
@@ -86,10 +85,12 @@ impl Transport {
|
||||
TcpTransport::start(binary, cx)
|
||||
.await
|
||||
.map(|(transports, tcp)| (transports, Self::Tcp(tcp)))
|
||||
.context("Tried to connect to a debug adapter via TCP transport layer")
|
||||
} else {
|
||||
StdioTransport::start(binary, cx)
|
||||
.await
|
||||
.map(|(transports, stdio)| (transports, Self::Stdio(stdio)))
|
||||
.context("Tried to connect to a debug adapter via stdin/stdout transport layer")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +103,7 @@ impl Transport {
|
||||
}
|
||||
}
|
||||
|
||||
async fn kill(&self) -> Result<()> {
|
||||
async fn kill(&self) {
|
||||
match self {
|
||||
Transport::Stdio(stdio_transport) => stdio_transport.kill().await,
|
||||
Transport::Tcp(tcp_transport) => tcp_transport.kill().await,
|
||||
@@ -191,7 +192,7 @@ impl TransportDelegate {
|
||||
match Self::handle_output(
|
||||
params.output,
|
||||
client_tx,
|
||||
pending_requests,
|
||||
pending_requests.clone(),
|
||||
output_log_handler,
|
||||
)
|
||||
.await
|
||||
@@ -199,6 +200,12 @@ impl TransportDelegate {
|
||||
Ok(()) => {}
|
||||
Err(e) => log::error!("Error handling debugger output: {e}"),
|
||||
}
|
||||
let mut pending_requests = pending_requests.lock().await;
|
||||
pending_requests.drain().for_each(|(_, request)| {
|
||||
request
|
||||
.send(Err(anyhow!("debugger shutdown unexpectedly")))
|
||||
.ok();
|
||||
});
|
||||
}));
|
||||
|
||||
if let Some(stderr) = params.stderr.take() {
|
||||
@@ -531,7 +538,7 @@ impl TransportDelegate {
|
||||
current_requests.clear();
|
||||
pending_requests.clear();
|
||||
|
||||
let _ = self.transport.kill().await.log_err();
|
||||
self.transport.kill().await;
|
||||
|
||||
drop(current_requests);
|
||||
drop(pending_requests);
|
||||
@@ -562,7 +569,7 @@ pub struct TcpTransport {
|
||||
pub port: u16,
|
||||
pub host: Ipv4Addr,
|
||||
pub timeout: u64,
|
||||
process: Mutex<Child>,
|
||||
process: Option<Mutex<Child>>,
|
||||
}
|
||||
|
||||
impl TcpTransport {
|
||||
@@ -591,26 +598,23 @@ impl TcpTransport {
|
||||
let host = connection_args.host;
|
||||
let port = connection_args.port;
|
||||
|
||||
let mut command = util::command::new_std_command(&binary.command);
|
||||
util::set_pre_exec_to_start_new_session(&mut command);
|
||||
let mut command = smol::process::Command::from(command);
|
||||
let mut process = if let Some(command) = &binary.command {
|
||||
let mut command = util::command::new_std_command(&command);
|
||||
|
||||
if let Some(cwd) = &binary.cwd {
|
||||
command.current_dir(cwd);
|
||||
}
|
||||
if let Some(cwd) = &binary.cwd {
|
||||
command.current_dir(cwd);
|
||||
}
|
||||
|
||||
command.args(&binary.arguments);
|
||||
command.envs(&binary.envs);
|
||||
command.args(&binary.arguments);
|
||||
command.envs(&binary.envs);
|
||||
|
||||
command
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let mut process = command
|
||||
.spawn()
|
||||
.with_context(|| "failed to start debug adapter.")?;
|
||||
Some(
|
||||
Child::spawn(command, Stdio::null())
|
||||
.with_context(|| "failed to start debug adapter.")?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let address = SocketAddrV4::new(host, port);
|
||||
|
||||
@@ -628,15 +632,18 @@ impl TcpTransport {
|
||||
match TcpStream::connect(address).await {
|
||||
Ok(stream) => return Ok((process, stream.split())),
|
||||
Err(_) => {
|
||||
if let Ok(Some(_)) = process.try_status() {
|
||||
let output = process.output().await?;
|
||||
let output = if output.stderr.is_empty() {
|
||||
String::from_utf8_lossy(&output.stdout).to_string()
|
||||
} else {
|
||||
String::from_utf8_lossy(&output.stderr).to_string()
|
||||
};
|
||||
anyhow::bail!("{output}\nerror: process exited before debugger attached.");
|
||||
if let Some(p) = &mut process {
|
||||
if let Ok(Some(_)) = p.try_status() {
|
||||
let output = process.take().unwrap().into_inner().output().await?;
|
||||
let output = if output.stderr.is_empty() {
|
||||
String::from_utf8_lossy(&output.stdout).to_string()
|
||||
} else {
|
||||
String::from_utf8_lossy(&output.stderr).to_string()
|
||||
};
|
||||
anyhow::bail!("{output}\nerror: process exited before debugger attached.");
|
||||
}
|
||||
}
|
||||
|
||||
cx.background_executor().timer(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
@@ -649,13 +656,13 @@ impl TcpTransport {
|
||||
host,
|
||||
port
|
||||
);
|
||||
let stdout = process.stdout.take();
|
||||
let stderr = process.stderr.take();
|
||||
let stdout = process.as_mut().and_then(|p| p.stdout.take());
|
||||
let stderr = process.as_mut().and_then(|p| p.stderr.take());
|
||||
|
||||
let this = Self {
|
||||
port,
|
||||
host,
|
||||
process: Mutex::new(process),
|
||||
process: process.map(Mutex::new),
|
||||
timeout,
|
||||
};
|
||||
|
||||
@@ -673,10 +680,19 @@ impl TcpTransport {
|
||||
true
|
||||
}
|
||||
|
||||
async fn kill(&self) -> Result<()> {
|
||||
self.process.lock().await.kill()?;
|
||||
async fn kill(&self) {
|
||||
if let Some(process) = &self.process {
|
||||
let mut process = process.lock().await;
|
||||
Child::kill(&mut process);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
impl Drop for TcpTransport {
|
||||
fn drop(&mut self) {
|
||||
if let Some(mut p) = self.process.take() {
|
||||
p.get_mut().kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,9 +703,12 @@ pub struct StdioTransport {
|
||||
impl StdioTransport {
|
||||
#[allow(dead_code, reason = "This is used in non test builds of Zed")]
|
||||
async fn start(binary: &DebugAdapterBinary, _: AsyncApp) -> Result<(TransportPipe, Self)> {
|
||||
let mut command = util::command::new_std_command(&binary.command);
|
||||
util::set_pre_exec_to_start_new_session(&mut command);
|
||||
let mut command = smol::process::Command::from(command);
|
||||
let Some(binary_command) = &binary.command else {
|
||||
bail!(
|
||||
"When using the `stdio` transport, the path to a debug adapter binary must be set by Zed."
|
||||
);
|
||||
};
|
||||
let mut command = util::command::new_std_command(&binary_command);
|
||||
|
||||
if let Some(cwd) = &binary.cwd {
|
||||
command.current_dir(cwd);
|
||||
@@ -698,16 +717,10 @@ impl StdioTransport {
|
||||
command.args(&binary.arguments);
|
||||
command.envs(&binary.envs);
|
||||
|
||||
command
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let mut process = command.spawn().with_context(|| {
|
||||
let mut process = Child::spawn(command, Stdio::piped()).with_context(|| {
|
||||
format!(
|
||||
"failed to spawn command `{} {}`.",
|
||||
binary.command,
|
||||
binary_command,
|
||||
binary.arguments.join(" ")
|
||||
)
|
||||
})?;
|
||||
@@ -722,7 +735,7 @@ impl StdioTransport {
|
||||
if stderr.is_none() {
|
||||
bail!(
|
||||
"Failed to connect to stderr for debug adapter command {}",
|
||||
&binary.command
|
||||
&binary_command
|
||||
);
|
||||
}
|
||||
|
||||
@@ -745,9 +758,15 @@ impl StdioTransport {
|
||||
false
|
||||
}
|
||||
|
||||
async fn kill(&self) -> Result<()> {
|
||||
self.process.lock().await.kill()?;
|
||||
Ok(())
|
||||
async fn kill(&self) {
|
||||
let mut process = self.process.lock().await;
|
||||
Child::kill(&mut process);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for StdioTransport {
|
||||
fn drop(&mut self) {
|
||||
self.process.get_mut().kill();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -921,7 +940,66 @@ impl FakeTransport {
|
||||
false
|
||||
}
|
||||
|
||||
async fn kill(&self) -> Result<()> {
|
||||
Ok(())
|
||||
async fn kill(&self) {}
|
||||
}
|
||||
|
||||
struct Child {
|
||||
process: smol::process::Child,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Child {
|
||||
type Target = smol::process::Child;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.process
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for Child {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.process
|
||||
}
|
||||
}
|
||||
|
||||
impl Child {
|
||||
fn into_inner(self) -> smol::process::Child {
|
||||
self.process
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn spawn(mut command: std::process::Command, stdin: Stdio) -> Result<Self> {
|
||||
util::set_pre_exec_to_start_new_session(&mut command);
|
||||
let process = smol::process::Command::from(command)
|
||||
.stdin(stdin)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
Ok(Self { process })
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn spawn(command: std::process::Command, stdin: Stdio) -> Result<Self> {
|
||||
// TODO(windows): create a job object and add the child process handle to it,
|
||||
// see https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects
|
||||
let process = smol::process::Command::from(command)
|
||||
.stdin(stdin)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
Ok(Self { process })
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn kill(&mut self) {
|
||||
let pid = self.process.id();
|
||||
unsafe {
|
||||
libc::killpg(pid as i32, libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn kill(&mut self) {
|
||||
// TODO(windows): terminate the job object in kill
|
||||
let _ = self.process.kill();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ doctest = false
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
dap.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
|
||||
@@ -21,18 +21,21 @@ impl CodeLldbDebugAdapter {
|
||||
|
||||
fn request_args(
|
||||
&self,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
task_definition: &DebugTaskDefinition,
|
||||
) -> Result<dap::StartDebuggingRequestArguments> {
|
||||
// CodeLLDB uses `name` for a terminal label.
|
||||
let mut configuration = task_definition.config.clone();
|
||||
|
||||
configuration
|
||||
let obj = configuration
|
||||
.as_object_mut()
|
||||
.context("CodeLLDB is not a valid json object")?
|
||||
.insert(
|
||||
"name".into(),
|
||||
Value::String(String::from(task_definition.label.as_ref())),
|
||||
);
|
||||
.context("CodeLLDB is not a valid json object")?;
|
||||
|
||||
obj.entry("name")
|
||||
.or_insert(Value::String(String::from(task_definition.label.as_ref())));
|
||||
|
||||
obj.entry("cwd")
|
||||
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
|
||||
|
||||
let request = self.request_kind(&configuration)?;
|
||||
|
||||
@@ -359,13 +362,13 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
};
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: command.unwrap(),
|
||||
command: Some(command.unwrap()),
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
arguments: vec![
|
||||
"--settings".into(),
|
||||
json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
|
||||
],
|
||||
request_args: self.request_args(&config)?,
|
||||
request_args: self.request_args(delegate, &config)?,
|
||||
envs: HashMap::default(),
|
||||
connection: None,
|
||||
})
|
||||
|
||||
@@ -177,18 +177,23 @@ impl DebugAdapter for GdbDebugAdapter {
|
||||
|
||||
let gdb_path = user_setting_path.unwrap_or(gdb_path?);
|
||||
|
||||
let request_args = StartDebuggingRequestArguments {
|
||||
request: self.request_kind(&config.config)?,
|
||||
configuration: config.config.clone(),
|
||||
};
|
||||
let mut configuration = config.config.clone();
|
||||
if let Some(configuration) = configuration.as_object_mut() {
|
||||
configuration
|
||||
.entry("cwd")
|
||||
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
|
||||
}
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: gdb_path,
|
||||
command: Some(gdb_path),
|
||||
arguments: vec!["-i=dap".into()],
|
||||
envs: HashMap::default(),
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
connection: None,
|
||||
request_args,
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
request: self.request_kind(&config.config)?,
|
||||
configuration,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,14 +462,21 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
]
|
||||
};
|
||||
|
||||
let mut configuration = task_definition.config.clone();
|
||||
if let Some(configuration) = configuration.as_object_mut() {
|
||||
configuration
|
||||
.entry("cwd")
|
||||
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
|
||||
}
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: minidelve_path.to_string_lossy().into_owned(),
|
||||
command: Some(minidelve_path.to_string_lossy().into_owned()),
|
||||
arguments,
|
||||
cwd: Some(cwd),
|
||||
envs: HashMap::default(),
|
||||
connection: None,
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration: task_definition.config.clone(),
|
||||
configuration,
|
||||
request: self.request_kind(&task_definition.config)?,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ use adapters::latest_github_release;
|
||||
use anyhow::Context as _;
|
||||
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
|
||||
use gpui::AsyncApp;
|
||||
use serde_json::Value;
|
||||
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
|
||||
use task::DebugRequest;
|
||||
use util::ResultExt;
|
||||
@@ -68,13 +69,24 @@ impl JsDebugAdapter {
|
||||
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
|
||||
let mut configuration = task_definition.config.clone();
|
||||
if let Some(configuration) = configuration.as_object_mut() {
|
||||
configuration
|
||||
.entry("cwd")
|
||||
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
|
||||
|
||||
configuration.entry("type").and_modify(normalize_task_type);
|
||||
}
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: delegate
|
||||
.node_runtime()
|
||||
.binary_path()
|
||||
.await?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
command: Some(
|
||||
delegate
|
||||
.node_runtime()
|
||||
.binary_path()
|
||||
.await?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
),
|
||||
arguments: vec![
|
||||
adapter_path
|
||||
.join(Self::ADAPTER_PATH)
|
||||
@@ -91,7 +103,7 @@ impl JsDebugAdapter {
|
||||
timeout,
|
||||
}),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration: task_definition.config.clone(),
|
||||
configuration,
|
||||
request: self.request_kind(&task_definition.config)?,
|
||||
},
|
||||
})
|
||||
@@ -171,7 +183,7 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "edge", "pwa-edge"],
|
||||
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge"],
|
||||
"description": "The type of debug session",
|
||||
"default": "pwa-node"
|
||||
},
|
||||
@@ -431,4 +443,25 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
self.get_installed_binary(delegate, &config, user_installed_path, cx)
|
||||
.await
|
||||
}
|
||||
|
||||
fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
|
||||
let label = args.configuration.get("name")?.as_str()?;
|
||||
Some(label.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_task_type(task_type: &mut Value) {
|
||||
let Some(task_type_str) = task_type.as_str() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let new_name = match task_type_str {
|
||||
"node" | "pwa-node" => "pwa-node",
|
||||
"chrome" | "pwa-chrome" => "pwa-chrome",
|
||||
"edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
|
||||
_ => task_type_str,
|
||||
}
|
||||
.to_owned();
|
||||
|
||||
*task_type = Value::String(new_name);
|
||||
}
|
||||
|
||||
@@ -71,13 +71,21 @@ impl PhpDebugAdapter {
|
||||
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
|
||||
let mut configuration = task_definition.config.clone();
|
||||
if let Some(obj) = configuration.as_object_mut() {
|
||||
obj.entry("cwd")
|
||||
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
|
||||
}
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: delegate
|
||||
.node_runtime()
|
||||
.binary_path()
|
||||
.await?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
command: Some(
|
||||
delegate
|
||||
.node_runtime()
|
||||
.binary_path()
|
||||
.await?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
),
|
||||
arguments: vec![
|
||||
adapter_path
|
||||
.join(Self::ADAPTER_PATH)
|
||||
@@ -93,7 +101,7 @@ impl PhpDebugAdapter {
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
envs: HashMap::default(),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration: task_definition.config.clone(),
|
||||
configuration,
|
||||
request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -83,6 +83,7 @@ impl PythonDebugAdapter {
|
||||
|
||||
fn request_args(
|
||||
&self,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
task_definition: &DebugTaskDefinition,
|
||||
) -> Result<StartDebuggingRequestArguments> {
|
||||
let request = self.request_kind(&task_definition.config)?;
|
||||
@@ -95,6 +96,11 @@ impl PythonDebugAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(obj) = configuration.as_object_mut() {
|
||||
obj.entry("cwd")
|
||||
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
|
||||
}
|
||||
|
||||
Ok(StartDebuggingRequestArguments {
|
||||
configuration,
|
||||
request,
|
||||
@@ -187,7 +193,7 @@ impl PythonDebugAdapter {
|
||||
);
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: python_command,
|
||||
command: Some(python_command),
|
||||
arguments,
|
||||
connection: Some(adapters::TcpArguments {
|
||||
host,
|
||||
@@ -196,7 +202,7 @@ impl PythonDebugAdapter {
|
||||
}),
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
envs: HashMap::default(),
|
||||
request_args: self.request_args(config)?,
|
||||
request_args: self.request_args(delegate, config)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, bail};
|
||||
use async_trait::async_trait;
|
||||
use collections::FxHashMap;
|
||||
use dap::{
|
||||
DebugRequest, StartDebuggingRequestArguments,
|
||||
DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
|
||||
adapters::{
|
||||
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
|
||||
},
|
||||
};
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use language::LanguageName;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::{ffi::OsStr, sync::Arc};
|
||||
use task::{DebugScenario, ZedDebugConfig};
|
||||
use util::command::new_smol_command;
|
||||
|
||||
@@ -21,6 +23,18 @@ impl RubyDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "Ruby";
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct RubyDebugConfig {
|
||||
script_or_command: Option<String>,
|
||||
script: Option<String>,
|
||||
command: Option<String>,
|
||||
#[serde(default)]
|
||||
args: Vec<String>,
|
||||
#[serde(default)]
|
||||
env: FxHashMap<String, String>,
|
||||
cwd: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for RubyDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
@@ -31,185 +45,70 @@ impl DebugAdapter for RubyDebugAdapter {
|
||||
Some(SharedString::new_static("Ruby").into())
|
||||
}
|
||||
|
||||
fn request_kind(&self, _: &serde_json::Value) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
Ok(StartDebuggingRequestArgumentsRequest::Launch)
|
||||
}
|
||||
|
||||
async fn dap_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"oneOf": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["request"],
|
||||
"properties": {
|
||||
"request": {
|
||||
"type": "string",
|
||||
"enum": ["launch"],
|
||||
"description": "Request to launch a new process"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["script"],
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
|
||||
"default": "ruby"
|
||||
},
|
||||
"script": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to a Ruby file."
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "Directory to execute the program in",
|
||||
"default": "${ZED_WORKTREE_ROOT}"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"description": "Command line arguments passed to the program",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"description": "Additional environment variables to pass to the debugging (and debugged) process",
|
||||
"default": {}
|
||||
},
|
||||
"showProtocolLog": {
|
||||
"type": "boolean",
|
||||
"description": "Show a log of DAP requests, events, and responses",
|
||||
"default": false
|
||||
},
|
||||
"useBundler": {
|
||||
"type": "boolean",
|
||||
"description": "Execute Ruby programs with `bundle exec` instead of directly",
|
||||
"default": false
|
||||
},
|
||||
"bundlePath": {
|
||||
"type": "string",
|
||||
"description": "Location of the bundle executable"
|
||||
},
|
||||
"rdbgPath": {
|
||||
"type": "string",
|
||||
"description": "Location of the rdbg executable"
|
||||
},
|
||||
"askParameters": {
|
||||
"type": "boolean",
|
||||
"description": "Ask parameters at first."
|
||||
},
|
||||
"debugPort": {
|
||||
"type": "string",
|
||||
"description": "UNIX domain socket name or TPC/IP host:port"
|
||||
},
|
||||
"waitLaunchTime": {
|
||||
"type": "number",
|
||||
"description": "Wait time before connection in milliseconds"
|
||||
},
|
||||
"localfs": {
|
||||
"type": "boolean",
|
||||
"description": "true if the VSCode and debugger run on a same machine",
|
||||
"default": false
|
||||
},
|
||||
"useTerminal": {
|
||||
"type": "boolean",
|
||||
"description": "Create a new terminal and then execute commands there",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["request"],
|
||||
"properties": {
|
||||
"request": {
|
||||
"type": "string",
|
||||
"enum": ["attach"],
|
||||
"description": "Request to attach to an existing process"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"rdbgPath": {
|
||||
"type": "string",
|
||||
"description": "Location of the rdbg executable"
|
||||
},
|
||||
"debugPort": {
|
||||
"type": "string",
|
||||
"description": "UNIX domain socket name or TPC/IP host:port"
|
||||
},
|
||||
"showProtocolLog": {
|
||||
"type": "boolean",
|
||||
"description": "Show a log of DAP requests, events, and responses",
|
||||
"default": false
|
||||
},
|
||||
"localfs": {
|
||||
"type": "boolean",
|
||||
"description": "true if the VSCode and debugger run on a same machine",
|
||||
"default": false
|
||||
},
|
||||
"localfsMap": {
|
||||
"type": "string",
|
||||
"description": "Specify pairs of remote root path and local root path like `/remote_dir:/local_dir`. You can specify multiple pairs like `/rem1:/loc1,/rem2:/loc2` by concatenating with `,`."
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"description": "Additional environment variables to pass to the rdbg process",
|
||||
"default": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"script": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to a Ruby file."
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "Directory to execute the program in",
|
||||
"default": "${ZED_WORKTREE_ROOT}"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"description": "Command line arguments passed to the program",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"description": "Additional environment variables to pass to the debugging (and debugged) process",
|
||||
"default": {}
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
let mut config = serde_json::Map::new();
|
||||
|
||||
match &zed_scenario.request {
|
||||
match zed_scenario.request {
|
||||
DebugRequest::Launch(launch) => {
|
||||
config.insert("request".to_string(), json!("launch"));
|
||||
config.insert("script".to_string(), json!(launch.program));
|
||||
config.insert("command".to_string(), json!("ruby"));
|
||||
let config = RubyDebugConfig {
|
||||
script_or_command: Some(launch.program),
|
||||
script: None,
|
||||
command: None,
|
||||
args: launch.args,
|
||||
env: launch.env,
|
||||
cwd: launch.cwd.clone(),
|
||||
};
|
||||
|
||||
if !launch.args.is_empty() {
|
||||
config.insert("args".to_string(), json!(launch.args));
|
||||
}
|
||||
let config = serde_json::to_value(config)?;
|
||||
|
||||
if !launch.env.is_empty() {
|
||||
config.insert("env".to_string(), json!(launch.env));
|
||||
}
|
||||
|
||||
if let Some(cwd) = &launch.cwd {
|
||||
config.insert("cwd".to_string(), json!(cwd));
|
||||
}
|
||||
|
||||
// Ruby stops on entry so there's no need to handle that case
|
||||
Ok(DebugScenario {
|
||||
adapter: zed_scenario.adapter,
|
||||
label: zed_scenario.label,
|
||||
config,
|
||||
tcp_connection: None,
|
||||
build: None,
|
||||
})
|
||||
}
|
||||
DebugRequest::Attach(attach) => {
|
||||
config.insert("request".to_string(), json!("attach"));
|
||||
|
||||
config.insert("processId".to_string(), json!(attach.process_id));
|
||||
DebugRequest::Attach(_) => {
|
||||
anyhow::bail!("Attach requests are unsupported");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(DebugScenario {
|
||||
adapter: zed_scenario.adapter,
|
||||
label: zed_scenario.label,
|
||||
config: serde_json::Value::Object(config),
|
||||
tcp_connection: None,
|
||||
build: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_binary(
|
||||
@@ -247,26 +146,58 @@ impl DebugAdapter for RubyDebugAdapter {
|
||||
|
||||
let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
let ruby_config = serde_json::from_value::<RubyDebugConfig>(definition.config.clone())?;
|
||||
|
||||
let arguments = vec![
|
||||
let mut arguments = vec![
|
||||
"--open".to_string(),
|
||||
format!("--port={}", port),
|
||||
format!("--host={}", host),
|
||||
];
|
||||
|
||||
if let Some(script) = &ruby_config.script {
|
||||
arguments.push(script.clone());
|
||||
} else if let Some(command) = &ruby_config.command {
|
||||
arguments.push("--command".to_string());
|
||||
arguments.push(command.clone());
|
||||
} else if let Some(command_or_script) = &ruby_config.script_or_command {
|
||||
if delegate
|
||||
.which(OsStr::new(&command_or_script))
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
arguments.push("--command".to_string());
|
||||
}
|
||||
arguments.push(command_or_script.clone());
|
||||
} else {
|
||||
bail!("Ruby debug config must have 'script' or 'command' args");
|
||||
}
|
||||
|
||||
arguments.extend(ruby_config.args);
|
||||
|
||||
let mut configuration = definition.config.clone();
|
||||
if let Some(configuration) = configuration.as_object_mut() {
|
||||
configuration
|
||||
.entry("cwd")
|
||||
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
|
||||
}
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: rdbg_path.to_string_lossy().to_string(),
|
||||
command: Some(rdbg_path.to_string_lossy().to_string()),
|
||||
arguments,
|
||||
connection: Some(dap::adapters::TcpArguments {
|
||||
host,
|
||||
port,
|
||||
timeout,
|
||||
}),
|
||||
cwd: None,
|
||||
envs: std::collections::HashMap::default(),
|
||||
cwd: Some(
|
||||
ruby_config
|
||||
.cwd
|
||||
.unwrap_or(delegate.worktree_root_path().to_owned()),
|
||||
),
|
||||
envs: ruby_config.env.into_iter().collect(),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
request: self.request_kind(&definition.config)?,
|
||||
configuration: definition.config.clone(),
|
||||
configuration,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,19 +4,17 @@ use crate::session::running::RunningState;
|
||||
use crate::{
|
||||
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
|
||||
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, NewProcessModal,
|
||||
NewProcessMode, Pause, Restart, ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop,
|
||||
ToggleExpandItem, ToggleIgnoreBreakpoints, ToggleSessionPicker, ToggleThreadPicker,
|
||||
persistence, spawn_task_or_modal,
|
||||
NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop, ToggleExpandItem,
|
||||
ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use dap::StartDebuggingRequestArguments;
|
||||
use dap::adapters::DebugAdapterName;
|
||||
use dap::debugger_settings::DebugPanelDockPosition;
|
||||
use dap::{
|
||||
ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
|
||||
client::SessionId, debugger_settings::DebuggerSettings,
|
||||
};
|
||||
use dap::{DapRegistry, StartDebuggingRequestArguments};
|
||||
use gpui::{
|
||||
Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
|
||||
FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity,
|
||||
@@ -29,7 +27,6 @@ use project::{Fs, WorktreeId};
|
||||
use project::{Project, debugger::session::ThreadStatus};
|
||||
use rpc::proto::{self};
|
||||
use settings::Settings;
|
||||
use std::any::TypeId;
|
||||
use std::sync::Arc;
|
||||
use task::{DebugScenario, TaskContext};
|
||||
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
@@ -140,82 +137,6 @@ impl DebugPanel {
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
}
|
||||
|
||||
pub(crate) fn filter_action_types(&self, cx: &mut App) {
|
||||
let (has_active_session, supports_restart, support_step_back, status) = self
|
||||
.active_session()
|
||||
.map(|item| {
|
||||
let running = item.read(cx).running_state().clone();
|
||||
let caps = running.read(cx).capabilities(cx);
|
||||
(
|
||||
!running.read(cx).session().read(cx).is_terminated(),
|
||||
caps.supports_restart_request.unwrap_or_default(),
|
||||
caps.supports_step_back.unwrap_or_default(),
|
||||
running.read(cx).thread_status(cx),
|
||||
)
|
||||
})
|
||||
.unwrap_or((false, false, false, None));
|
||||
|
||||
let filter = CommandPaletteFilter::global_mut(cx);
|
||||
let debugger_action_types = [
|
||||
TypeId::of::<Detach>(),
|
||||
TypeId::of::<Stop>(),
|
||||
TypeId::of::<ToggleIgnoreBreakpoints>(),
|
||||
];
|
||||
|
||||
let running_action_types = [TypeId::of::<Pause>()];
|
||||
|
||||
let stopped_action_type = [
|
||||
TypeId::of::<Continue>(),
|
||||
TypeId::of::<StepOver>(),
|
||||
TypeId::of::<StepInto>(),
|
||||
TypeId::of::<StepOut>(),
|
||||
TypeId::of::<ShowStackTrace>(),
|
||||
TypeId::of::<editor::actions::DebuggerRunToCursor>(),
|
||||
TypeId::of::<editor::actions::DebuggerEvaluateSelectedText>(),
|
||||
];
|
||||
|
||||
let step_back_action_type = [TypeId::of::<StepBack>()];
|
||||
let restart_action_type = [TypeId::of::<Restart>()];
|
||||
|
||||
if has_active_session {
|
||||
filter.show_action_types(debugger_action_types.iter());
|
||||
|
||||
if supports_restart {
|
||||
filter.show_action_types(restart_action_type.iter());
|
||||
} else {
|
||||
filter.hide_action_types(&restart_action_type);
|
||||
}
|
||||
|
||||
if support_step_back {
|
||||
filter.show_action_types(step_back_action_type.iter());
|
||||
} else {
|
||||
filter.hide_action_types(&step_back_action_type);
|
||||
}
|
||||
|
||||
match status {
|
||||
Some(ThreadStatus::Running) => {
|
||||
filter.show_action_types(running_action_types.iter());
|
||||
filter.hide_action_types(&stopped_action_type);
|
||||
}
|
||||
Some(ThreadStatus::Stopped) => {
|
||||
filter.show_action_types(stopped_action_type.iter());
|
||||
filter.hide_action_types(&running_action_types);
|
||||
}
|
||||
_ => {
|
||||
filter.hide_action_types(&running_action_types);
|
||||
filter.hide_action_types(&stopped_action_type);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// show only the `debug: start`
|
||||
filter.hide_action_types(&debugger_action_types);
|
||||
filter.hide_action_types(&step_back_action_type);
|
||||
filter.hide_action_types(&restart_action_type);
|
||||
filter.hide_action_types(&running_action_types);
|
||||
filter.hide_action_types(&stopped_action_type);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
@@ -233,17 +154,6 @@ impl DebugPanel {
|
||||
)
|
||||
});
|
||||
|
||||
cx.observe_new::<DebugPanel>(|debug_panel, _, cx| {
|
||||
Self::filter_action_types(debug_panel, cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe(&debug_panel, |_, debug_panel, cx| {
|
||||
debug_panel.update(cx, |debug_panel, cx| {
|
||||
Self::filter_action_types(debug_panel, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));
|
||||
|
||||
debug_panel
|
||||
@@ -445,10 +355,7 @@ impl DebugPanel {
|
||||
};
|
||||
|
||||
let dap_store_handle = self.project.read(cx).dap_store().clone();
|
||||
let mut label = parent_session.read(cx).label().clone();
|
||||
if !label.ends_with("(child)") {
|
||||
label = format!("{label} (child)").into();
|
||||
}
|
||||
let label = self.label_for_child_session(&parent_session, request, cx);
|
||||
let adapter = parent_session.read(cx).adapter().clone();
|
||||
let mut binary = parent_session.read(cx).binary().clone();
|
||||
binary.request_args = request.clone();
|
||||
@@ -608,6 +515,12 @@ impl DebugPanel {
|
||||
}
|
||||
})
|
||||
};
|
||||
let documentation_button = || {
|
||||
IconButton::new("debug-open-documentation", IconName::CircleHelp)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(move |_, _, cx| cx.open_url("https://zed.dev/docs/debugger"))
|
||||
.tooltip(Tooltip::text("Open Documentation"))
|
||||
};
|
||||
|
||||
Some(
|
||||
div.border_b_1()
|
||||
@@ -629,6 +542,8 @@ impl DebugPanel {
|
||||
project::debugger::session::ThreadStatus::Exited,
|
||||
);
|
||||
let capabilities = running_state.read(cx).capabilities(cx);
|
||||
let supports_detach =
|
||||
running_state.read(cx).session().read(cx).is_attached();
|
||||
this.map(|this| {
|
||||
if thread_status == ThreadStatus::Running {
|
||||
this.child(
|
||||
@@ -817,33 +732,48 @@ impl DebugPanel {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-disconnect", IconName::DebugDetach)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(window.listener_for(
|
||||
&running_state,
|
||||
|this, _, _, cx| {
|
||||
this.detach_client(cx);
|
||||
},
|
||||
))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Detach",
|
||||
&Detach,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
.when(
|
||||
supports_detach,
|
||||
|div| {
|
||||
div.child(
|
||||
IconButton::new(
|
||||
"debug-disconnect",
|
||||
IconName::DebugDetach,
|
||||
)
|
||||
.disabled(
|
||||
thread_status != ThreadStatus::Stopped
|
||||
&& thread_status != ThreadStatus::Running,
|
||||
)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(window.listener_for(
|
||||
&running_state,
|
||||
|this, _, _, cx| {
|
||||
this.detach_client(cx);
|
||||
},
|
||||
))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Detach",
|
||||
&Detach,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
.justify_around()
|
||||
.when(is_side, |this| this.child(new_session_button())),
|
||||
.when(is_side, |this| {
|
||||
this.child(new_session_button())
|
||||
.child(documentation_button())
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -884,7 +814,10 @@ impl DebugPanel {
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.when(!is_side, |this| this.child(new_session_button())),
|
||||
.when(!is_side, |this| {
|
||||
this.child(new_session_button())
|
||||
.child(documentation_button())
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -1039,6 +972,25 @@ impl DebugPanel {
|
||||
cx.emit(PanelEvent::ZoomIn);
|
||||
}
|
||||
}
|
||||
|
||||
fn label_for_child_session(
|
||||
&self,
|
||||
parent_session: &Entity<Session>,
|
||||
request: &StartDebuggingRequestArguments,
|
||||
cx: &mut Context<'_, Self>,
|
||||
) -> SharedString {
|
||||
let adapter = parent_session.read(cx).adapter();
|
||||
if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) {
|
||||
if let Some(label) = adapter.label_for_child_session(request) {
|
||||
return label.into();
|
||||
}
|
||||
}
|
||||
let mut label = parent_session.read(cx).label().clone();
|
||||
if !label.ends_with("(child)") {
|
||||
label = format!("{label} (child)").into();
|
||||
}
|
||||
label
|
||||
}
|
||||
}
|
||||
|
||||
async fn register_session_inner(
|
||||
@@ -1066,6 +1018,11 @@ async fn register_session_inner(
|
||||
.ok();
|
||||
let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
|
||||
let debug_session = this.update_in(cx, |this, window, cx| {
|
||||
let parent_session = this
|
||||
.sessions
|
||||
.iter()
|
||||
.find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx))
|
||||
.cloned();
|
||||
this.sessions.retain(|session| {
|
||||
!session
|
||||
.read(cx)
|
||||
@@ -1079,8 +1036,8 @@ async fn register_session_inner(
|
||||
let debug_session = DebugSession::running(
|
||||
this.project.clone(),
|
||||
this.workspace.clone(),
|
||||
parent_session.map(|p| p.read(cx).running_state().read(cx).debug_terminal.clone()),
|
||||
session,
|
||||
cx.weak_entity(),
|
||||
serialized_layout,
|
||||
this.position(window, cx).axis(),
|
||||
window,
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use std::any::TypeId;
|
||||
|
||||
use dap::debugger_settings::DebuggerSettings;
|
||||
use debugger_panel::{DebugPanel, ToggleFocus};
|
||||
use editor::Editor;
|
||||
use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
|
||||
use gpui::{App, EntityInputHandler, actions};
|
||||
use gpui::{App, DispatchPhase, EntityInputHandler, actions};
|
||||
use new_process_modal::{NewProcessModal, NewProcessMode};
|
||||
use project::debugger::{self, breakpoint_store::SourceBreakpoint};
|
||||
use project::debugger::{self, breakpoint_store::SourceBreakpoint, session::ThreadStatus};
|
||||
use session::DebugSession;
|
||||
use settings::Settings;
|
||||
use stack_trace_view::StackTraceView;
|
||||
use tasks_ui::{Spawn, TaskOverrides};
|
||||
use ui::{FluentBuilder, InteractiveElement};
|
||||
use util::maybe;
|
||||
use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
|
||||
|
||||
@@ -68,148 +71,6 @@ pub fn init(cx: &mut App) {
|
||||
.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
workspace.toggle_panel_focus::<DebugPanel>(window, cx);
|
||||
})
|
||||
.register_action(|workspace, _: &Pause, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
{
|
||||
active_item.update(cx, |item, cx| item.pause_thread(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &Restart, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
{
|
||||
active_item.update(cx, |item, cx| item.restart_session(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &Continue, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
{
|
||||
active_item.update(cx, |item, cx| item.continue_thread(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &StepInto, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
{
|
||||
active_item.update(cx, |item, cx| item.step_in(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &StepOver, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
{
|
||||
active_item.update(cx, |item, cx| item.step_over(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &StepOut, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
|
||||
panel
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
}) {
|
||||
active_item.update(cx, |item, cx| item.step_out(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &StepBack, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
{
|
||||
active_item.update(cx, |item, cx| item.step_back(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &Stop, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
{
|
||||
cx.defer(move |cx| {
|
||||
active_item.update(cx, |item, cx| item.stop_thread(cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &ToggleIgnoreBreakpoints, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
{
|
||||
active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |store, cx| {
|
||||
store.shutdown_sessions(cx).detach();
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &ShowStackTrace, window, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx) {
|
||||
let is_active = workspace
|
||||
.active_item(cx)
|
||||
.is_some_and(|item| item.item_id() == existing.item_id());
|
||||
workspace.activate_item(&existing, true, !is_active, window, cx);
|
||||
} else {
|
||||
let Some(active_session) = debug_panel.read(cx).active_session() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = workspace.project();
|
||||
|
||||
let stack_trace_view = active_session.update(cx, |session, cx| {
|
||||
session.stack_trace_view(project, window, cx).clone()
|
||||
});
|
||||
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(stack_trace_view),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
|
||||
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
|
||||
})
|
||||
@@ -223,90 +84,255 @@ pub fn init(cx: &mut App) {
|
||||
debug_panel.rerun_last_session(workspace, window, cx);
|
||||
})
|
||||
},
|
||||
);
|
||||
)
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |store, cx| {
|
||||
store.shutdown_sessions(cx).detach();
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
.register_action_renderer(|div, workspace, _, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return div;
|
||||
};
|
||||
let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
else {
|
||||
return div;
|
||||
};
|
||||
let running_state = active_item.read(cx);
|
||||
if running_state.session().read(cx).is_terminated() {
|
||||
return div;
|
||||
}
|
||||
|
||||
let caps = running_state.capabilities(cx);
|
||||
let supports_restart = caps.supports_restart_request.unwrap_or_default();
|
||||
let supports_step_back = caps.supports_step_back.unwrap_or_default();
|
||||
let supports_detach = running_state.session().read(cx).is_attached();
|
||||
let status = running_state.thread_status(cx);
|
||||
|
||||
let active_item = active_item.downgrade();
|
||||
div.when(status == Some(ThreadStatus::Running), |div| {
|
||||
let active_item = active_item.clone();
|
||||
div.on_action(move |_: &Pause, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.pause_thread(cx))
|
||||
.ok();
|
||||
})
|
||||
})
|
||||
.when(status == Some(ThreadStatus::Stopped), |div| {
|
||||
div.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &StepInto, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_in(cx)).ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &StepOver, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_over(cx)).ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &StepOut, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_out(cx)).ok();
|
||||
}
|
||||
})
|
||||
.when(supports_step_back, |div| {
|
||||
let active_item = active_item.clone();
|
||||
div.on_action(move |_: &StepBack, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_back(cx)).ok();
|
||||
})
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &Continue, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.continue_thread(cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action(cx.listener(
|
||||
|workspace, _: &ShowStackTrace, window, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx)
|
||||
{
|
||||
let is_active = workspace
|
||||
.active_item(cx)
|
||||
.is_some_and(|item| item.item_id() == existing.item_id());
|
||||
workspace
|
||||
.activate_item(&existing, true, !is_active, window, cx);
|
||||
} else {
|
||||
let Some(active_session) =
|
||||
debug_panel.read(cx).active_session()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = workspace.project();
|
||||
|
||||
let stack_trace_view =
|
||||
active_session.update(cx, |session, cx| {
|
||||
session.stack_trace_view(project, window, cx).clone()
|
||||
});
|
||||
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(stack_trace_view),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
))
|
||||
})
|
||||
.when(supports_detach, |div| {
|
||||
let active_item = active_item.clone();
|
||||
div.on_action(move |_: &Detach, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.detach_client(cx))
|
||||
.ok();
|
||||
})
|
||||
})
|
||||
.when(supports_restart, |div| {
|
||||
let active_item = active_item.clone();
|
||||
div.on_action(move |_: &Restart, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.restart_session(cx))
|
||||
.ok();
|
||||
})
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &Stop, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.stop_thread(cx)).ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &ToggleIgnoreBreakpoints, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe_new({
|
||||
move |editor: &mut Editor, _, cx| {
|
||||
move |editor: &mut Editor, _, _| {
|
||||
editor
|
||||
.register_action(cx.listener(
|
||||
move |editor, _: &editor::actions::DebuggerRunToCursor, _, cx| {
|
||||
maybe!({
|
||||
let debug_panel =
|
||||
editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;
|
||||
let cursor_point: language::Point = editor.selections.newest(cx).head();
|
||||
let active_session = debug_panel.read(cx).active_session()?;
|
||||
.register_action_renderer(move |editor, window, cx| {
|
||||
let Some(workspace) = editor.workspace() else {
|
||||
return;
|
||||
};
|
||||
let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
let Some(active_session) = debug_panel
|
||||
.clone()
|
||||
.update(cx, |panel, _| panel.active_session())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let editor = cx.entity().downgrade();
|
||||
window.on_action(TypeId::of::<editor::actions::RunToCursor>(), {
|
||||
let editor = editor.clone();
|
||||
let active_session = active_session.clone();
|
||||
move |_, phase, _, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
maybe!({
|
||||
let (buffer, position, _) = editor
|
||||
.update(cx, |editor, cx| {
|
||||
let cursor_point: language::Point =
|
||||
editor.selections.newest(cx).head();
|
||||
|
||||
let (buffer, position, _) = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.point_to_buffer_point(cursor_point, cx)?;
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.point_to_buffer_point(cursor_point, cx)
|
||||
})
|
||||
.ok()??;
|
||||
|
||||
let path =
|
||||
let path =
|
||||
debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer(
|
||||
&buffer, cx,
|
||||
)?;
|
||||
|
||||
let source_breakpoint = SourceBreakpoint {
|
||||
row: position.row,
|
||||
path,
|
||||
message: None,
|
||||
condition: None,
|
||||
hit_condition: None,
|
||||
state: debugger::breakpoint_store::BreakpointState::Enabled,
|
||||
};
|
||||
let source_breakpoint = SourceBreakpoint {
|
||||
row: position.row,
|
||||
path,
|
||||
message: None,
|
||||
condition: None,
|
||||
hit_condition: None,
|
||||
state: debugger::breakpoint_store::BreakpointState::Enabled,
|
||||
};
|
||||
|
||||
active_session.update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |state, cx| {
|
||||
if let Some(thread_id) = state.selected_thread_id() {
|
||||
state.session().update(cx, |session, cx| {
|
||||
session.run_to_position(
|
||||
source_breakpoint,
|
||||
thread_id,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Some(())
|
||||
});
|
||||
},
|
||||
))
|
||||
.detach();
|
||||
|
||||
editor
|
||||
.register_action(cx.listener(
|
||||
move |editor, _: &editor::actions::DebuggerEvaluateSelectedText, window, cx| {
|
||||
maybe!({
|
||||
let debug_panel =
|
||||
editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;
|
||||
let active_session = debug_panel.read(cx).active_session()?;
|
||||
|
||||
let text = editor.text_for_range(
|
||||
editor.selections.newest(cx).range(),
|
||||
&mut None,
|
||||
window,
|
||||
cx,
|
||||
)?;
|
||||
|
||||
active_session.update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |state, cx| {
|
||||
let stack_id = state.selected_stack_frame_id(cx);
|
||||
|
||||
state.session().update(cx, |session, cx| {
|
||||
session.evaluate(text, None, stack_id, None, cx).detach();
|
||||
active_session.update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |state, cx| {
|
||||
if let Some(thread_id) = state.selected_thread_id() {
|
||||
state.session().update(cx, |session, cx| {
|
||||
session.run_to_position(
|
||||
source_breakpoint,
|
||||
thread_id,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Some(())
|
||||
});
|
||||
},
|
||||
))
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.on_action(
|
||||
TypeId::of::<editor::actions::EvaluateSelectedText>(),
|
||||
move |_, _, window, cx| {
|
||||
maybe!({
|
||||
let text = editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.text_for_range(
|
||||
editor.selections.newest(cx).range(),
|
||||
&mut None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()??;
|
||||
|
||||
active_session.update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |state, cx| {
|
||||
let stack_id = state.selected_stack_frame_id(cx);
|
||||
|
||||
state.session().update(cx, |session, cx| {
|
||||
session
|
||||
.evaluate(text, None, stack_id, None, cx)
|
||||
.detach();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Some(())
|
||||
});
|
||||
},
|
||||
);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use collections::FxHashMap;
|
||||
use collections::{FxHashMap, HashMap};
|
||||
use language::LanguageRegistry;
|
||||
use paths::local_debug_file_relative_path;
|
||||
use std::{
|
||||
@@ -15,9 +15,9 @@ use dap::{
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,
|
||||
InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, Subscription,
|
||||
TextStyle, UnderlineStyle, WeakEntity,
|
||||
Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText,
|
||||
Subscription, TextStyle, UnderlineStyle, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools as _;
|
||||
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
|
||||
@@ -28,10 +28,10 @@ use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
|
||||
ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,
|
||||
IconWithIndicator, Indicator, InteractiveElement, IntoElement, Label, LabelCommon as _,
|
||||
ListItem, ListItemSpacing, ParentElement, RenderOnce, SharedString, Styled, StyledExt,
|
||||
StyledTypography, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div, h_flex, px,
|
||||
relative, rems, v_flex,
|
||||
IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label,
|
||||
LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce,
|
||||
SharedString, Styled, StyledExt, StyledTypography, ToggleButton, ToggleState, Toggleable,
|
||||
Tooltip, Window, div, h_flex, px, relative, rems, v_flex,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Workspace, pane};
|
||||
@@ -50,7 +50,7 @@ pub(super) struct NewProcessModal {
|
||||
mode: NewProcessMode,
|
||||
debug_picker: Entity<Picker<DebugDelegate>>,
|
||||
attach_mode: Entity<AttachMode>,
|
||||
launch_mode: Entity<ConfigureMode>,
|
||||
configure_mode: Entity<ConfigureMode>,
|
||||
task_mode: TaskMode,
|
||||
debugger: Option<DebugAdapterName>,
|
||||
// save_scenario_state: Option<SaveScenarioState>,
|
||||
@@ -194,11 +194,12 @@ impl NewProcessModal {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let (used_tasks, current_resolved_tasks) =
|
||||
task_inventory.update(cx, |task_inventory, cx| {
|
||||
let (used_tasks, current_resolved_tasks) = task_inventory
|
||||
.update(cx, |task_inventory, cx| {
|
||||
task_inventory
|
||||
.used_and_current_resolved_tasks(&task_contexts, cx)
|
||||
})?;
|
||||
.used_and_current_resolved_tasks(task_contexts.clone(), cx)
|
||||
})?
|
||||
.await;
|
||||
|
||||
debug_picker
|
||||
.update_in(cx, |picker, window, cx| {
|
||||
@@ -253,7 +254,7 @@ impl NewProcessModal {
|
||||
Self {
|
||||
debug_picker,
|
||||
attach_mode,
|
||||
launch_mode: configure_mode,
|
||||
configure_mode,
|
||||
task_mode,
|
||||
debugger: None,
|
||||
mode,
|
||||
@@ -283,7 +284,7 @@ impl NewProcessModal {
|
||||
NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| {
|
||||
this.clone().render(window, cx).into_any_element()
|
||||
}),
|
||||
NewProcessMode::Launch => self.launch_mode.update(cx, |this, cx| {
|
||||
NewProcessMode::Launch => self.configure_mode.update(cx, |this, cx| {
|
||||
this.clone().render(dap_menu, window, cx).into_any_element()
|
||||
}),
|
||||
NewProcessMode::Debug => v_flex()
|
||||
@@ -297,7 +298,7 @@ impl NewProcessModal {
|
||||
match self.mode {
|
||||
NewProcessMode::Task => self.task_mode.task_modal.focus_handle(cx),
|
||||
NewProcessMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),
|
||||
NewProcessMode::Launch => self.launch_mode.read(cx).program.focus_handle(cx),
|
||||
NewProcessMode::Launch => self.configure_mode.read(cx).program.focus_handle(cx),
|
||||
NewProcessMode::Debug => self.debug_picker.focus_handle(cx),
|
||||
}
|
||||
}
|
||||
@@ -305,7 +306,7 @@ impl NewProcessModal {
|
||||
fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
|
||||
let request = match self.mode {
|
||||
NewProcessMode::Launch => Some(DebugRequest::Launch(
|
||||
self.launch_mode.read(cx).debug_request(cx),
|
||||
self.configure_mode.read(cx).debug_request(cx),
|
||||
)),
|
||||
NewProcessMode::Attach => Some(DebugRequest::Attach(
|
||||
self.attach_mode.read(cx).debug_request(),
|
||||
@@ -315,7 +316,7 @@ impl NewProcessModal {
|
||||
let label = suggested_label(&request, debugger);
|
||||
|
||||
let stop_on_entry = if let NewProcessMode::Launch = &self.mode {
|
||||
Some(self.launch_mode.read(cx).stop_on_entry.selected())
|
||||
Some(self.configure_mode.read(cx).stop_on_entry.selected())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -831,7 +832,7 @@ impl Render for NewProcessModal {
|
||||
.disabled(
|
||||
self.debugger.is_none()
|
||||
|| self
|
||||
.launch_mode
|
||||
.configure_mode
|
||||
.read(cx)
|
||||
.program
|
||||
.read(cx)
|
||||
@@ -1202,7 +1203,7 @@ impl PickerDelegate for DebugDelegate {
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
|
||||
"".into()
|
||||
"Find a debug task, or debug a command.".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
@@ -1265,6 +1266,96 @@ impl PickerDelegate for DebugDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_input(
|
||||
&mut self,
|
||||
_secondary: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
let text = self.prompt.clone();
|
||||
let (task_context, worktree_id) = self
|
||||
.task_contexts
|
||||
.as_ref()
|
||||
.and_then(|task_contexts| {
|
||||
Some((
|
||||
task_contexts.active_context().cloned()?,
|
||||
task_contexts.worktree(),
|
||||
))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut args = shlex::split(&text).into_iter().flatten().peekable();
|
||||
let mut env = HashMap::default();
|
||||
while args.peek().is_some_and(|arg| arg.contains('=')) {
|
||||
let arg = args.next().unwrap();
|
||||
let (lhs, rhs) = arg.split_once('=').unwrap();
|
||||
env.insert(lhs.to_string(), rhs.to_string());
|
||||
}
|
||||
|
||||
let program = if let Some(program) = args.next() {
|
||||
program
|
||||
} else {
|
||||
env = HashMap::default();
|
||||
text
|
||||
};
|
||||
|
||||
let args = args.collect::<Vec<_>>();
|
||||
let task = task::TaskTemplate {
|
||||
label: "one-off".to_owned(),
|
||||
env,
|
||||
command: program,
|
||||
args,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let Some(location) = self
|
||||
.task_contexts
|
||||
.as_ref()
|
||||
.and_then(|cx| cx.location().cloned())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let file = location.buffer.read(cx).file();
|
||||
let language = location.buffer.read(cx).language();
|
||||
let language_name = language.as_ref().map(|l| l.name());
|
||||
let Some(adapter): Option<DebugAdapterName> =
|
||||
language::language_settings::language_settings(language_name, file, cx)
|
||||
.debuggers
|
||||
.first()
|
||||
.map(SharedString::from)
|
||||
.map(Into::into)
|
||||
.or_else(|| {
|
||||
language.and_then(|l| {
|
||||
l.config()
|
||||
.debuggers
|
||||
.first()
|
||||
.map(SharedString::from)
|
||||
.map(Into::into)
|
||||
})
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(debug_scenario) = cx
|
||||
.global::<DapRegistry>()
|
||||
.locators()
|
||||
.iter()
|
||||
.find_map(|locator| locator.1.create_scenario(&task, "one-off", adapter.clone()))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
|
||||
|
||||
self.debug_panel
|
||||
.update(cx, |panel, cx| {
|
||||
panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
|
||||
let debug_scenario = self
|
||||
.matches
|
||||
@@ -1300,6 +1391,60 @@ impl PickerDelegate for DebugDelegate {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_footer(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<ui::AnyElement> {
|
||||
let current_modifiers = window.modifiers();
|
||||
let footer = h_flex()
|
||||
.w_full()
|
||||
.h_8()
|
||||
.p_2()
|
||||
.justify_between()
|
||||
.rounded_b_sm()
|
||||
.bg(cx.theme().colors().ghost_element_selected)
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
// TODO: add button to open selected task in debug.json
|
||||
h_flex().into_any_element(),
|
||||
)
|
||||
.map(|this| {
|
||||
if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {
|
||||
let action = picker::ConfirmInput {
|
||||
secondary: current_modifiers.secondary(),
|
||||
}
|
||||
.boxed_clone();
|
||||
this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| {
|
||||
Button::new("launch-custom", "Launch Custom")
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(keybind)
|
||||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(action.boxed_clone(), cx)
|
||||
})
|
||||
}))
|
||||
} else {
|
||||
this.children(KeyBinding::for_action(&menu::Confirm, window, cx).map(
|
||||
|keybind| {
|
||||
let is_recent_selected =
|
||||
self.divider_index >= Some(self.selected_index);
|
||||
let run_entry_label =
|
||||
if is_recent_selected { "Rerun" } else { "Spawn" };
|
||||
|
||||
Button::new("spawn", run_entry_label)
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(keybind)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(menu::Confirm.boxed_clone(), cx);
|
||||
})
|
||||
},
|
||||
))
|
||||
}
|
||||
});
|
||||
Some(footer.into_any_element())
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pub mod running;
|
||||
|
||||
use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
|
||||
use crate::{StackTraceView, persistence::SerializedLayout, session::running::DebugTerminal};
|
||||
use dap::client::SessionId;
|
||||
use gpui::{
|
||||
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
|
||||
@@ -22,7 +22,6 @@ pub struct DebugSession {
|
||||
running_state: Entity<RunningState>,
|
||||
label: OnceLock<SharedString>,
|
||||
stack_trace_view: OnceCell<Entity<StackTraceView>>,
|
||||
_debug_panel: WeakEntity<DebugPanel>,
|
||||
_worktree_store: WeakEntity<WorktreeStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_subscriptions: [Subscription; 1],
|
||||
@@ -38,8 +37,8 @@ impl DebugSession {
|
||||
pub(crate) fn running(
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
parent_terminal: Option<Entity<DebugTerminal>>,
|
||||
session: Entity<Session>,
|
||||
_debug_panel: WeakEntity<DebugPanel>,
|
||||
serialized_layout: Option<SerializedLayout>,
|
||||
dock_axis: Axis,
|
||||
window: &mut Window,
|
||||
@@ -50,6 +49,7 @@ impl DebugSession {
|
||||
session.clone(),
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
parent_terminal,
|
||||
serialized_layout,
|
||||
dock_axis,
|
||||
window,
|
||||
@@ -64,7 +64,6 @@ impl DebugSession {
|
||||
remote_id: None,
|
||||
running_state,
|
||||
label: OnceLock::new(),
|
||||
_debug_panel,
|
||||
stack_trace_view: OnceCell::new(),
|
||||
_worktree_store: project.read(cx).worktree_store().downgrade(),
|
||||
workspace,
|
||||
|
||||
@@ -605,6 +605,7 @@ impl RunningState {
|
||||
session: Entity<Session>,
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
parent_terminal: Option<Entity<DebugTerminal>>,
|
||||
serialized_pane_layout: Option<SerializedLayout>,
|
||||
dock_axis: Axis,
|
||||
window: &mut Window,
|
||||
@@ -617,7 +618,8 @@ impl RunningState {
|
||||
StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
|
||||
});
|
||||
|
||||
let debug_terminal = cx.new(|cx| DebugTerminal::empty(window, cx));
|
||||
let debug_terminal =
|
||||
parent_terminal.unwrap_or_else(|| cx.new(|cx| DebugTerminal::empty(window, cx)));
|
||||
|
||||
let variable_list =
|
||||
cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
|
||||
@@ -816,20 +818,20 @@ impl RunningState {
|
||||
|
||||
let request_type = dap_registry
|
||||
.adapter(&adapter)
|
||||
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
|
||||
.with_context(|| format!("{}: is not a valid adapter name", &adapter))
|
||||
.and_then(|adapter| adapter.request_kind(&config));
|
||||
|
||||
let config_is_valid = request_type.is_ok();
|
||||
|
||||
let build_output = if let Some(build) = build {
|
||||
let (task, locator_name) = match build {
|
||||
let (task_template, locator_name) = match build {
|
||||
BuildTaskDefinition::Template {
|
||||
task_template,
|
||||
locator_name,
|
||||
} => (task_template, locator_name),
|
||||
BuildTaskDefinition::ByName(ref label) => {
|
||||
let Some(task) = task_store.update(cx, |this, cx| {
|
||||
this.task_inventory().and_then(|inventory| {
|
||||
let task = task_store.update(cx, |this, cx| {
|
||||
this.task_inventory().map(|inventory| {
|
||||
inventory.read(cx).task_template_by_label(
|
||||
buffer,
|
||||
worktree_id,
|
||||
@@ -837,14 +839,15 @@ impl RunningState {
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
else {
|
||||
anyhow::bail!("Couldn't find task template for {:?}", build)
|
||||
};
|
||||
})?;
|
||||
let task = match task {
|
||||
Some(task) => task.await,
|
||||
None => None,
|
||||
}.with_context(|| format!("Couldn't find task template for {build:?}"))?;
|
||||
(task, None)
|
||||
}
|
||||
};
|
||||
let Some(task) = task.resolve_task("debug-build-task", &task_context) else {
|
||||
let Some(task) = task_template.resolve_task("debug-build-task", &task_context) else {
|
||||
anyhow::bail!("Could not resolve task variables within a debug scenario");
|
||||
};
|
||||
|
||||
@@ -929,15 +932,13 @@ impl RunningState {
|
||||
};
|
||||
|
||||
if config_is_valid {
|
||||
// Ok(DebugTaskDefinition {
|
||||
// label,
|
||||
// adapter: DebugAdapterName(adapter),
|
||||
// config,
|
||||
// tcp_connection,
|
||||
// })
|
||||
} else if let Some((task, locator_name)) = build_output {
|
||||
let locator_name =
|
||||
locator_name.context("Could not find a valid locator for a build task")?;
|
||||
locator_name.with_context(|| {
|
||||
format!("Could not find a valid locator for a build task and configure is invalid with error: {}", request_type.err()
|
||||
.map(|err| err.to_string())
|
||||
.unwrap_or_default())
|
||||
})?;
|
||||
let request = dap_store
|
||||
.update(cx, |this, cx| {
|
||||
this.run_debug_locator(&locator_name, task, cx)
|
||||
@@ -953,7 +954,7 @@ impl RunningState {
|
||||
|
||||
let scenario = dap_registry
|
||||
.adapter(&adapter)
|
||||
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
|
||||
.with_context(|| anyhow!("{}: is not a valid adapter name", &adapter))
|
||||
.map(|adapter| adapter.config_from_zed_format(zed_config))??;
|
||||
config = scenario.config;
|
||||
Self::substitute_variables_in_config(&mut config, &task_context);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
@@ -277,10 +278,9 @@ impl BreakpointList {
|
||||
let selected_ix = self.selected_ix;
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
uniform_list(
|
||||
cx.entity(),
|
||||
"breakpoint-list",
|
||||
self.breakpoints.len(),
|
||||
move |this, range, window, cx| {
|
||||
cx.processor(move |this, range: Range<usize>, window, cx| {
|
||||
range
|
||||
.clone()
|
||||
.zip(&mut this.breakpoints[range])
|
||||
@@ -291,7 +291,7 @@ impl BreakpointList {
|
||||
.into_any_element()
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
}),
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.flex_grow()
|
||||
|
||||
@@ -8,7 +8,7 @@ use project::{
|
||||
ProjectItem as _, ProjectPath,
|
||||
debugger::session::{Session, SessionEvent},
|
||||
};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use std::{ops::Range, path::Path, sync::Arc};
|
||||
use ui::{Scrollbar, ScrollbarState, prelude::*};
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -281,10 +281,11 @@ impl ModuleList {
|
||||
|
||||
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
uniform_list(
|
||||
cx.entity(),
|
||||
"module-list",
|
||||
self.entries.len(),
|
||||
|this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
|
||||
cx.processor(|this, range: Range<usize>, _window, cx| {
|
||||
range.map(|ix| this.render_entry(ix, cx)).collect()
|
||||
}),
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.size_full()
|
||||
|
||||
@@ -5,8 +5,8 @@ use std::time::Duration;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use dap::StackFrameId;
|
||||
use gpui::{
|
||||
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, MouseButton, ScrollStrategy,
|
||||
Stateful, Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
|
||||
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, MouseButton, Stateful,
|
||||
Subscription, Task, WeakEntity, list,
|
||||
};
|
||||
|
||||
use crate::StackTraceView;
|
||||
@@ -35,7 +35,7 @@ pub struct StackFrameList {
|
||||
selected_ix: Option<usize>,
|
||||
opened_stack_frame_id: Option<StackFrameId>,
|
||||
scrollbar_state: ScrollbarState,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
list_state: ListState,
|
||||
_refresh_task: Task<()>,
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ impl StackFrameList {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
|
||||
let _subscription =
|
||||
cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
|
||||
@@ -67,8 +66,16 @@ impl StackFrameList {
|
||||
_ => {}
|
||||
});
|
||||
|
||||
let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.), {
|
||||
let this = cx.weak_entity();
|
||||
move |ix, _window, cx| {
|
||||
this.update(cx, |this, cx| this.render_entry(ix, cx))
|
||||
.unwrap_or(div().into_any())
|
||||
}
|
||||
});
|
||||
let scrollbar_state = ScrollbarState::new(list_state.clone());
|
||||
|
||||
let mut this = Self {
|
||||
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
|
||||
session,
|
||||
workspace,
|
||||
focus_handle,
|
||||
@@ -77,7 +84,8 @@ impl StackFrameList {
|
||||
entries: Default::default(),
|
||||
selected_ix: None,
|
||||
opened_stack_frame_id: None,
|
||||
scroll_handle,
|
||||
list_state,
|
||||
scrollbar_state,
|
||||
_refresh_task: Task::ready(()),
|
||||
};
|
||||
this.schedule_refresh(true, window, cx);
|
||||
@@ -175,6 +183,7 @@ impl StackFrameList {
|
||||
let mut entries = Vec::new();
|
||||
let mut collapsed_entries = Vec::new();
|
||||
let mut first_stack_frame = None;
|
||||
let mut first_not_subtle_frame = None;
|
||||
|
||||
let stack_frames = self.stack_frames(cx);
|
||||
for stack_frame in &stack_frames {
|
||||
@@ -189,6 +198,11 @@ impl StackFrameList {
|
||||
}
|
||||
|
||||
first_stack_frame.get_or_insert(entries.len());
|
||||
if stack_frame.dap.presentation_hint
|
||||
!= Some(dap::StackFramePresentationHint::Subtle)
|
||||
{
|
||||
first_not_subtle_frame.get_or_insert(entries.len());
|
||||
}
|
||||
entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
|
||||
}
|
||||
}
|
||||
@@ -201,7 +215,10 @@ impl StackFrameList {
|
||||
|
||||
std::mem::swap(&mut self.entries, &mut entries);
|
||||
|
||||
if let Some(ix) = first_stack_frame.filter(|_| open_first_stack_frame) {
|
||||
if let Some(ix) = first_not_subtle_frame
|
||||
.or(first_stack_frame)
|
||||
.filter(|_| open_first_stack_frame)
|
||||
{
|
||||
self.select_ix(Some(ix), cx);
|
||||
self.activate_selected_entry(window, cx);
|
||||
} else if let Some(old_selected_frame_id) = old_selected_frame_id {
|
||||
@@ -214,6 +231,7 @@ impl StackFrameList {
|
||||
self.selected_ix = ix;
|
||||
}
|
||||
|
||||
self.list_state.reset(self.entries.len());
|
||||
cx.emit(StackFrameListEvent::BuiltEntries);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -555,10 +573,6 @@ impl StackFrameList {
|
||||
|
||||
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
|
||||
self.selected_ix = ix;
|
||||
if let Some(ix) = self.selected_ix {
|
||||
self.scroll_handle
|
||||
.scroll_to_item(ix, ScrollStrategy::Center);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -642,15 +656,8 @@ impl StackFrameList {
|
||||
self.activate_selected_entry(window, cx);
|
||||
}
|
||||
|
||||
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
uniform_list(
|
||||
cx.entity(),
|
||||
"stack-frame-list",
|
||||
self.entries.len(),
|
||||
|this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.size_full()
|
||||
fn render_list(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
list(self.list_state.clone()).size_full()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -980,10 +980,11 @@ impl Render for VariableList {
|
||||
.on_action(cx.listener(Self::edit_variable))
|
||||
.child(
|
||||
uniform_list(
|
||||
cx.entity().clone(),
|
||||
"variable-list",
|
||||
self.entries.len(),
|
||||
move |this, range, window, cx| this.render_entries(range, window, cx),
|
||||
cx.processor(move |this, range: Range<usize>, window, cx| {
|
||||
this.render_entries(range, window, cx)
|
||||
}),
|
||||
)
|
||||
.track_scroll(self.list_handle.clone())
|
||||
.gap_1_5()
|
||||
|
||||
@@ -35,7 +35,6 @@ assets.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
convert_case.workspace = true
|
||||
dap.workspace = true
|
||||
db.workspace = true
|
||||
|
||||
@@ -243,6 +243,8 @@ impl_actions!(
|
||||
]
|
||||
);
|
||||
|
||||
actions!(debugger, [RunToCursor, EvaluateSelectedText]);
|
||||
|
||||
actions!(
|
||||
editor,
|
||||
[
|
||||
@@ -426,8 +428,6 @@ actions!(
|
||||
DisableBreakpoint,
|
||||
EnableBreakpoint,
|
||||
EditLogBreakpoint,
|
||||
DebuggerRunToCursor,
|
||||
DebuggerEvaluateSelectedText,
|
||||
ToggleAutoSignatureHelp,
|
||||
ToggleGitBlameInline,
|
||||
OpenGitBlameCommit,
|
||||
|
||||
@@ -722,10 +722,9 @@ impl CompletionsMenu {
|
||||
let last_rendered_range = self.last_rendered_range.clone();
|
||||
let style = style.clone();
|
||||
let list = uniform_list(
|
||||
cx.entity().clone(),
|
||||
"completions",
|
||||
self.entries.borrow().len(),
|
||||
move |_editor, range, _window, cx| {
|
||||
cx.processor(move |_editor, range: Range<usize>, _window, cx| {
|
||||
last_rendered_range.borrow_mut().replace(range.clone());
|
||||
let start_ix = range.start;
|
||||
let completions_guard = completions.borrow_mut();
|
||||
@@ -837,7 +836,7 @@ impl CompletionsMenu {
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
}),
|
||||
)
|
||||
.occlude()
|
||||
.max_h(max_height_in_lines as f32 * window.line_height())
|
||||
@@ -1452,10 +1451,9 @@ impl CodeActionsMenu {
|
||||
let actions = self.actions.clone();
|
||||
let selected_item = self.selected_item;
|
||||
let list = uniform_list(
|
||||
cx.entity().clone(),
|
||||
"code_actions_menu",
|
||||
self.actions.len(),
|
||||
move |_this, range, _, cx| {
|
||||
cx.processor(move |_this, range: Range<usize>, _, cx| {
|
||||
actions
|
||||
.iter()
|
||||
.skip(range.start)
|
||||
@@ -1518,7 +1516,7 @@ impl CodeActionsMenu {
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
}),
|
||||
)
|
||||
.occlude()
|
||||
.max_h(max_height_in_lines as f32 * window.line_height())
|
||||
|
||||
@@ -464,7 +464,7 @@ impl BlockMap {
|
||||
map
|
||||
}
|
||||
|
||||
pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapReader {
|
||||
pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapReader<'_> {
|
||||
self.sync(&wrap_snapshot, edits);
|
||||
*self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone();
|
||||
BlockMapReader {
|
||||
@@ -479,7 +479,7 @@ impl BlockMap {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapWriter {
|
||||
pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapWriter<'_> {
|
||||
self.sync(&wrap_snapshot, edits);
|
||||
*self.wrap_snapshot.borrow_mut() = wrap_snapshot;
|
||||
BlockMapWriter(self)
|
||||
@@ -1327,7 +1327,7 @@ impl BlockSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows {
|
||||
pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows<'_> {
|
||||
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
|
||||
cursor.seek(&start_row, Bias::Right, &());
|
||||
let (output_start, input_start) = cursor.start();
|
||||
|
||||
@@ -357,7 +357,7 @@ impl FoldMap {
|
||||
&mut self,
|
||||
inlay_snapshot: InlaySnapshot,
|
||||
edits: Vec<InlayEdit>,
|
||||
) -> (FoldMapWriter, FoldSnapshot, Vec<FoldEdit>) {
|
||||
) -> (FoldMapWriter<'_>, FoldSnapshot, Vec<FoldEdit>) {
|
||||
let (snapshot, edits) = self.read(inlay_snapshot, edits);
|
||||
(FoldMapWriter(self), snapshot, edits)
|
||||
}
|
||||
@@ -730,7 +730,7 @@ impl FoldSnapshot {
|
||||
(line_end - line_start) as u32
|
||||
}
|
||||
|
||||
pub fn row_infos(&self, start_row: u32) -> FoldRows {
|
||||
pub fn row_infos(&self, start_row: u32) -> FoldRows<'_> {
|
||||
if start_row > self.transforms.summary().output.lines.row {
|
||||
panic!("invalid display row {}", start_row);
|
||||
}
|
||||
|
||||
@@ -726,7 +726,7 @@ impl WrapSnapshot {
|
||||
self.transforms.summary().output.longest_row
|
||||
}
|
||||
|
||||
pub fn row_infos(&self, start_row: u32) -> WrapRows {
|
||||
pub fn row_infos(&self, start_row: u32) -> WrapRows<'_> {
|
||||
let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
|
||||
transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
|
||||
let mut input_row = transforms.start().1.row();
|
||||
|
||||
@@ -240,7 +240,6 @@ pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration:
|
||||
|
||||
pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction";
|
||||
pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict";
|
||||
pub(crate) const MIN_LINE_NUMBER_DIGITS: u32 = 4;
|
||||
pub(crate) const MINIMAP_FONT_SIZE: AbsoluteLength = AbsoluteLength::Pixels(px(2.));
|
||||
|
||||
pub type RenderDiffHunkControlsFn = Arc<
|
||||
@@ -698,7 +697,7 @@ impl EditorActionId {
|
||||
// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
|
||||
|
||||
type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>);
|
||||
type GutterHighlight = (fn(&App) -> Hsla, Arc<[Range<Anchor>]>);
|
||||
type GutterHighlight = (fn(&App) -> Hsla, Vec<Range<Anchor>>);
|
||||
|
||||
#[derive(Default)]
|
||||
struct ScrollbarMarkerState {
|
||||
@@ -910,17 +909,21 @@ enum SelectionDragState {
|
||||
/// State when no drag related activity is detected.
|
||||
None,
|
||||
/// State when the mouse is down on a selection that is about to be dragged.
|
||||
ReadyToDrag { selection: Selection<Anchor> },
|
||||
ReadyToDrag {
|
||||
selection: Selection<Anchor>,
|
||||
click_position: gpui::Point<Pixels>,
|
||||
},
|
||||
/// State when the mouse is dragging the selection in the editor.
|
||||
Dragging {
|
||||
selection: Selection<Anchor>,
|
||||
drop_cursor: Selection<Anchor>,
|
||||
hide_drop_cursor: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have
|
||||
/// a breakpoint on them.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct PhantomBreakpointIndicator {
|
||||
display_row: DisplayRow,
|
||||
/// There's a small debounce between hovering over the line and showing the indicator.
|
||||
@@ -928,6 +931,7 @@ struct PhantomBreakpointIndicator {
|
||||
is_active: bool,
|
||||
collides_with_existing_breakpoint: bool,
|
||||
}
|
||||
|
||||
/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`].
|
||||
///
|
||||
/// See the [module level documentation](self) for more information.
|
||||
@@ -1049,8 +1053,9 @@ pub struct Editor {
|
||||
style: Option<EditorStyle>,
|
||||
text_style_refinement: Option<TextStyleRefinement>,
|
||||
next_editor_action_id: EditorActionId,
|
||||
editor_actions:
|
||||
Rc<RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&mut Window, &mut Context<Self>)>>>>,
|
||||
editor_actions: Rc<
|
||||
RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&Editor, &mut Window, &mut Context<Self>)>>>,
|
||||
>,
|
||||
use_autoclose: bool,
|
||||
use_auto_surround: bool,
|
||||
auto_replace_emoji_shortcode: bool,
|
||||
@@ -1196,10 +1201,12 @@ struct SelectionHistoryEntry {
|
||||
add_selections_state: Option<AddSelectionsState>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
enum SelectionHistoryMode {
|
||||
Normal,
|
||||
Undoing,
|
||||
Redoing,
|
||||
Skipping,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
@@ -1233,11 +1240,19 @@ struct SelectionHistory {
|
||||
}
|
||||
|
||||
impl SelectionHistory {
|
||||
#[track_caller]
|
||||
fn insert_transaction(
|
||||
&mut self,
|
||||
transaction_id: TransactionId,
|
||||
selections: Arc<[Selection<Anchor>]>,
|
||||
) {
|
||||
if selections.is_empty() {
|
||||
log::error!(
|
||||
"SelectionHistory::insert_transaction called with empty selections. Caller: {}",
|
||||
std::panic::Location::caller()
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.selections_by_transaction
|
||||
.insert(transaction_id, (selections, None));
|
||||
}
|
||||
@@ -1267,6 +1282,7 @@ impl SelectionHistory {
|
||||
}
|
||||
SelectionHistoryMode::Undoing => self.push_redo(entry),
|
||||
SelectionHistoryMode::Redoing => self.push_undo(entry),
|
||||
SelectionHistoryMode::Skipping => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2086,7 +2102,11 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
// skip adding the initial selection to selection history
|
||||
editor.selection_history.mode = SelectionHistoryMode::Skipping;
|
||||
editor.end_selection(window, cx);
|
||||
editor.selection_history.mode = SelectionHistoryMode::Normal;
|
||||
|
||||
editor.scroll_manager.show_scrollbars(window, cx);
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &buffer, cx);
|
||||
|
||||
@@ -5119,10 +5139,13 @@ impl Editor {
|
||||
.as_ref()
|
||||
.map_or(true, |provider| provider.filter_completions());
|
||||
|
||||
// When `is_incomplete` is false, can filter completions instead of re-querying when the
|
||||
// current query is a suffix of the initial query.
|
||||
if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() {
|
||||
if !menu.is_incomplete && filter_completions {
|
||||
if filter_completions {
|
||||
menu.filter(query.clone(), provider.clone(), window, cx);
|
||||
}
|
||||
// When `is_incomplete` is false, no need to re-query completions when the current query
|
||||
// is a suffix of the initial query.
|
||||
if !menu.is_incomplete {
|
||||
// If the new query is a suffix of the old query (typing more characters) and
|
||||
// the previous result was complete, the existing completions can be filtered.
|
||||
//
|
||||
@@ -5140,7 +5163,6 @@ impl Editor {
|
||||
menu.initial_position.to_offset(&snapshot) == position.to_offset(&snapshot)
|
||||
};
|
||||
if position_matches {
|
||||
menu.filter(query.clone(), provider.clone(), window, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -7519,8 +7541,7 @@ impl Editor {
|
||||
"Set Breakpoint"
|
||||
};
|
||||
|
||||
let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx)
|
||||
.map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor));
|
||||
let run_to_cursor = window.is_action_available(&RunToCursor, cx);
|
||||
|
||||
let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state {
|
||||
BreakpointState::Enabled => Some("Disable"),
|
||||
@@ -7544,7 +7565,7 @@ impl Editor {
|
||||
})
|
||||
.ok();
|
||||
|
||||
window.dispatch_action(Box::new(DebuggerRunToCursor), cx);
|
||||
window.dispatch_action(Box::new(RunToCursor), cx);
|
||||
})
|
||||
.separator()
|
||||
})
|
||||
@@ -10601,54 +10622,42 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn drop_selection(
|
||||
pub fn move_selection_on_drop(
|
||||
&mut self,
|
||||
point_for_position: Option<PointForPosition>,
|
||||
selection: &Selection<Anchor>,
|
||||
target: DisplayPoint,
|
||||
is_cut: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
if let Some(point_for_position) = point_for_position {
|
||||
match self.selection_drag_state {
|
||||
SelectionDragState::Dragging { ref selection, .. } => {
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let selection_display =
|
||||
selection.map(|anchor| anchor.to_display_point(&snapshot));
|
||||
if !point_for_position.intersects_selection(&selection_display) {
|
||||
let point = point_for_position.previous_valid;
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let buffer = &display_map.buffer_snapshot;
|
||||
let mut edits = Vec::new();
|
||||
let insert_point = display_map
|
||||
.clip_point(point, Bias::Left)
|
||||
.to_point(&display_map);
|
||||
let text = buffer
|
||||
.text_for_range(selection.start..selection.end)
|
||||
.collect::<String>();
|
||||
if is_cut {
|
||||
edits.push(((selection.start..selection.end), String::new()));
|
||||
}
|
||||
let insert_anchor = buffer.anchor_before(insert_point);
|
||||
edits.push(((insert_anchor..insert_anchor), text));
|
||||
let last_edit_start = insert_anchor.bias_left(buffer);
|
||||
let last_edit_end = insert_anchor.bias_right(buffer);
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits, None, cx);
|
||||
});
|
||||
this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.select_anchor_ranges([last_edit_start..last_edit_end]);
|
||||
});
|
||||
});
|
||||
self.selection_drag_state = SelectionDragState::None;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
) {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let buffer = &display_map.buffer_snapshot;
|
||||
let mut edits = Vec::new();
|
||||
let insert_point = display_map
|
||||
.clip_point(target, Bias::Left)
|
||||
.to_point(&display_map);
|
||||
let text = buffer
|
||||
.text_for_range(selection.start..selection.end)
|
||||
.collect::<String>();
|
||||
if is_cut {
|
||||
edits.push(((selection.start..selection.end), String::new()));
|
||||
}
|
||||
let insert_anchor = buffer.anchor_before(insert_point);
|
||||
edits.push(((insert_anchor..insert_anchor), text));
|
||||
let last_edit_start = insert_anchor.bias_left(buffer);
|
||||
let last_edit_end = insert_anchor.bias_right(buffer);
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits, None, cx);
|
||||
});
|
||||
this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.select_anchor_ranges([last_edit_start..last_edit_end]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn clear_selection_drag_state(&mut self) {
|
||||
self.selection_drag_state = SelectionDragState::None;
|
||||
false
|
||||
}
|
||||
|
||||
pub fn duplicate(
|
||||
@@ -14028,7 +14037,8 @@ impl Editor {
|
||||
prefer_lsp && !lsp_tasks_by_rows.is_empty(),
|
||||
new_rows,
|
||||
cx.clone(),
|
||||
);
|
||||
)
|
||||
.await;
|
||||
editor
|
||||
.update(cx, |editor, _| {
|
||||
editor.clear_tasks();
|
||||
@@ -14058,35 +14068,40 @@ impl Editor {
|
||||
snapshot: DisplaySnapshot,
|
||||
prefer_lsp: bool,
|
||||
runnable_ranges: Vec<RunnableRange>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Vec<((BufferId, BufferRow), RunnableTasks)> {
|
||||
runnable_ranges
|
||||
.into_iter()
|
||||
.filter_map(|mut runnable| {
|
||||
let mut tasks = cx
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Vec<((BufferId, BufferRow), RunnableTasks)>> {
|
||||
cx.spawn(async move |cx| {
|
||||
let mut runnable_rows = Vec::with_capacity(runnable_ranges.len());
|
||||
for mut runnable in runnable_ranges {
|
||||
let Some(tasks) = cx
|
||||
.update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx))
|
||||
.ok()?;
|
||||
.ok()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let mut tasks = tasks.await;
|
||||
|
||||
if prefer_lsp {
|
||||
tasks.retain(|(task_kind, _)| {
|
||||
!matches!(task_kind, TaskSourceKind::Language { .. })
|
||||
});
|
||||
}
|
||||
if tasks.is_empty() {
|
||||
return None;
|
||||
continue;
|
||||
}
|
||||
|
||||
let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot);
|
||||
|
||||
let row = snapshot
|
||||
let Some(row) = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_line_for_row(MultiBufferRow(point.row))?
|
||||
.1
|
||||
.start
|
||||
.row;
|
||||
.buffer_line_for_row(MultiBufferRow(point.row))
|
||||
.map(|(_, range)| range.start.row)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let context_range =
|
||||
BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end);
|
||||
Some((
|
||||
runnable_rows.push((
|
||||
(runnable.buffer_id, row),
|
||||
RunnableTasks {
|
||||
templates: tasks,
|
||||
@@ -14097,16 +14112,17 @@ impl Editor {
|
||||
column: point.column,
|
||||
extra_variables: runnable.extra_captures,
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
));
|
||||
}
|
||||
runnable_rows
|
||||
})
|
||||
}
|
||||
|
||||
fn templates_with_tags(
|
||||
project: &Entity<Project>,
|
||||
runnable: &mut Runnable,
|
||||
cx: &mut App,
|
||||
) -> Vec<(TaskSourceKind, TaskTemplate)> {
|
||||
) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> {
|
||||
let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| {
|
||||
let (worktree_id, file) = project
|
||||
.buffer_for_id(runnable.buffer, cx)
|
||||
@@ -14121,39 +14137,40 @@ impl Editor {
|
||||
)
|
||||
});
|
||||
|
||||
let mut templates_with_tags = mem::take(&mut runnable.tags)
|
||||
.into_iter()
|
||||
.flat_map(|RunnableTag(tag)| {
|
||||
inventory
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flat_map(|inventory| {
|
||||
inventory.read(cx).list_tasks(
|
||||
file.clone(),
|
||||
Some(runnable.language.clone()),
|
||||
worktree_id,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.filter(move |(_, template)| {
|
||||
template.tags.iter().any(|source_tag| source_tag == &tag)
|
||||
})
|
||||
})
|
||||
.sorted_by_key(|(kind, _)| kind.to_owned())
|
||||
.collect::<Vec<_>>();
|
||||
if let Some((leading_tag_source, _)) = templates_with_tags.first() {
|
||||
// Strongest source wins; if we have worktree tag binding, prefer that to
|
||||
// global and language bindings;
|
||||
// if we have a global binding, prefer that to language binding.
|
||||
let first_mismatch = templates_with_tags
|
||||
.iter()
|
||||
.position(|(tag_source, _)| tag_source != leading_tag_source);
|
||||
if let Some(index) = first_mismatch {
|
||||
templates_with_tags.truncate(index);
|
||||
let tags = mem::take(&mut runnable.tags);
|
||||
let language = runnable.language.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let mut templates_with_tags = Vec::new();
|
||||
if let Some(inventory) = inventory {
|
||||
for RunnableTag(tag) in tags {
|
||||
let Ok(new_tasks) = inventory.update(cx, |inventory, cx| {
|
||||
inventory.list_tasks(file.clone(), Some(language.clone()), worktree_id, cx)
|
||||
}) else {
|
||||
return templates_with_tags;
|
||||
};
|
||||
templates_with_tags.extend(new_tasks.await.into_iter().filter(
|
||||
move |(_, template)| {
|
||||
template.tags.iter().any(|source_tag| source_tag == &tag)
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
templates_with_tags.sort_by_key(|(kind, _)| kind.to_owned());
|
||||
|
||||
templates_with_tags
|
||||
if let Some((leading_tag_source, _)) = templates_with_tags.first() {
|
||||
// Strongest source wins; if we have worktree tag binding, prefer that to
|
||||
// global and language bindings;
|
||||
// if we have a global binding, prefer that to language binding.
|
||||
let first_mismatch = templates_with_tags
|
||||
.iter()
|
||||
.position(|(tag_source, _)| tag_source != leading_tag_source);
|
||||
if let Some(index) = first_mismatch {
|
||||
templates_with_tags.truncate(index);
|
||||
}
|
||||
}
|
||||
|
||||
templates_with_tags
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_enclosing_bracket(
|
||||
@@ -14220,18 +14237,20 @@ impl Editor {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);
|
||||
self.end_selection(window, cx);
|
||||
self.selection_history.mode = SelectionHistoryMode::Undoing;
|
||||
if let Some(entry) = self.selection_history.undo_stack.pop_back() {
|
||||
self.change_selections(None, window, cx, |s| {
|
||||
s.select_anchors(entry.selections.to_vec())
|
||||
self.selection_history.mode = SelectionHistoryMode::Undoing;
|
||||
self.with_selection_effects_deferred(window, cx, |this, window, cx| {
|
||||
this.end_selection(window, cx);
|
||||
this.change_selections(Some(Autoscroll::newest()), window, cx, |s| {
|
||||
s.select_anchors(entry.selections.to_vec())
|
||||
});
|
||||
});
|
||||
self.selection_history.mode = SelectionHistoryMode::Normal;
|
||||
|
||||
self.select_next_state = entry.select_next_state;
|
||||
self.select_prev_state = entry.select_prev_state;
|
||||
self.add_selections_state = entry.add_selections_state;
|
||||
self.request_autoscroll(Autoscroll::newest(), cx);
|
||||
}
|
||||
self.selection_history.mode = SelectionHistoryMode::Normal;
|
||||
}
|
||||
|
||||
pub fn redo_selection(
|
||||
@@ -14241,18 +14260,20 @@ impl Editor {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);
|
||||
self.end_selection(window, cx);
|
||||
self.selection_history.mode = SelectionHistoryMode::Redoing;
|
||||
if let Some(entry) = self.selection_history.redo_stack.pop_back() {
|
||||
self.change_selections(None, window, cx, |s| {
|
||||
s.select_anchors(entry.selections.to_vec())
|
||||
self.selection_history.mode = SelectionHistoryMode::Redoing;
|
||||
self.with_selection_effects_deferred(window, cx, |this, window, cx| {
|
||||
this.end_selection(window, cx);
|
||||
this.change_selections(Some(Autoscroll::newest()), window, cx, |s| {
|
||||
s.select_anchors(entry.selections.to_vec())
|
||||
});
|
||||
});
|
||||
self.selection_history.mode = SelectionHistoryMode::Normal;
|
||||
|
||||
self.select_next_state = entry.select_next_state;
|
||||
self.select_prev_state = entry.select_prev_state;
|
||||
self.add_selections_state = entry.add_selections_state;
|
||||
self.request_autoscroll(Autoscroll::newest(), cx);
|
||||
}
|
||||
self.selection_history.mode = SelectionHistoryMode::Normal;
|
||||
}
|
||||
|
||||
pub fn expand_excerpts(
|
||||
@@ -16065,13 +16086,16 @@ impl Editor {
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
let project = self.project.as_ref()?.downgrade();
|
||||
if !self.mode().is_full() {
|
||||
return None;
|
||||
}
|
||||
let pull_diagnostics_settings = ProjectSettings::get_global(cx)
|
||||
.diagnostics
|
||||
.lsp_pull_diagnostics;
|
||||
if !pull_diagnostics_settings.enabled {
|
||||
return None;
|
||||
}
|
||||
let project = self.project.as_ref()?.downgrade();
|
||||
let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms);
|
||||
let mut buffers = self.buffer.read(cx).all_buffers();
|
||||
if let Some(buffer_id) = buffer_id {
|
||||
@@ -18122,16 +18146,9 @@ impl Editor {
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
.map(|selection| {
|
||||
let range = if selection.reversed {
|
||||
selection.end.text_anchor..selection.start.text_anchor
|
||||
} else {
|
||||
selection.start.text_anchor..selection.end.text_anchor
|
||||
};
|
||||
Location {
|
||||
buffer: buffer.clone(),
|
||||
range,
|
||||
}
|
||||
.map(|range| Location {
|
||||
buffer: buffer.clone(),
|
||||
range: range.start.text_anchor..range.end.text_anchor,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -18386,12 +18403,12 @@ impl Editor {
|
||||
|
||||
pub fn highlight_gutter<T: 'static>(
|
||||
&mut self,
|
||||
ranges: &[Range<Anchor>],
|
||||
ranges: impl Into<Vec<Range<Anchor>>>,
|
||||
color_fetcher: fn(&App) -> Hsla,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.gutter_highlights
|
||||
.insert(TypeId::of::<T>(), (color_fetcher, Arc::from(ranges)));
|
||||
.insert(TypeId::of::<T>(), (color_fetcher, ranges.into()));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -18403,6 +18420,65 @@ impl Editor {
|
||||
self.gutter_highlights.remove(&TypeId::of::<T>())
|
||||
}
|
||||
|
||||
pub fn insert_gutter_highlight<T: 'static>(
|
||||
&mut self,
|
||||
range: Range<Anchor>,
|
||||
color_fetcher: fn(&App) -> Hsla,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let mut highlights = self
|
||||
.gutter_highlights
|
||||
.remove(&TypeId::of::<T>())
|
||||
.map(|(_, highlights)| highlights)
|
||||
.unwrap_or_default();
|
||||
let ix = highlights.binary_search_by(|highlight| {
|
||||
Ordering::Equal
|
||||
.then_with(|| highlight.start.cmp(&range.start, &snapshot))
|
||||
.then_with(|| highlight.end.cmp(&range.end, &snapshot))
|
||||
});
|
||||
if let Err(ix) = ix {
|
||||
highlights.insert(ix, range);
|
||||
}
|
||||
self.gutter_highlights
|
||||
.insert(TypeId::of::<T>(), (color_fetcher, highlights));
|
||||
}
|
||||
|
||||
pub fn remove_gutter_highlights<T: 'static>(
|
||||
&mut self,
|
||||
ranges_to_remove: Vec<Range<Anchor>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let Some((color_fetcher, mut gutter_highlights)) =
|
||||
self.gutter_highlights.remove(&TypeId::of::<T>())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let mut ranges_to_remove = ranges_to_remove.iter().peekable();
|
||||
gutter_highlights.retain(|highlight| {
|
||||
while let Some(range_to_remove) = ranges_to_remove.peek() {
|
||||
match range_to_remove.end.cmp(&highlight.start, &snapshot) {
|
||||
Ordering::Less | Ordering::Equal => {
|
||||
ranges_to_remove.next();
|
||||
}
|
||||
Ordering::Greater => {
|
||||
match range_to_remove.start.cmp(&highlight.end, &snapshot) {
|
||||
Ordering::Less | Ordering::Equal => {
|
||||
return false;
|
||||
}
|
||||
Ordering::Greater => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
self.gutter_highlights
|
||||
.insert(TypeId::of::<T>(), (color_fetcher, gutter_highlights));
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn all_text_background_highlights(
|
||||
&self,
|
||||
@@ -18858,7 +18934,7 @@ impl Editor {
|
||||
cx.emit(EditorEvent::BufferEdited);
|
||||
cx.emit(SearchEvent::MatchesInvalidated);
|
||||
if *singleton_buffer_edited {
|
||||
if let Some(buffer) = multibuffer.read(cx).as_singleton() {
|
||||
if let Some(buffer) = edited_buffer {
|
||||
if buffer.read(cx).file().is_none() {
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
}
|
||||
@@ -19665,7 +19741,7 @@ impl Editor {
|
||||
.flatten()
|
||||
.filter_map(|keystroke| {
|
||||
if keystroke.modifiers.is_subset_of(&Modifiers::shift()) {
|
||||
Some(keystroke.key_char.clone().unwrap_or(keystroke.key.clone()))
|
||||
keystroke.key_char.clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -19742,6 +19818,21 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_action_renderer(
|
||||
&mut self,
|
||||
listener: impl Fn(&Editor, &mut Window, &mut Context<Editor>) + 'static,
|
||||
) -> Subscription {
|
||||
let id = self.next_editor_action_id.post_inc();
|
||||
self.editor_actions
|
||||
.borrow_mut()
|
||||
.insert(id, Box::new(listener));
|
||||
|
||||
let editor_actions = self.editor_actions.clone();
|
||||
Subscription::new(move || {
|
||||
editor_actions.borrow_mut().remove(&id);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register_action<A: Action>(
|
||||
&mut self,
|
||||
listener: impl Fn(&A, &mut Window, &mut App) + 'static,
|
||||
@@ -19750,7 +19841,7 @@ impl Editor {
|
||||
let listener = Arc::new(listener);
|
||||
self.editor_actions.borrow_mut().insert(
|
||||
id,
|
||||
Box::new(move |window, _| {
|
||||
Box::new(move |_, window, _| {
|
||||
let listener = listener.clone();
|
||||
window.on_action(TypeId::of::<A>(), move |action, phase, window, cx| {
|
||||
let action = action.downcast_ref().unwrap();
|
||||
@@ -19948,12 +20039,15 @@ impl Editor {
|
||||
if !selections.is_empty() {
|
||||
let snapshot =
|
||||
buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx));
|
||||
// skip adding the initial selection to selection history
|
||||
self.selection_history.mode = SelectionHistoryMode::Skipping;
|
||||
self.change_selections(None, window, cx, |s| {
|
||||
s.select_ranges(selections.into_iter().map(|(start, end)| {
|
||||
snapshot.clip_offset(start, Bias::Left)
|
||||
..snapshot.clip_offset(end, Bias::Right)
|
||||
}));
|
||||
});
|
||||
self.selection_history.mode = SelectionHistoryMode::Normal;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -19979,8 +20073,13 @@ fn process_completion_for_edit(
|
||||
let buffer = buffer.read(cx);
|
||||
let buffer_snapshot = buffer.snapshot();
|
||||
let (snippet, new_text) = if completion.is_snippet() {
|
||||
// Workaround for typescript language server issues so that methods don't expand within
|
||||
// strings and functions with type expressions. The previous point is used because the query
|
||||
// for function identifier doesn't match when the cursor is immediately after. See PR #30312
|
||||
let mut snippet_source = completion.new_text.clone();
|
||||
if let Some(scope) = buffer_snapshot.language_scope_at(cursor_position) {
|
||||
let mut previous_point = text::ToPoint::to_point(cursor_position, buffer);
|
||||
previous_point.column = previous_point.column.saturating_sub(1);
|
||||
if let Some(scope) = buffer_snapshot.language_scope_at(previous_point) {
|
||||
if scope.prefers_label_for_snippet_in_completion() {
|
||||
if let Some(label) = completion.label() {
|
||||
if matches!(
|
||||
@@ -21458,7 +21557,8 @@ impl EditorSnapshot {
|
||||
.unwrap_or(gutter_settings.line_numbers);
|
||||
let line_gutter_width = if show_line_numbers {
|
||||
// Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines.
|
||||
let min_width_for_number_on_gutter = em_advance * MIN_LINE_NUMBER_DIGITS as f32;
|
||||
let min_width_for_number_on_gutter =
|
||||
em_advance * gutter_settings.min_line_number_digits as f32;
|
||||
max_line_number_width.max(min_width_for_number_on_gutter)
|
||||
} else {
|
||||
0.0.into()
|
||||
|
||||
@@ -150,6 +150,7 @@ impl Minimap {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct Gutter {
|
||||
pub min_line_number_digits: usize,
|
||||
pub line_numbers: bool,
|
||||
pub runnables: bool,
|
||||
pub breakpoints: bool,
|
||||
@@ -609,6 +610,10 @@ pub struct GutterContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub line_numbers: Option<bool>,
|
||||
/// Minimum number of characters to reserve space for in the gutter.
|
||||
///
|
||||
/// Default: 4
|
||||
pub min_line_number_digits: Option<usize>,
|
||||
/// Whether to show runnable buttons in the gutter.
|
||||
///
|
||||
/// Default: true
|
||||
|
||||
@@ -1907,7 +1907,6 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
|
||||
DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
|
||||
])
|
||||
});
|
||||
|
||||
editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
|
||||
assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
|
||||
|
||||
@@ -1927,29 +1926,29 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
|
||||
assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
|
||||
|
||||
editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
|
||||
assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
|
||||
assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
|
||||
|
||||
editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
|
||||
assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
|
||||
assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
|
||||
|
||||
editor.move_right(&MoveRight, window, cx);
|
||||
editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
|
||||
assert_selection_ranges(
|
||||
"use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
|
||||
"use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
|
||||
editor,
|
||||
cx,
|
||||
);
|
||||
|
||||
editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
|
||||
assert_selection_ranges(
|
||||
"use std«ˇ::s»tr::{foo, bar}\n\n«ˇ {b»az.qux()}",
|
||||
"use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
|
||||
editor,
|
||||
cx,
|
||||
);
|
||||
|
||||
editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
|
||||
assert_selection_ranges(
|
||||
"use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
|
||||
"use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
|
||||
editor,
|
||||
cx,
|
||||
);
|
||||
@@ -21942,7 +21941,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
|
||||
.expect("created a singleton buffer")
|
||||
.read(cx)
|
||||
.remote_id();
|
||||
let buffer_result_id = project.lsp_store().read(cx).result_id(buffer_id);
|
||||
let buffer_result_id = project.lsp_store().read(cx).result_id(buffer_id, cx);
|
||||
assert_eq!(expected, buffer_result_id);
|
||||
});
|
||||
};
|
||||
@@ -21988,7 +21987,6 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
|
||||
"Cursor movement should not trigger diagnostic request"
|
||||
);
|
||||
ensure_result_id(Some("2".to_string()), cx);
|
||||
|
||||
// Multiple rapid edits should be debounced
|
||||
for _ in 0..5 {
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
|
||||
@@ -6,10 +6,10 @@ use crate::{
|
||||
Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT,
|
||||
FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
|
||||
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
|
||||
MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
OpenExcerpts, PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt,
|
||||
SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SoftWrap,
|
||||
StickyHeaderExcerpt, ToPoint, ToggleFold,
|
||||
MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown,
|
||||
PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase,
|
||||
SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint,
|
||||
ToggleFold,
|
||||
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
|
||||
display_map::{
|
||||
Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightedChunk,
|
||||
@@ -187,7 +187,7 @@ impl EditorElement {
|
||||
let editor = &self.editor;
|
||||
editor.update(cx, |editor, cx| {
|
||||
for action in editor.editor_actions.borrow().values() {
|
||||
(action)(window, cx)
|
||||
(action)(editor, window, cx)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -641,6 +641,7 @@ impl EditorElement {
|
||||
if point_for_position.intersects_selection(&selection) {
|
||||
editor.selection_drag_state = SelectionDragState::ReadyToDrag {
|
||||
selection: newest_anchor.clone(),
|
||||
click_position: event.position,
|
||||
};
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
@@ -832,9 +833,48 @@ impl EditorElement {
|
||||
let pending_nonempty_selections = editor.has_pending_nonempty_selection();
|
||||
let point_for_position = position_map.point_for_position(event.position);
|
||||
|
||||
let is_cut = !event.modifiers.control;
|
||||
if editor.drop_selection(Some(point_for_position), is_cut, window, cx) {
|
||||
return;
|
||||
match editor.selection_drag_state {
|
||||
SelectionDragState::ReadyToDrag {
|
||||
selection: _,
|
||||
ref click_position,
|
||||
} => {
|
||||
if event.position == *click_position {
|
||||
editor.select(
|
||||
SelectPhase::Begin {
|
||||
position: point_for_position.previous_valid,
|
||||
add: false,
|
||||
click_count: 1, // ready to drag state only occurs on click count 1
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.selection_drag_state = SelectionDragState::None;
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
SelectionDragState::Dragging { ref selection, .. } => {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let selection_display = selection.map(|anchor| anchor.to_display_point(&snapshot));
|
||||
if !point_for_position.intersects_selection(&selection_display)
|
||||
&& text_hitbox.is_hovered(window)
|
||||
{
|
||||
let is_cut = !(cfg!(target_os = "macos") && event.modifiers.alt
|
||||
|| cfg!(not(target_os = "macos")) && event.modifiers.control);
|
||||
editor.move_selection_on_drop(
|
||||
&selection.clone(),
|
||||
point_for_position.previous_valid,
|
||||
is_cut,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
editor.selection_drag_state = SelectionDragState::None;
|
||||
cx.stop_propagation();
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if end_selection {
|
||||
@@ -905,7 +945,8 @@ impl EditorElement {
|
||||
return;
|
||||
}
|
||||
|
||||
let text_bounds = position_map.text_hitbox.bounds;
|
||||
let text_hitbox = &position_map.text_hitbox;
|
||||
let text_bounds = text_hitbox.bounds;
|
||||
let point_for_position = position_map.point_for_position(event.position);
|
||||
|
||||
let mut scroll_delta = gpui::Point::<f32>::default();
|
||||
@@ -946,12 +987,14 @@ impl EditorElement {
|
||||
match editor.selection_drag_state {
|
||||
SelectionDragState::Dragging {
|
||||
ref mut drop_cursor,
|
||||
ref mut hide_drop_cursor,
|
||||
..
|
||||
} => {
|
||||
drop_cursor.start = drop_anchor;
|
||||
drop_cursor.end = drop_anchor;
|
||||
*hide_drop_cursor = !text_hitbox.is_hovered(window);
|
||||
}
|
||||
SelectionDragState::ReadyToDrag { ref selection } => {
|
||||
SelectionDragState::ReadyToDrag { ref selection, .. } => {
|
||||
let drop_cursor = Selection {
|
||||
id: post_inc(&mut editor.selections.next_selection_id),
|
||||
start: drop_anchor,
|
||||
@@ -962,6 +1005,7 @@ impl EditorElement {
|
||||
editor.selection_drag_state = SelectionDragState::Dragging {
|
||||
selection: selection.clone(),
|
||||
drop_cursor,
|
||||
hide_drop_cursor: false,
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
@@ -995,7 +1039,7 @@ impl EditorElement {
|
||||
editor.set_gutter_hovered(gutter_hovered, cx);
|
||||
editor.mouse_cursor_hidden = false;
|
||||
|
||||
if gutter_hovered {
|
||||
let breakpoint_indicator = if gutter_hovered {
|
||||
let new_point = position_map
|
||||
.point_for_position(event.position)
|
||||
.previous_valid;
|
||||
@@ -1009,7 +1053,6 @@ impl EditorElement {
|
||||
.buffer_for_excerpt(buffer_anchor.excerpt_id)
|
||||
.and_then(|buffer| buffer.file().map(|file| (buffer, file)))
|
||||
{
|
||||
let was_hovered = editor.gutter_breakpoint_indicator.0.is_some();
|
||||
let as_point = text::ToPoint::to_point(&buffer_anchor.text_anchor, buffer_snapshot);
|
||||
|
||||
let is_visible = editor
|
||||
@@ -1037,38 +1080,43 @@ impl EditorElement {
|
||||
.is_some()
|
||||
});
|
||||
|
||||
editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
|
||||
display_row: new_point.row(),
|
||||
is_active: is_visible,
|
||||
collides_with_existing_breakpoint: has_existing_breakpoint,
|
||||
});
|
||||
|
||||
editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| {
|
||||
cx.spawn(async move |this, cx| {
|
||||
if !was_hovered {
|
||||
if !is_visible {
|
||||
editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| {
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(200))
|
||||
.await;
|
||||
}
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut() {
|
||||
indicator.is_active = true;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut()
|
||||
{
|
||||
indicator.is_active = true;
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Some(PhantomBreakpointIndicator {
|
||||
display_row: new_point.row(),
|
||||
is_active: is_visible,
|
||||
collides_with_existing_breakpoint: has_existing_breakpoint,
|
||||
})
|
||||
} else {
|
||||
editor.gutter_breakpoint_indicator = (None, None);
|
||||
editor.gutter_breakpoint_indicator.1 = None;
|
||||
None
|
||||
}
|
||||
} else {
|
||||
editor.gutter_breakpoint_indicator = (None, None);
|
||||
}
|
||||
editor.gutter_breakpoint_indicator.1 = None;
|
||||
None
|
||||
};
|
||||
|
||||
cx.notify();
|
||||
if &breakpoint_indicator != &editor.gutter_breakpoint_indicator.0 {
|
||||
editor.gutter_breakpoint_indicator.0 = breakpoint_indicator;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// Don't trigger hover popover if mouse is hovering over context menu
|
||||
if text_hitbox.is_hovered(window) {
|
||||
@@ -1211,16 +1259,18 @@ impl EditorElement {
|
||||
if let SelectionDragState::Dragging {
|
||||
ref selection,
|
||||
ref drop_cursor,
|
||||
ref hide_drop_cursor,
|
||||
} = editor.selection_drag_state
|
||||
{
|
||||
if drop_cursor
|
||||
.start
|
||||
.cmp(&selection.start, &snapshot.buffer_snapshot)
|
||||
.eq(&Ordering::Less)
|
||||
|| drop_cursor
|
||||
.end
|
||||
.cmp(&selection.end, &snapshot.buffer_snapshot)
|
||||
.eq(&Ordering::Greater)
|
||||
if !hide_drop_cursor
|
||||
&& (drop_cursor
|
||||
.start
|
||||
.cmp(&selection.start, &snapshot.buffer_snapshot)
|
||||
.eq(&Ordering::Less)
|
||||
|| drop_cursor
|
||||
.end
|
||||
.cmp(&selection.end, &snapshot.buffer_snapshot)
|
||||
.eq(&Ordering::Greater))
|
||||
{
|
||||
let drag_cursor_layout = SelectionLayout::new(
|
||||
drop_cursor.clone(),
|
||||
@@ -2776,7 +2826,8 @@ impl EditorElement {
|
||||
let available_width = gutter_dimensions.left_padding - git_gutter_width;
|
||||
|
||||
let editor = self.editor.clone();
|
||||
let is_wide = max_line_number_length >= MIN_LINE_NUMBER_DIGITS
|
||||
let is_wide = max_line_number_length
|
||||
>= EditorSettings::get_global(cx).gutter.min_line_number_digits as u32
|
||||
&& row_info
|
||||
.buffer_row
|
||||
.is_some_and(|row| (row + 1).ilog10() + 1 == max_line_number_length)
|
||||
@@ -7289,6 +7340,17 @@ impl LineWithInvisibles {
|
||||
paint(window, cx);
|
||||
}),
|
||||
|
||||
ShowWhitespaceSetting::Trailing => {
|
||||
let mut previous_start = self.len;
|
||||
for ([start, end], paint) in invisible_iter.rev() {
|
||||
if previous_start != end {
|
||||
break;
|
||||
}
|
||||
previous_start = start;
|
||||
paint(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
// For a whitespace to be on a boundary, any of the following conditions need to be met:
|
||||
// - It is a tab
|
||||
// - It is adjacent to an edge (start or end)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DebuggerEvaluateSelectedText, DisplayPoint,
|
||||
DisplaySnapshot, Editor, FindAllReferences, GoToDeclaration, GoToDefinition,
|
||||
GoToImplementation, GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode,
|
||||
SelectionExt, ToDisplayPoint, ToggleCodeActions,
|
||||
Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor,
|
||||
EvaluateSelectedText, FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation,
|
||||
GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionExt,
|
||||
ToDisplayPoint, ToggleCodeActions,
|
||||
actions::{Format, FormatSelections},
|
||||
selections_collection::SelectionsCollection,
|
||||
};
|
||||
@@ -199,17 +199,14 @@ pub fn deploy_context_menu(
|
||||
.is_some()
|
||||
});
|
||||
|
||||
let evaluate_selection = command_palette_hooks::CommandPaletteFilter::try_global(cx)
|
||||
.map_or(false, |filter| {
|
||||
!filter.is_hidden(&DebuggerEvaluateSelectedText)
|
||||
});
|
||||
let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx);
|
||||
|
||||
ui::ContextMenu::build(window, cx, |menu, _window, _cx| {
|
||||
let builder = menu
|
||||
.on_blur_subscription(Subscription::new(|| {}))
|
||||
.when(evaluate_selection && has_selections, |builder| {
|
||||
builder
|
||||
.action("Evaluate Selection", Box::new(DebuggerEvaluateSelectedText))
|
||||
.action("Evaluate Selection", Box::new(EvaluateSelectedText))
|
||||
.separator()
|
||||
})
|
||||
.action("Go to Definition", Box::new(GoToDefinition))
|
||||
|
||||
@@ -266,10 +266,11 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa
|
||||
|
||||
let mut is_first_iteration = true;
|
||||
find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
|
||||
// Make alt-left skip punctuation on Mac OS to respect Mac VSCode behaviour. For example: hello.| goes to |hello.
|
||||
// Make alt-left skip punctuation to respect VSCode behaviour. For example: hello.| goes to |hello.
|
||||
if is_first_iteration
|
||||
&& classifier.is_punctuation(right)
|
||||
&& !classifier.is_punctuation(left)
|
||||
&& left != '\n'
|
||||
{
|
||||
is_first_iteration = false;
|
||||
return false;
|
||||
@@ -318,10 +319,11 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint
|
||||
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
|
||||
let mut is_first_iteration = true;
|
||||
find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
||||
// Make alt-right skip punctuation on Mac OS to respect the Mac behaviour. For example: |.hello goes to .hello|
|
||||
// Make alt-right skip punctuation to respect VSCode behaviour. For example: |.hello goes to .hello|
|
||||
if is_first_iteration
|
||||
&& classifier.is_punctuation(left)
|
||||
&& !classifier.is_punctuation(right)
|
||||
&& right != '\n'
|
||||
{
|
||||
is_first_iteration = false;
|
||||
return false;
|
||||
|
||||
@@ -411,7 +411,7 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
self.collection.display_map(self.cx)
|
||||
}
|
||||
|
||||
pub fn buffer(&self) -> Ref<MultiBufferSnapshot> {
|
||||
pub fn buffer(&self) -> Ref<'_, MultiBufferSnapshot> {
|
||||
self.collection.buffer(self.cx)
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ interface dap {
|
||||
}
|
||||
|
||||
record debug-adapter-binary {
|
||||
command: string,
|
||||
command: option<string>,
|
||||
arguments: list<string>,
|
||||
envs: env-vars,
|
||||
cwd: option<string>,
|
||||
|
||||
@@ -1682,12 +1682,15 @@ impl ExtensionStore {
|
||||
|
||||
pub fn register_ssh_client(&mut self, client: Entity<SshRemoteClient>, cx: &mut Context<Self>) {
|
||||
let connection_options = client.read(cx).connection_options();
|
||||
if self.ssh_clients.contains_key(&connection_options.ssh_url()) {
|
||||
return;
|
||||
let ssh_url = connection_options.ssh_url();
|
||||
|
||||
if let Some(existing_client) = self.ssh_clients.get(&ssh_url) {
|
||||
if existing_client.upgrade().is_some() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.ssh_clients
|
||||
.insert(connection_options.ssh_url(), client.downgrade());
|
||||
self.ssh_clients.insert(ssh_url, client.downgrade());
|
||||
self.ssh_registered_tx.unbounded_send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,7 +724,7 @@ impl IncrementalCompilationCache {
|
||||
}
|
||||
|
||||
impl CacheStore for IncrementalCompilationCache {
|
||||
fn get(&self, key: &[u8]) -> Option<Cow<[u8]>> {
|
||||
fn get(&self, key: &[u8]) -> Option<Cow<'_, [u8]>> {
|
||||
self.cache.get(key).map(|v| v.into())
|
||||
}
|
||||
|
||||
|
||||
@@ -554,13 +554,15 @@ impl ExtensionsPage {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.child(
|
||||
Button::new(
|
||||
SharedString::from(format!("rebuild-{}", extension.id)),
|
||||
"Rebuild",
|
||||
)
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Upgrading))
|
||||
.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
move |_, _, cx| {
|
||||
@@ -568,12 +570,12 @@ impl ExtensionsPage {
|
||||
store.rebuild_dev_extension(extension_id.clone(), cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Upgrading)),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new(SharedString::from(extension.id.clone()), "Uninstall")
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Removing))
|
||||
.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
move |_, _, cx| {
|
||||
@@ -581,9 +583,7 @@ impl ExtensionsPage {
|
||||
store.uninstall_extension(extension_id.clone(), cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Removing)),
|
||||
}),
|
||||
)
|
||||
.when(can_configure, |this| {
|
||||
this.child(
|
||||
@@ -591,8 +591,8 @@ impl ExtensionsPage {
|
||||
SharedString::from(format!("configure-{}", extension.id)),
|
||||
"Configure",
|
||||
)
|
||||
|
||||
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Installing))
|
||||
.on_click({
|
||||
let manifest = Arc::new(extension.clone());
|
||||
move |_, _, cx| {
|
||||
@@ -609,9 +609,7 @@ impl ExtensionsPage {
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Installing)),
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
@@ -1479,18 +1477,12 @@ impl Render for ExtensionsPage {
|
||||
return this.py_4().child(self.render_empty_state(cx));
|
||||
}
|
||||
|
||||
let extensions_page = cx.entity().clone();
|
||||
let scroll_handle = self.list.clone();
|
||||
this.child(
|
||||
uniform_list(
|
||||
extensions_page,
|
||||
"entries",
|
||||
count,
|
||||
Self::render_extensions,
|
||||
)
|
||||
.flex_grow()
|
||||
.pb_4()
|
||||
.track_scroll(scroll_handle),
|
||||
uniform_list("entries", count, cx.processor(Self::render_extensions))
|
||||
.flex_grow()
|
||||
.pb_4()
|
||||
.track_scroll(scroll_handle),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -323,7 +323,7 @@ pub trait GitRepository: Send + Sync {
|
||||
/// Resolve a list of refs to SHAs.
|
||||
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>>;
|
||||
|
||||
fn head_sha(&self) -> BoxFuture<Option<String>> {
|
||||
fn head_sha(&self) -> BoxFuture<'_, Option<String>> {
|
||||
async move {
|
||||
self.revparse_batch(vec!["HEAD".into()])
|
||||
.await
|
||||
@@ -525,7 +525,7 @@ impl GitRepository for RealGitRepository {
|
||||
repo.commondir().into()
|
||||
}
|
||||
|
||||
fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>> {
|
||||
fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>> {
|
||||
let working_directory = self.working_directory();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
@@ -561,7 +561,7 @@ impl GitRepository for RealGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>> {
|
||||
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result<CommitDiff>> {
|
||||
let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned)
|
||||
else {
|
||||
return future::ready(Err(anyhow!("no working directory"))).boxed();
|
||||
@@ -668,7 +668,7 @@ impl GitRepository for RealGitRepository {
|
||||
commit: String,
|
||||
mode: ResetMode,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
) -> BoxFuture<'_, Result<()>> {
|
||||
async move {
|
||||
let working_directory = self.working_directory();
|
||||
|
||||
@@ -698,7 +698,7 @@ impl GitRepository for RealGitRepository {
|
||||
commit: String,
|
||||
paths: Vec<RepoPath>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
) -> BoxFuture<'_, Result<()>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
async move {
|
||||
@@ -723,7 +723,7 @@ impl GitRepository for RealGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
|
||||
fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
|
||||
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
|
||||
const GIT_MODE_SYMLINK: u32 = 0o120000;
|
||||
|
||||
@@ -756,7 +756,7 @@ impl GitRepository for RealGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
|
||||
fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
|
||||
let repo = self.repository.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
@@ -777,7 +777,7 @@ impl GitRepository for RealGitRepository {
|
||||
path: RepoPath,
|
||||
content: Option<String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<anyhow::Result<()>> {
|
||||
) -> BoxFuture<'_, anyhow::Result<()>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
self.executor
|
||||
@@ -841,7 +841,7 @@ impl GitRepository for RealGitRepository {
|
||||
remote.url().map(|url| url.to_string())
|
||||
}
|
||||
|
||||
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>> {
|
||||
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {
|
||||
let working_directory = self.working_directory();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
@@ -891,14 +891,14 @@ impl GitRepository for RealGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn merge_message(&self) -> BoxFuture<Option<String>> {
|
||||
fn merge_message(&self) -> BoxFuture<'_, Option<String>> {
|
||||
let path = self.path().join("MERGE_MSG");
|
||||
self.executor
|
||||
.spawn(async move { std::fs::read_to_string(&path).ok() })
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>> {
|
||||
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result<GitStatus>> {
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
let working_directory = self.working_directory();
|
||||
let path_prefixes = path_prefixes.to_owned();
|
||||
@@ -919,7 +919,7 @@ impl GitRepository for RealGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
|
||||
fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
self.executor
|
||||
@@ -986,7 +986,7 @@ impl GitRepository for RealGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn change_branch(&self, name: String) -> BoxFuture<Result<()>> {
|
||||
fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
|
||||
let repo = self.repository.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
@@ -1018,7 +1018,7 @@ impl GitRepository for RealGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn create_branch(&self, name: String) -> BoxFuture<Result<()>> {
|
||||
fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
|
||||
let repo = self.repository.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
@@ -1030,7 +1030,7 @@ impl GitRepository for RealGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<Result<crate::blame::Blame>> {
|
||||
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<'_, Result<crate::blame::Blame>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
|
||||
@@ -1052,7 +1052,7 @@ impl GitRepository for RealGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn diff(&self, diff: DiffType) -> BoxFuture<Result<String>> {
|
||||
fn diff(&self, diff: DiffType) -> BoxFuture<'_, Result<String>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
self.executor
|
||||
@@ -1083,7 +1083,7 @@ impl GitRepository for RealGitRepository {
|
||||
&self,
|
||||
paths: Vec<RepoPath>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
) -> BoxFuture<'_, Result<()>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
self.executor
|
||||
@@ -1111,7 +1111,7 @@ impl GitRepository for RealGitRepository {
|
||||
&self,
|
||||
paths: Vec<RepoPath>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
) -> BoxFuture<'_, Result<()>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
|
||||
@@ -1143,7 +1143,7 @@ impl GitRepository for RealGitRepository {
|
||||
name_and_email: Option<(SharedString, SharedString)>,
|
||||
options: CommitOptions,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
) -> BoxFuture<'_, Result<()>> {
|
||||
let working_directory = self.working_directory();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
@@ -1182,7 +1182,7 @@ impl GitRepository for RealGitRepository {
|
||||
ask_pass: AskPassDelegate,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
cx: AsyncApp,
|
||||
) -> BoxFuture<Result<RemoteCommandOutput>> {
|
||||
) -> BoxFuture<'_, Result<RemoteCommandOutput>> {
|
||||
let working_directory = self.working_directory();
|
||||
let executor = cx.background_executor().clone();
|
||||
async move {
|
||||
@@ -1214,7 +1214,7 @@ impl GitRepository for RealGitRepository {
|
||||
ask_pass: AskPassDelegate,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
cx: AsyncApp,
|
||||
) -> BoxFuture<Result<RemoteCommandOutput>> {
|
||||
) -> BoxFuture<'_, Result<RemoteCommandOutput>> {
|
||||
let working_directory = self.working_directory();
|
||||
let executor = cx.background_executor().clone();
|
||||
async move {
|
||||
@@ -1239,7 +1239,7 @@ impl GitRepository for RealGitRepository {
|
||||
ask_pass: AskPassDelegate,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
cx: AsyncApp,
|
||||
) -> BoxFuture<Result<RemoteCommandOutput>> {
|
||||
) -> BoxFuture<'_, Result<RemoteCommandOutput>> {
|
||||
let working_directory = self.working_directory();
|
||||
let remote_name = format!("{}", fetch_options);
|
||||
let executor = cx.background_executor().clone();
|
||||
@@ -1257,7 +1257,7 @@ impl GitRepository for RealGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<Result<Vec<Remote>>> {
|
||||
fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<'_, Result<Vec<Remote>>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
self.executor
|
||||
@@ -1303,7 +1303,7 @@ impl GitRepository for RealGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<SharedString>>> {
|
||||
fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<SharedString>>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
self.executor
|
||||
@@ -1396,7 +1396,7 @@ impl GitRepository for RealGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
|
||||
fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
|
||||
@@ -1435,7 +1435,7 @@ impl GitRepository for RealGitRepository {
|
||||
&self,
|
||||
left: GitRepositoryCheckpoint,
|
||||
right: GitRepositoryCheckpoint,
|
||||
) -> BoxFuture<Result<bool>> {
|
||||
) -> BoxFuture<'_, Result<bool>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
|
||||
@@ -1474,7 +1474,7 @@ impl GitRepository for RealGitRepository {
|
||||
&self,
|
||||
base_checkpoint: GitRepositoryCheckpoint,
|
||||
target_checkpoint: GitRepositoryCheckpoint,
|
||||
) -> BoxFuture<Result<String>> {
|
||||
) -> BoxFuture<'_, Result<String>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
|
||||
|
||||
@@ -248,6 +248,8 @@ fn conflicts_updated(
|
||||
removed_block_ids.insert(block_id);
|
||||
}
|
||||
|
||||
editor.remove_gutter_highlights::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
|
||||
|
||||
editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
|
||||
editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
|
||||
editor
|
||||
@@ -325,8 +327,7 @@ fn update_conflict_highlighting(
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
log::debug!("update conflict highlighting for {conflict:?}");
|
||||
let theme = cx.theme().clone();
|
||||
let colors = theme.colors();
|
||||
|
||||
let outer_start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, conflict.range.start)
|
||||
.unwrap();
|
||||
@@ -346,26 +347,29 @@ fn update_conflict_highlighting(
|
||||
.anchor_in_excerpt(excerpt_id, conflict.theirs.end)
|
||||
.unwrap();
|
||||
|
||||
let ours_background = colors.version_control_conflict_ours_background;
|
||||
let ours_marker = colors.version_control_conflict_ours_marker_background;
|
||||
let theirs_background = colors.version_control_conflict_theirs_background;
|
||||
let theirs_marker = colors.version_control_conflict_theirs_marker_background;
|
||||
let divider_background = colors.version_control_conflict_divider_background;
|
||||
let ours_background = cx.theme().colors().version_control_conflict_marker_ours;
|
||||
let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs;
|
||||
|
||||
let options = RowHighlightOptions {
|
||||
include_gutter: false,
|
||||
include_gutter: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
editor.insert_gutter_highlight::<ConflictsOuter>(
|
||||
outer_start..their_end,
|
||||
|cx| cx.theme().colors().editor_background,
|
||||
cx,
|
||||
);
|
||||
|
||||
// Prevent diff hunk highlighting within the entire conflict region.
|
||||
editor.highlight_rows::<ConflictsOuter>(
|
||||
outer_start..outer_end,
|
||||
divider_background,
|
||||
editor.highlight_rows::<ConflictsOuter>(outer_start..outer_end, theirs_background, options, cx);
|
||||
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
|
||||
editor.highlight_rows::<ConflictsOursMarker>(
|
||||
outer_start..our_start,
|
||||
ours_background,
|
||||
options,
|
||||
cx,
|
||||
);
|
||||
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
|
||||
editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
|
||||
editor.highlight_rows::<ConflictsTheirs>(
|
||||
their_start..their_end,
|
||||
theirs_background,
|
||||
@@ -374,7 +378,7 @@ fn update_conflict_highlighting(
|
||||
);
|
||||
editor.highlight_rows::<ConflictsTheirsMarker>(
|
||||
their_end..outer_end,
|
||||
theirs_marker,
|
||||
theirs_background,
|
||||
options,
|
||||
cx,
|
||||
);
|
||||
@@ -512,6 +516,9 @@ pub(crate) fn resolve_conflict(
|
||||
let end = snapshot
|
||||
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
|
||||
.unwrap();
|
||||
|
||||
editor.remove_gutter_highlights::<ConflictsOuter>(vec![start..end], cx);
|
||||
|
||||
editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
|
||||
editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
|
||||
editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
|
||||
|
||||
@@ -27,11 +27,12 @@ use git::status::StageStatus;
|
||||
use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus};
|
||||
use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, Axis, ClickEvent, Corner, DismissEvent, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior,
|
||||
ListSizingBehavior, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, Point,
|
||||
PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle,
|
||||
WeakEntity, actions, anchored, deferred, percentage, uniform_list,
|
||||
Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner,
|
||||
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
|
||||
ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
|
||||
MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task,
|
||||
Transformation, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, percentage,
|
||||
uniform_list,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, File};
|
||||
@@ -54,6 +55,7 @@ use project::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use std::future::Future;
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{collections::HashSet, sync::Arc, time::Duration, usize};
|
||||
use strum::{IntoEnumIterator, VariantNames};
|
||||
@@ -63,11 +65,11 @@ use ui::{
|
||||
Tooltip, prelude::*,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt, maybe};
|
||||
use workspace::AppState;
|
||||
|
||||
use workspace::{
|
||||
Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::DetachAndPromptErr,
|
||||
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId},
|
||||
};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
@@ -389,144 +391,148 @@ pub(crate) fn commit_message_editor(
|
||||
}
|
||||
|
||||
impl GitPanel {
|
||||
pub fn new(
|
||||
workspace: Entity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
app_state: Arc<AppState>,
|
||||
fn new(
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Entity<Self> {
|
||||
let project = workspace.project().clone();
|
||||
let app_state = workspace.app_state().clone();
|
||||
let fs = app_state.fs.clone();
|
||||
let git_store = project.read(cx).git_store().clone();
|
||||
let active_repository = project.read(cx).active_repository(cx);
|
||||
let workspace = workspace.downgrade();
|
||||
|
||||
let focus_handle = cx.focus_handle();
|
||||
cx.on_focus(&focus_handle, window, Self::focus_in).detach();
|
||||
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
|
||||
this.hide_scrollbars(window, cx);
|
||||
})
|
||||
.detach();
|
||||
let git_panel = cx.new(|cx| {
|
||||
let focus_handle = cx.focus_handle();
|
||||
cx.on_focus(&focus_handle, window, Self::focus_in).detach();
|
||||
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
|
||||
this.hide_scrollbars(window, cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
|
||||
cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
|
||||
if is_sort_by_path != was_sort_by_path {
|
||||
this.update_visible_entries(cx);
|
||||
}
|
||||
was_sort_by_path = is_sort_by_path
|
||||
})
|
||||
.detach();
|
||||
let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
|
||||
cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
|
||||
if is_sort_by_path != was_sort_by_path {
|
||||
this.update_visible_entries(cx);
|
||||
}
|
||||
was_sort_by_path = is_sort_by_path
|
||||
})
|
||||
.detach();
|
||||
|
||||
// just to let us render a placeholder editor.
|
||||
// Once the active git repo is set, this buffer will be replaced.
|
||||
let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
|
||||
let commit_editor = cx.new(|cx| {
|
||||
commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx)
|
||||
// just to let us render a placeholder editor.
|
||||
// Once the active git repo is set, this buffer will be replaced.
|
||||
let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
|
||||
let commit_editor = cx.new(|cx| {
|
||||
commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx)
|
||||
});
|
||||
|
||||
commit_editor.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
});
|
||||
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
|
||||
let vertical_scrollbar = ScrollbarProperties {
|
||||
axis: Axis::Vertical,
|
||||
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
|
||||
show_scrollbar: false,
|
||||
show_track: false,
|
||||
auto_hide: false,
|
||||
hide_task: None,
|
||||
};
|
||||
|
||||
let horizontal_scrollbar = ScrollbarProperties {
|
||||
axis: Axis::Horizontal,
|
||||
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
|
||||
show_scrollbar: false,
|
||||
show_track: false,
|
||||
auto_hide: false,
|
||||
hide_task: None,
|
||||
};
|
||||
|
||||
let mut assistant_enabled = AgentSettings::get_global(cx).enabled;
|
||||
let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
|
||||
if assistant_enabled != AgentSettings::get_global(cx).enabled {
|
||||
assistant_enabled = AgentSettings::get_global(cx).enabled;
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
cx.subscribe_in(
|
||||
&git_store,
|
||||
window,
|
||||
move |this, _git_store, event, window, cx| match event {
|
||||
GitStoreEvent::ActiveRepositoryChanged(_) => {
|
||||
this.active_repository = this.project.read(cx).active_repository(cx);
|
||||
this.schedule_update(true, window, cx);
|
||||
}
|
||||
GitStoreEvent::RepositoryUpdated(
|
||||
_,
|
||||
RepositoryEvent::Updated { full_scan },
|
||||
true,
|
||||
) => {
|
||||
this.schedule_update(*full_scan, window, cx);
|
||||
}
|
||||
|
||||
GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => {
|
||||
this.schedule_update(false, window, cx);
|
||||
}
|
||||
GitStoreEvent::IndexWriteError(error) => {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_error(error, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
GitStoreEvent::RepositoryUpdated(_, _, _) => {}
|
||||
GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
let mut this = Self {
|
||||
active_repository,
|
||||
commit_editor,
|
||||
conflicted_count: 0,
|
||||
conflicted_staged_count: 0,
|
||||
current_modifiers: window.modifiers(),
|
||||
add_coauthors: true,
|
||||
generate_commit_message_task: None,
|
||||
entries: Vec::new(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
fs,
|
||||
new_count: 0,
|
||||
new_staged_count: 0,
|
||||
pending: Vec::new(),
|
||||
pending_commit: None,
|
||||
amend_pending: false,
|
||||
pending_serialization: Task::ready(None),
|
||||
single_staged_entry: None,
|
||||
single_tracked_entry: None,
|
||||
project,
|
||||
scroll_handle,
|
||||
max_width_item_index: None,
|
||||
selected_entry: None,
|
||||
marked_entries: Vec::new(),
|
||||
tracked_count: 0,
|
||||
tracked_staged_count: 0,
|
||||
update_visible_entries_task: Task::ready(()),
|
||||
width: None,
|
||||
show_placeholders: false,
|
||||
context_menu: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
modal_open: false,
|
||||
entry_count: 0,
|
||||
horizontal_scrollbar,
|
||||
vertical_scrollbar,
|
||||
_settings_subscription,
|
||||
};
|
||||
|
||||
this.schedule_update(false, window, cx);
|
||||
this
|
||||
});
|
||||
|
||||
commit_editor.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
});
|
||||
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
|
||||
cx.subscribe_in(
|
||||
&git_store,
|
||||
window,
|
||||
move |this, git_store, event, window, cx| match event {
|
||||
GitStoreEvent::ActiveRepositoryChanged(_) => {
|
||||
this.active_repository = git_store.read(cx).active_repository();
|
||||
this.schedule_update(true, window, cx);
|
||||
}
|
||||
GitStoreEvent::RepositoryUpdated(
|
||||
_,
|
||||
RepositoryEvent::Updated { full_scan },
|
||||
true,
|
||||
) => {
|
||||
this.schedule_update(*full_scan, window, cx);
|
||||
}
|
||||
|
||||
GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => {
|
||||
this.schedule_update(false, window, cx);
|
||||
}
|
||||
GitStoreEvent::IndexWriteError(error) => {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_error(error, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
GitStoreEvent::RepositoryUpdated(_, _, _) => {}
|
||||
GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
let vertical_scrollbar = ScrollbarProperties {
|
||||
axis: Axis::Vertical,
|
||||
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
|
||||
show_scrollbar: false,
|
||||
show_track: false,
|
||||
auto_hide: false,
|
||||
hide_task: None,
|
||||
};
|
||||
|
||||
let horizontal_scrollbar = ScrollbarProperties {
|
||||
axis: Axis::Horizontal,
|
||||
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
|
||||
show_scrollbar: false,
|
||||
show_track: false,
|
||||
auto_hide: false,
|
||||
hide_task: None,
|
||||
};
|
||||
|
||||
let mut assistant_enabled = AgentSettings::get_global(cx).enabled;
|
||||
let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
|
||||
if assistant_enabled != AgentSettings::get_global(cx).enabled {
|
||||
assistant_enabled = AgentSettings::get_global(cx).enabled;
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
let mut git_panel = Self {
|
||||
active_repository,
|
||||
commit_editor,
|
||||
conflicted_count: 0,
|
||||
conflicted_staged_count: 0,
|
||||
current_modifiers: window.modifiers(),
|
||||
add_coauthors: true,
|
||||
generate_commit_message_task: None,
|
||||
entries: Vec::new(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
fs,
|
||||
new_count: 0,
|
||||
new_staged_count: 0,
|
||||
pending: Vec::new(),
|
||||
pending_commit: None,
|
||||
amend_pending: false,
|
||||
pending_serialization: Task::ready(None),
|
||||
single_staged_entry: None,
|
||||
single_tracked_entry: None,
|
||||
project,
|
||||
scroll_handle,
|
||||
max_width_item_index: None,
|
||||
selected_entry: None,
|
||||
marked_entries: Vec::new(),
|
||||
tracked_count: 0,
|
||||
tracked_staged_count: 0,
|
||||
update_visible_entries_task: Task::ready(()),
|
||||
width: None,
|
||||
show_placeholders: false,
|
||||
context_menu: None,
|
||||
workspace,
|
||||
modal_open: false,
|
||||
entry_count: 0,
|
||||
horizontal_scrollbar,
|
||||
vertical_scrollbar,
|
||||
_settings_subscription,
|
||||
};
|
||||
git_panel.schedule_update(false, window, cx);
|
||||
git_panel
|
||||
}
|
||||
|
||||
@@ -1774,7 +1780,19 @@ impl GitPanel {
|
||||
this.generate_commit_message_task.take();
|
||||
});
|
||||
|
||||
let mut diff_text = diff.await??;
|
||||
let mut diff_text = match diff.await {
|
||||
Ok(result) => match result {
|
||||
Ok(text) => text,
|
||||
Err(e) => {
|
||||
Self::show_commit_message_error(&this, &e, cx);
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
Self::show_commit_message_error(&this, &e, cx);
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
const ONE_MB: usize = 1_000_000;
|
||||
if diff_text.len() > ONE_MB {
|
||||
@@ -1812,26 +1830,37 @@ impl GitPanel {
|
||||
};
|
||||
|
||||
let stream = model.stream_completion_text(request, &cx);
|
||||
let mut messages = stream.await?;
|
||||
match stream.await {
|
||||
Ok(mut messages) => {
|
||||
if !text_empty {
|
||||
this.update(cx, |this, cx| {
|
||||
this.commit_message_buffer(cx).update(cx, |buffer, cx| {
|
||||
let insert_position = buffer.anchor_before(buffer.len());
|
||||
buffer.edit([(insert_position..insert_position, "\n")], None, cx)
|
||||
});
|
||||
})?;
|
||||
}
|
||||
|
||||
if !text_empty {
|
||||
this.update(cx, |this, cx| {
|
||||
this.commit_message_buffer(cx).update(cx, |buffer, cx| {
|
||||
let insert_position = buffer.anchor_before(buffer.len());
|
||||
buffer.edit([(insert_position..insert_position, "\n")], None, cx)
|
||||
});
|
||||
})?;
|
||||
}
|
||||
|
||||
while let Some(message) = messages.stream.next().await {
|
||||
let text = message?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.commit_message_buffer(cx).update(cx, |buffer, cx| {
|
||||
let insert_position = buffer.anchor_before(buffer.len());
|
||||
buffer.edit([(insert_position..insert_position, text)], None, cx);
|
||||
});
|
||||
})?;
|
||||
while let Some(message) = messages.stream.next().await {
|
||||
match message {
|
||||
Ok(text) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.commit_message_buffer(cx).update(cx, |buffer, cx| {
|
||||
let insert_position = buffer.anchor_before(buffer.len());
|
||||
buffer.edit([(insert_position..insert_position, text)], None, cx);
|
||||
});
|
||||
})?;
|
||||
}
|
||||
Err(e) => {
|
||||
Self::show_commit_message_error(&this, &e, cx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
Self::show_commit_message_error(&this, &e, cx);
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -2689,6 +2718,26 @@ impl GitPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn show_commit_message_error<E>(weak_this: &WeakEntity<Self>, err: &E, cx: &mut AsyncApp)
|
||||
where
|
||||
E: std::fmt::Debug + std::fmt::Display,
|
||||
{
|
||||
if let Ok(Some(workspace)) = weak_this.update(cx, |this, _cx| this.workspace.upgrade()) {
|
||||
let _ = workspace.update(cx, |workspace, cx| {
|
||||
struct CommitMessageError;
|
||||
let notification_id = NotificationId::unique::<CommitMessageError>();
|
||||
workspace.show_notification(notification_id, cx, |cx| {
|
||||
cx.new(|cx| {
|
||||
ErrorMessagePrompt::new(
|
||||
format!("Failed to generate commit message: {err}"),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn show_remote_output(&self, action: RemoteAction, info: RemoteCommandOutput, cx: &mut App) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
@@ -3662,8 +3711,10 @@ impl GitPanel {
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
uniform_list(cx.entity().clone(), "entries", entry_count, {
|
||||
move |this, range, window, cx| {
|
||||
uniform_list(
|
||||
"entries",
|
||||
entry_count,
|
||||
cx.processor(move |this, range: Range<usize>, window, cx| {
|
||||
let mut items = Vec::with_capacity(range.end - range.start);
|
||||
|
||||
for ix in range {
|
||||
@@ -3691,8 +3742,8 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
items
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
!self.horizontal_scrollbar.show_track
|
||||
&& self.horizontal_scrollbar.show_scrollbar,
|
||||
@@ -4141,6 +4192,32 @@ impl GitPanel {
|
||||
self.amend_pending = value;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub async fn load(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> anyhow::Result<Entity<Self>> {
|
||||
let serialized_panel = cx
|
||||
.background_spawn(async move { KEY_VALUE_STORE.read_kvp(&GIT_PANEL_KEY) })
|
||||
.await
|
||||
.context("loading git panel")
|
||||
.log_err()
|
||||
.flatten()
|
||||
.and_then(|panel| serde_json::from_str::<SerializedGitPanel>(&panel).log_err());
|
||||
|
||||
workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
let panel = GitPanel::new(workspace, window, cx);
|
||||
|
||||
if let Some(serialized_panel) = serialized_panel {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.width = serialized_panel.width;
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
|
||||
panel
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
|
||||
@@ -4852,7 +4929,7 @@ impl Component for PanelRepoFooter {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use git::status::StatusCode;
|
||||
use gpui::TestAppContext;
|
||||
use gpui::{TestAppContext, VisualTestContext};
|
||||
use project::{FakeFs, WorktreeSettings};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
@@ -4916,8 +4993,9 @@ mod tests {
|
||||
|
||||
let project =
|
||||
Project::test(fs.clone(), [path!("/root/zed/crates/gpui").as_ref()], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let workspace =
|
||||
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
cx.read(|cx| {
|
||||
project
|
||||
@@ -4934,10 +5012,7 @@ mod tests {
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let app_state = workspace.read_with(cx, |workspace, _| workspace.app_state().clone());
|
||||
let panel = cx.new_window_entity(|window, cx| {
|
||||
GitPanel::new(workspace.clone(), project.clone(), app_state, window, cx)
|
||||
});
|
||||
let panel = workspace.update(cx, GitPanel::new).unwrap();
|
||||
|
||||
let handle = cx.update_window_entity(&panel, |panel, _, _| {
|
||||
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
|
||||
|
||||
@@ -378,8 +378,6 @@ impl DataTable {
|
||||
|
||||
impl Render for DataTable {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let entity = cx.entity();
|
||||
|
||||
div()
|
||||
.font_family(".SystemUIFont")
|
||||
.bg(gpui::white())
|
||||
@@ -431,8 +429,10 @@ impl Render for DataTable {
|
||||
.relative()
|
||||
.size_full()
|
||||
.child(
|
||||
uniform_list(entity, "items", self.quotes.len(), {
|
||||
move |this, range, _, _| {
|
||||
uniform_list(
|
||||
"items",
|
||||
self.quotes.len(),
|
||||
cx.processor(move |this, range: Range<usize>, _, _| {
|
||||
this.visible_range = range.clone();
|
||||
let mut items = Vec::with_capacity(range.end - range.start);
|
||||
for i in range {
|
||||
@@ -441,8 +441,8 @@ impl Render for DataTable {
|
||||
}
|
||||
}
|
||||
items
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.size_full()
|
||||
.track_scroll(self.scroll_handle.clone()),
|
||||
)
|
||||
|
||||
@@ -9,10 +9,9 @@ impl Render for UniformListExample {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().size_full().bg(rgb(0xffffff)).child(
|
||||
uniform_list(
|
||||
cx.entity().clone(),
|
||||
"entries",
|
||||
50,
|
||||
|_this, range, _window, _cx| {
|
||||
cx.processor(|_this, range, _window, _cx| {
|
||||
let mut items = Vec::new();
|
||||
for ix in range {
|
||||
let item = ix + 1;
|
||||
@@ -29,7 +28,7 @@ impl Render for UniformListExample {
|
||||
);
|
||||
}
|
||||
items
|
||||
},
|
||||
}),
|
||||
)
|
||||
.h_full(),
|
||||
)
|
||||
|
||||
@@ -64,7 +64,7 @@ pub struct AppCell {
|
||||
impl AppCell {
|
||||
#[doc(hidden)]
|
||||
#[track_caller]
|
||||
pub fn borrow(&self) -> AppRef {
|
||||
pub fn borrow(&self) -> AppRef<'_> {
|
||||
if option_env!("TRACK_THREAD_BORROWS").is_some() {
|
||||
let thread_id = std::thread::current().id();
|
||||
eprintln!("borrowed {thread_id:?}");
|
||||
@@ -74,7 +74,7 @@ impl AppCell {
|
||||
|
||||
#[doc(hidden)]
|
||||
#[track_caller]
|
||||
pub fn borrow_mut(&self) -> AppRefMut {
|
||||
pub fn borrow_mut(&self) -> AppRefMut<'_> {
|
||||
if option_env!("TRACK_THREAD_BORROWS").is_some() {
|
||||
let thread_id = std::thread::current().id();
|
||||
eprintln!("borrowed {thread_id:?}");
|
||||
@@ -84,7 +84,7 @@ impl AppCell {
|
||||
|
||||
#[doc(hidden)]
|
||||
#[track_caller]
|
||||
pub fn try_borrow_mut(&self) -> Result<AppRefMut, BorrowMutError> {
|
||||
pub fn try_borrow_mut(&self) -> Result<AppRefMut<'_>, BorrowMutError> {
|
||||
if option_env!("TRACK_THREAD_BORROWS").is_some() {
|
||||
let thread_id = std::thread::current().id();
|
||||
eprintln!("borrowed {thread_id:?}");
|
||||
|
||||
@@ -225,6 +225,18 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience method for producing view state in a closure.
|
||||
/// See `listener` for more details.
|
||||
pub fn processor<E, R>(
|
||||
&self,
|
||||
f: impl Fn(&mut T, E, &mut Window, &mut Context<T>) -> R + 'static,
|
||||
) -> impl Fn(E, &mut Window, &mut App) -> R + 'static {
|
||||
let view = self.entity();
|
||||
move |e: E, window: &mut Window, cx: &mut App| {
|
||||
view.update(cx, |view, cx| f(view, e, window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
/// Run something using this entity and cx, when the returned struct is dropped
|
||||
pub fn on_drop(
|
||||
&self,
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
//! elements with uniform height.
|
||||
|
||||
use crate::{
|
||||
AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, Element, ElementId, Entity,
|
||||
GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement,
|
||||
IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Render, ScrollHandle, Size,
|
||||
StyleRefinement, Styled, Window, point, size,
|
||||
AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId,
|
||||
Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
|
||||
ListSizingBehavior, Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled, Window,
|
||||
point, size,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||
@@ -19,28 +19,23 @@ use super::ListHorizontalSizingBehavior;
|
||||
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
|
||||
/// uniform_list will only render the visible subset of items.
|
||||
#[track_caller]
|
||||
pub fn uniform_list<I, R, V>(
|
||||
view: Entity<V>,
|
||||
id: I,
|
||||
pub fn uniform_list<R>(
|
||||
id: impl Into<ElementId>,
|
||||
item_count: usize,
|
||||
f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
|
||||
f: impl 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<R>,
|
||||
) -> UniformList
|
||||
where
|
||||
I: Into<ElementId>,
|
||||
R: IntoElement,
|
||||
V: Render,
|
||||
{
|
||||
let id = id.into();
|
||||
let mut base_style = StyleRefinement::default();
|
||||
base_style.overflow.y = Some(Overflow::Scroll);
|
||||
|
||||
let render_range = move |range, window: &mut Window, cx: &mut App| {
|
||||
view.update(cx, |this, cx| {
|
||||
f(this, range, window, cx)
|
||||
.into_iter()
|
||||
.map(|component| component.into_any_element())
|
||||
.collect()
|
||||
})
|
||||
let render_range = move |range: Range<usize>, window: &mut Window, cx: &mut App| {
|
||||
f(range, window, cx)
|
||||
.into_iter()
|
||||
.map(|component| component.into_any_element())
|
||||
.collect()
|
||||
};
|
||||
|
||||
UniformList {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user