Compare commits
86 Commits
fix_devcon
...
performanc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4caf96c26 | ||
|
|
ed38711bb4 | ||
|
|
65a65a740a | ||
|
|
4a9ab5dcf1 | ||
|
|
7c44e25ff9 | ||
|
|
395cd249af | ||
|
|
c14efd2e77 | ||
|
|
ef866162b9 | ||
|
|
49e156cb6b | ||
|
|
58fd39ddcb | ||
|
|
624dab2027 | ||
|
|
f9b25f07cb | ||
|
|
8514799277 | ||
|
|
5af3a1554a | ||
|
|
945d0fb48f | ||
|
|
478e2a5c34 | ||
|
|
9b0d618f4a | ||
|
|
4c0abdd245 | ||
|
|
fc85692e39 | ||
|
|
44a64e78d4 | ||
|
|
c9daa565e2 | ||
|
|
e9260f0e99 | ||
|
|
aa27bd27fc | ||
|
|
8f59769c0f | ||
|
|
4b056288df | ||
|
|
28b027f927 | ||
|
|
45c2476a23 | ||
|
|
7ae28d854c | ||
|
|
4118b71010 | ||
|
|
f00fe516ac | ||
|
|
55f5b477b1 | ||
|
|
6c5757c74c | ||
|
|
8c2b507a10 | ||
|
|
278df0f1c2 | ||
|
|
f8bbe37e82 | ||
|
|
298dbd881c | ||
|
|
4ed5fd1ecd | ||
|
|
287eed624b | ||
|
|
d6d433be5e | ||
|
|
fcf46cddc2 | ||
|
|
3f59934489 | ||
|
|
abcffeffa0 | ||
|
|
4d2bf71c06 | ||
|
|
c072236774 | ||
|
|
a47332c91f | ||
|
|
be6ca2f53d | ||
|
|
873fe01158 | ||
|
|
d2b40350a5 | ||
|
|
5d8cfdda7c | ||
|
|
1649cc2655 | ||
|
|
4a82746089 | ||
|
|
9318ca049b | ||
|
|
3651206f66 | ||
|
|
18df6158ee | ||
|
|
6922ab8eab | ||
|
|
6973c81c88 | ||
|
|
3f57872b2d | ||
|
|
dd6a64017d | ||
|
|
de4a97dfef | ||
|
|
865c46130a | ||
|
|
399c10ba66 | ||
|
|
b84bd6dcde | ||
|
|
afff1738a8 | ||
|
|
ecdcdc4bee | ||
|
|
1213cbf0a9 | ||
|
|
f2ac1a1810 | ||
|
|
6a25efc856 | ||
|
|
df4a134b33 | ||
|
|
c0a2eb784b | ||
|
|
d8ebfa948a | ||
|
|
55e2255062 | ||
|
|
91cda35cbd | ||
|
|
e3f610c2f9 | ||
|
|
e6cf656f0f | ||
|
|
c92023cb72 | ||
|
|
ddc18f2588 | ||
|
|
ccad41b741 | ||
|
|
793f6ff5a2 | ||
|
|
2a7a259d2b | ||
|
|
be3c16c721 | ||
|
|
ae4a422652 | ||
|
|
d323039524 | ||
|
|
8a0f8cc744 | ||
|
|
ab290acd20 | ||
|
|
08b740c1c7 | ||
|
|
07e8d81639 |
59
.github/ISSUE_TEMPLATE/01_bug_ai.yml
vendored
59
.github/ISSUE_TEMPLATE/01_bug_ai.yml
vendored
@@ -1,59 +0,0 @@
|
|||||||
name: Bug Report (AI)
|
|
||||||
description: Zed Agent Panel Bugs
|
|
||||||
type: "Bug"
|
|
||||||
labels: ["ai"]
|
|
||||||
title: "AI: <a short description of the AI Related 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.
|
|
||||||
|
|
||||||
**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)
|
|
||||||
- Other Details (MCPs, other settings, etc):
|
|
||||||
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
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: If applicable, attach your `Zed.log` file to this issue.
|
|
||||||
description: |
|
|
||||||
From the command palette, run `zed: open log` to see the last 1000 lines.
|
|
||||||
Or run `zed: reveal log in file manager` to reveal the log file itself.
|
|
||||||
value: |
|
|
||||||
<details><summary>Zed.log</summary>
|
|
||||||
|
|
||||||
<!-- Paste your log inside the code block. -->
|
|
||||||
```log
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
53
.github/ISSUE_TEMPLATE/04_bug_debugger.yml
vendored
53
.github/ISSUE_TEMPLATE/04_bug_debugger.yml
vendored
@@ -1,53 +0,0 @@
|
|||||||
name: Bug Report (Debugger)
|
|
||||||
description: Zed Debugger-Related Bugs
|
|
||||||
type: "Bug"
|
|
||||||
labels: ["debugger"]
|
|
||||||
title: "Debugger: <a short description of the Debugger 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.
|
|
||||||
|
|
||||||
**Expected Behavior**:
|
|
||||||
**Actual 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
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: If applicable, attach your `Zed.log` file to this issue.
|
|
||||||
description: |
|
|
||||||
From the command palette, run `zed: open log` to see the last 1000 lines.
|
|
||||||
Or run `zed: reveal log in file manager` to reveal the log file itself.
|
|
||||||
value: |
|
|
||||||
<details><summary>Zed.log</summary>
|
|
||||||
|
|
||||||
<!-- Paste your log inside the code block. -->
|
|
||||||
```log
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
53
.github/ISSUE_TEMPLATE/06_bug_git.yml
vendored
53
.github/ISSUE_TEMPLATE/06_bug_git.yml
vendored
@@ -1,53 +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.
|
|
||||||
|
|
||||||
**Expected Behavior**:
|
|
||||||
**Actual 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
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: If applicable, attach your `Zed.log` file to this issue.
|
|
||||||
description: |
|
|
||||||
From the command palette, run `zed: open log` to see the last 1000 lines.
|
|
||||||
Or run `zed: reveal log in file manager` to reveal the log file itself.
|
|
||||||
value: |
|
|
||||||
<details><summary>Zed.log</summary>
|
|
||||||
|
|
||||||
<!-- Paste your log inside the code block. -->
|
|
||||||
```log
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
53
.github/ISSUE_TEMPLATE/07_bug_windows.yml
vendored
53
.github/ISSUE_TEMPLATE/07_bug_windows.yml
vendored
@@ -1,53 +0,0 @@
|
|||||||
name: Bug Report (Windows)
|
|
||||||
description: Zed Windows Related Bugs
|
|
||||||
type: "Bug"
|
|
||||||
labels: ["windows"]
|
|
||||||
title: "Windows: <a short description of the Windows 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.
|
|
||||||
|
|
||||||
**Expected Behavior**:
|
|
||||||
**Actual 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
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: If applicable, attach your `Zed.log` file to this issue.
|
|
||||||
description: |
|
|
||||||
From the command palette, run `zed: open log` to see the last 1000 lines.
|
|
||||||
Or run `zed: reveal log in file manager` to reveal the log file itself.
|
|
||||||
value: |
|
|
||||||
<details><summary>Zed.log</summary>
|
|
||||||
|
|
||||||
<!-- Paste your log inside the code block. -->
|
|
||||||
```log
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
70
.github/ISSUE_TEMPLATE/1.bug-report.yml
vendored
Normal file
70
.github/ISSUE_TEMPLATE/1.bug-report.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
name: Report an issue
|
||||||
|
description: Report an issue with Zed.
|
||||||
|
type: Bug
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Feature requests should be opened in [discussions](https://github.com/zed-industries/zed/discussions/new/choose).
|
||||||
|
|
||||||
|
Before opening a new issue, please do a [search](https://github.com/zed-industries/zed/issues) of existing issues and :+1: upvote the existing issue instead. This will help us maintain a proper signal-to-noise ratio.
|
||||||
|
|
||||||
|
If you need help with your own project, you can ask a question in our [Discord Support Forums](https://discord.com/invite/zedindustries).
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Reproduction steps
|
||||||
|
description: A step-by-step description of how to reproduce the issue from a **clean Zed install**. Any code must be sufficient to reproduce (make sure to include context!). Include code as text, not just as a screenshot. **Issues with insufficient detail may be summarily closed**.
|
||||||
|
placeholder: |
|
||||||
|
1. Start Zed
|
||||||
|
2. Click X
|
||||||
|
3. Y will happen
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Current vs. Expected behavior
|
||||||
|
description: |
|
||||||
|
A clear and concise description of what is the current behavior (screenshots, videos), vs. what you expected the behavior to be.
|
||||||
|
|
||||||
|
**Skipping this/failure to provide complete information will result in the issue being closed.**
|
||||||
|
placeholder: "Based on my reproduction steps above, when I click X, I expect this to happen, but instead Y happens."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: If applicable, attach your Zed log file to this issue.
|
||||||
|
description: |
|
||||||
|
Open the command palette in Zed, then type `zed: open log` to see the last 1000 lines. Or type `zed: reveal log in file manager` in the command palette to reveal the log file itself.
|
||||||
|
value: |
|
||||||
|
<details><summary>Zed.log</summary>
|
||||||
|
|
||||||
|
<!-- Paste your log inside the code block. -->
|
||||||
|
```log
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: If applicable, provide details about your model provider
|
||||||
|
placeholder: |
|
||||||
|
- Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc.)
|
||||||
|
- Model Name: (Claude Sonnet 4.5, Gemini 3 Pro, GPT-5)
|
||||||
|
- Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
|
||||||
|
- Other details (ACPs, MCPs, other settings, etc.):
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Zed version and system specs
|
||||||
|
description: |
|
||||||
|
Open the command palette in Zed, then type “zed: copy system specs into clipboard”. **Skipping this/failure to provide complete information will result in the issue being closed**.
|
||||||
|
placeholder: |
|
||||||
|
Zed: v0.215.0 (Zed Nightly bfe141ea79aa4984028934067ba75c48d99136ae)
|
||||||
|
OS: macOS 15.1
|
||||||
|
Memory: 36 GiB
|
||||||
|
Architecture: aarch64
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
75
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
75
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
@@ -1,75 +0,0 @@
|
|||||||
name: Bug Report (Other)
|
|
||||||
description: |
|
|
||||||
Something else is broken in Zed (exclude crashing).
|
|
||||||
type: "Bug"
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Summary
|
|
||||||
description: Provide a one sentence summary and detailed reproduction steps
|
|
||||||
value: |
|
|
||||||
<!-- Begin your issue with a one sentence summary -->
|
|
||||||
SUMMARY_SENTENCE_HERE
|
|
||||||
|
|
||||||
### Description
|
|
||||||
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
|
|
||||||
- Any code must be sufficient to reproduce (include context!)
|
|
||||||
- Include code as text, not just as a screenshot.
|
|
||||||
- Issues with insufficient detail may be summarily closed.
|
|
||||||
-->
|
|
||||||
|
|
||||||
DESCRIPTION_HERE
|
|
||||||
|
|
||||||
Steps to reproduce:
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
4.
|
|
||||||
|
|
||||||
**Expected Behavior**:
|
|
||||||
**Actual Behavior**:
|
|
||||||
|
|
||||||
<!-- Before Submitting, did you:
|
|
||||||
1. Include settings.json, keymap.json, .editorconfig if relevant?
|
|
||||||
2. Check your Zed.log for relevant errors? (please include!)
|
|
||||||
3. Click Preview to ensure everything looks right?
|
|
||||||
4. Hide videos, large images and logs in ``` inside collapsible blocks:
|
|
||||||
|
|
||||||
<details><summary>click to expand</summary>
|
|
||||||
|
|
||||||
```json
|
|
||||||
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
-->
|
|
||||||
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: environment
|
|
||||||
attributes:
|
|
||||||
label: Zed Version and System Specs
|
|
||||||
description: |
|
|
||||||
Open Zed, from the command palette select "zed: copy system specs into clipboard"
|
|
||||||
placeholder: |
|
|
||||||
Output of "zed: copy system specs into clipboard"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: If applicable, attach your `Zed.log` file to this issue.
|
|
||||||
description: |
|
|
||||||
From the command palette, run `zed: open log` to see the last 1000 lines.
|
|
||||||
Or run `zed: reveal log in file manager` to reveal the log file itself.
|
|
||||||
value: |
|
|
||||||
<details><summary>Zed.log</summary>
|
|
||||||
|
|
||||||
<!-- Paste your log inside the code block. -->
|
|
||||||
```log
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
50
.github/ISSUE_TEMPLATE/11_crash_report.yml
vendored
50
.github/ISSUE_TEMPLATE/11_crash_report.yml
vendored
@@ -1,50 +0,0 @@
|
|||||||
name: Crash Report
|
|
||||||
description: Zed is Crashing or Hanging
|
|
||||||
type: "Crash"
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Summary
|
|
||||||
description: Summarize the issue with detailed reproduction steps
|
|
||||||
value: |
|
|
||||||
<!-- Begin your issue with a one sentence summary -->
|
|
||||||
SUMMARY_SENTENCE_HERE
|
|
||||||
|
|
||||||
### Description
|
|
||||||
<!-- Include all steps necessary to reproduce from a clean Zed installation. Be verbose -->
|
|
||||||
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
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: If applicable, attach your `Zed.log` file to this issue.
|
|
||||||
description: |
|
|
||||||
From the command palette, run `zed: open log` to see the last 1000 lines.
|
|
||||||
Or run `zed: reveal log in file manager` to reveal the log file itself.
|
|
||||||
value: |
|
|
||||||
<details><summary>Zed.log</summary>
|
|
||||||
|
|
||||||
<!-- Paste your log inside the code block. -->
|
|
||||||
```log
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
52
.github/ISSUE_TEMPLATE/2.crash-report.yml
vendored
Normal file
52
.github/ISSUE_TEMPLATE/2.crash-report.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
name: Report a crash
|
||||||
|
description: Zed is crashing or freezing or hanging.
|
||||||
|
type: Crash
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Reproduction steps
|
||||||
|
description: A step-by-step description of how to reproduce the crash from a **clean Zed install**. **Be verbose**. **Issues with insufficient detail may be summarily closed**.
|
||||||
|
placeholder: |
|
||||||
|
1. Start Zed
|
||||||
|
2. Perform an action
|
||||||
|
3. Zed crashes
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Current vs. Expected behavior
|
||||||
|
description: |
|
||||||
|
Go into depth about what actions you’re performing in Zed to trigger the crash. If Zed crashes before it loads any windows, make sure to mention that. Again, **be verbose**.
|
||||||
|
|
||||||
|
**Skipping this/failure to provide complete information will result in the issue being closed.**
|
||||||
|
placeholder: "Based on my reproduction steps above, when I perform said action, I expect this to happen, but instead Zed crashes."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Zed version and system specs
|
||||||
|
description: |
|
||||||
|
Open the command palette in Zed, then type “zed: copy system specs into clipboard”. **Skipping this/failure to provide complete information will result in the issue being closed**.
|
||||||
|
placeholder: |
|
||||||
|
Zed: v0.215.0 (Zed Nightly bfe141ea79aa4984028934067ba75c48d99136ae)
|
||||||
|
OS: macOS 15.1
|
||||||
|
Memory: 36 GiB
|
||||||
|
Architecture: aarch64
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: If applicable, attach your Zed log file to this issue
|
||||||
|
description: |
|
||||||
|
Open the command palette in Zed, then type `zed: open log` to see the last 1000 lines. Or type `zed: reveal log in file manager` in the command palette to reveal the log file itself.
|
||||||
|
value: |
|
||||||
|
<details><summary>Zed.log</summary>
|
||||||
|
|
||||||
|
<!-- Paste your log inside the code block. -->
|
||||||
|
```log
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
10
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,9 +1,9 @@
|
|||||||
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-config.json
|
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-config.json
|
||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Feature Request
|
- name: Feature request
|
||||||
url: https://github.com/zed-industries/zed/discussions/new/choose
|
url: https://github.com/zed-industries/zed/discussions/new/choose
|
||||||
about: To request a feature, open a new Discussion in one of the appropriate Discussion categories
|
about: To request a feature, open a new discussion under one of the appropriate categories.
|
||||||
- name: "Zed Discord"
|
- name: Our Discord community
|
||||||
url: https://zed.dev/community-links
|
url: https://discord.com/invite/zedindustries
|
||||||
about: Real-time discussion and user support
|
about: Join our Discord server for real-time discussion and user support.
|
||||||
|
|||||||
@@ -13,13 +13,65 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check if author is a community champion and apply label
|
- name: Check if author is a community champion and apply label
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
|
env:
|
||||||
|
COMMUNITY_CHAMPIONS: |
|
||||||
|
0x2CA
|
||||||
|
5brian
|
||||||
|
5herlocked
|
||||||
|
abdelq
|
||||||
|
afgomez
|
||||||
|
AidanV
|
||||||
|
akbxr
|
||||||
|
AlvaroParker
|
||||||
|
artemevsevev
|
||||||
|
bajrangCoder
|
||||||
|
bcomnes
|
||||||
|
Be-ing
|
||||||
|
blopker
|
||||||
|
bobbymannino
|
||||||
|
CharlesChen0823
|
||||||
|
chbk
|
||||||
|
cppcoffee
|
||||||
|
davewa
|
||||||
|
ddoemonn
|
||||||
|
djsauble
|
||||||
|
fantacell
|
||||||
|
findrakecil
|
||||||
|
gko
|
||||||
|
huacnlee
|
||||||
|
imumesh18
|
||||||
|
jacobtread
|
||||||
|
jansol
|
||||||
|
jeffreyguenther
|
||||||
|
jenslys
|
||||||
|
jongretar
|
||||||
|
lemorage
|
||||||
|
lnay
|
||||||
|
marcocondrache
|
||||||
|
marius851000
|
||||||
|
mikebronner
|
||||||
|
ognevny
|
||||||
|
RemcoSmitsDev
|
||||||
|
romaninsh
|
||||||
|
Simek
|
||||||
|
someone13574
|
||||||
|
sourcefrog
|
||||||
|
suxiaoshao
|
||||||
|
Takk8IS
|
||||||
|
tidely
|
||||||
|
timvermeulen
|
||||||
|
valentinegb
|
||||||
|
versecafe
|
||||||
|
vitallium
|
||||||
|
warrenjokinen
|
||||||
|
ya7010
|
||||||
|
Zertsov
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const communityChampionBody = `${{ secrets.COMMUNITY_CHAMPIONS }}`;
|
const communityChampions = process.env.COMMUNITY_CHAMPIONS
|
||||||
|
|
||||||
const communityChampions = communityChampionBody
|
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map(handle => handle.trim().toLowerCase());
|
.map(handle => handle.trim().toLowerCase())
|
||||||
|
.filter(handle => handle.length > 0);
|
||||||
|
|
||||||
let author;
|
let author;
|
||||||
if (context.eventName === 'issues') {
|
if (context.eventName === 'issues') {
|
||||||
|
|||||||
2
.github/workflows/extension_tests.yml
vendored
2
.github/workflows/extension_tests.yml
vendored
@@ -77,7 +77,7 @@ jobs:
|
|||||||
uses: taiki-e/install-action@nextest
|
uses: taiki-e/install-action@nextest
|
||||||
- name: steps::cargo_nextest
|
- name: steps::cargo_nextest
|
||||||
if: inputs.run_tests
|
if: inputs.run_tests
|
||||||
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
run: cargo nextest run --workspace --no-fail-fast
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
timeout-minutes: 3
|
timeout-minutes: 3
|
||||||
check_extension:
|
check_extension:
|
||||||
|
|||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
|||||||
run: ./script/clear-target-dir-if-larger-than 300
|
run: ./script/clear-target-dir-if-larger-than 300
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cargo_nextest
|
- name: steps::cargo_nextest
|
||||||
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
run: cargo nextest run --workspace --no-fail-fast
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cleanup_cargo_config
|
- name: steps::cleanup_cargo_config
|
||||||
if: always()
|
if: always()
|
||||||
@@ -80,7 +80,7 @@ jobs:
|
|||||||
run: ./script/clear-target-dir-if-larger-than 250
|
run: ./script/clear-target-dir-if-larger-than 250
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cargo_nextest
|
- name: steps::cargo_nextest
|
||||||
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
run: cargo nextest run --workspace --no-fail-fast
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cleanup_cargo_config
|
- name: steps::cleanup_cargo_config
|
||||||
if: always()
|
if: always()
|
||||||
@@ -112,7 +112,7 @@ jobs:
|
|||||||
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: steps::cargo_nextest
|
- name: steps::cargo_nextest
|
||||||
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
run: cargo nextest run --workspace --no-fail-fast
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: steps::cleanup_cargo_config
|
- name: steps::cleanup_cargo_config
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
2
.github/workflows/release_nightly.yml
vendored
2
.github/workflows/release_nightly.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
|||||||
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: steps::cargo_nextest
|
- name: steps::cargo_nextest
|
||||||
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
run: cargo nextest run --workspace --no-fail-fast
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: steps::cleanup_cargo_config
|
- name: steps::cleanup_cargo_config
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
6
.github/workflows/run_tests.yml
vendored
6
.github/workflows/run_tests.yml
vendored
@@ -117,7 +117,7 @@ jobs:
|
|||||||
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: steps::cargo_nextest
|
- name: steps::cargo_nextest
|
||||||
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
run: cargo nextest run --workspace --no-fail-fast
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: steps::cleanup_cargo_config
|
- name: steps::cleanup_cargo_config
|
||||||
if: always()
|
if: always()
|
||||||
@@ -166,7 +166,7 @@ jobs:
|
|||||||
run: ./script/clear-target-dir-if-larger-than 250
|
run: ./script/clear-target-dir-if-larger-than 250
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cargo_nextest
|
- name: steps::cargo_nextest
|
||||||
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
run: cargo nextest run --workspace --no-fail-fast
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cleanup_cargo_config
|
- name: steps::cleanup_cargo_config
|
||||||
if: always()
|
if: always()
|
||||||
@@ -200,7 +200,7 @@ jobs:
|
|||||||
run: ./script/clear-target-dir-if-larger-than 300
|
run: ./script/clear-target-dir-if-larger-than 300
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cargo_nextest
|
- name: steps::cargo_nextest
|
||||||
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
run: cargo nextest run --workspace --no-fail-fast
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cleanup_cargo_config
|
- name: steps::cleanup_cargo_config
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
401
Cargo.lock
generated
401
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
13
Cargo.toml
@@ -147,7 +147,6 @@ members = [
|
|||||||
"crates/rules_library",
|
"crates/rules_library",
|
||||||
"crates/schema_generator",
|
"crates/schema_generator",
|
||||||
"crates/search",
|
"crates/search",
|
||||||
"crates/semantic_version",
|
|
||||||
"crates/session",
|
"crates/session",
|
||||||
"crates/settings",
|
"crates/settings",
|
||||||
"crates/settings_json",
|
"crates/settings_json",
|
||||||
@@ -202,7 +201,6 @@ members = [
|
|||||||
"crates/zed_actions",
|
"crates/zed_actions",
|
||||||
"crates/zed_env_vars",
|
"crates/zed_env_vars",
|
||||||
"crates/zeta",
|
"crates/zeta",
|
||||||
"crates/zeta2",
|
|
||||||
"crates/zeta_cli",
|
"crates/zeta_cli",
|
||||||
"crates/zlog",
|
"crates/zlog",
|
||||||
"crates/zlog_settings",
|
"crates/zlog_settings",
|
||||||
@@ -381,7 +379,6 @@ rope = { path = "crates/rope" }
|
|||||||
rpc = { path = "crates/rpc" }
|
rpc = { path = "crates/rpc" }
|
||||||
rules_library = { path = "crates/rules_library" }
|
rules_library = { path = "crates/rules_library" }
|
||||||
search = { path = "crates/search" }
|
search = { path = "crates/search" }
|
||||||
semantic_version = { path = "crates/semantic_version" }
|
|
||||||
session = { path = "crates/session" }
|
session = { path = "crates/session" }
|
||||||
settings = { path = "crates/settings" }
|
settings = { path = "crates/settings" }
|
||||||
settings_json = { path = "crates/settings_json" }
|
settings_json = { path = "crates/settings_json" }
|
||||||
@@ -435,7 +432,6 @@ zed = { path = "crates/zed" }
|
|||||||
zed_actions = { path = "crates/zed_actions" }
|
zed_actions = { path = "crates/zed_actions" }
|
||||||
zed_env_vars = { path = "crates/zed_env_vars" }
|
zed_env_vars = { path = "crates/zed_env_vars" }
|
||||||
zeta = { path = "crates/zeta" }
|
zeta = { path = "crates/zeta" }
|
||||||
zeta2 = { path = "crates/zeta2" }
|
|
||||||
zlog = { path = "crates/zlog" }
|
zlog = { path = "crates/zlog" }
|
||||||
zlog_settings = { path = "crates/zlog_settings" }
|
zlog_settings = { path = "crates/zlog_settings" }
|
||||||
|
|
||||||
@@ -607,7 +603,6 @@ pulldown-cmark = { version = "0.12.0", default-features = false }
|
|||||||
quote = "1.0.9"
|
quote = "1.0.9"
|
||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
rayon = "1.8"
|
rayon = "1.8"
|
||||||
ref-cast = "1.0.24"
|
|
||||||
regex = "1.5"
|
regex = "1.5"
|
||||||
# WARNING: If you change this, you must also publish a new version of zed-reqwest to crates.io
|
# WARNING: If you change this, you must also publish a new version of zed-reqwest to crates.io
|
||||||
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [
|
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [
|
||||||
@@ -630,7 +625,7 @@ rustls-platform-verifier = "0.5.0"
|
|||||||
# WARNING: If you change this, you must also publish a new version of zed-scap to crates.io
|
# WARNING: If you change this, you must also publish a new version of zed-scap to crates.io
|
||||||
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
|
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
|
||||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||||
semver = "1.0"
|
semver = { version = "1.0", features = ["serde"] }
|
||||||
serde = { version = "1.0.221", features = ["derive", "rc"] }
|
serde = { version = "1.0.221", features = ["derive", "rc"] }
|
||||||
serde_derive = "1.0.221"
|
serde_derive = "1.0.221"
|
||||||
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
|
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
|
||||||
@@ -641,7 +636,6 @@ serde_json_lenient = { version = "0.2", features = [
|
|||||||
serde_path_to_error = "0.1.17"
|
serde_path_to_error = "0.1.17"
|
||||||
serde_repr = "0.1"
|
serde_repr = "0.1"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
serde_with = "3.4.0"
|
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
shellexpand = "2.1.0"
|
shellexpand = "2.1.0"
|
||||||
shlex = "1.3.0"
|
shlex = "1.3.0"
|
||||||
@@ -847,7 +841,6 @@ refineable = { codegen-units = 1 }
|
|||||||
release_channel = { codegen-units = 1 }
|
release_channel = { codegen-units = 1 }
|
||||||
reqwest_client = { codegen-units = 1 }
|
reqwest_client = { codegen-units = 1 }
|
||||||
rich_text = { codegen-units = 1 }
|
rich_text = { codegen-units = 1 }
|
||||||
semantic_version = { codegen-units = 1 }
|
|
||||||
session = { codegen-units = 1 }
|
session = { codegen-units = 1 }
|
||||||
snippet = { codegen-units = 1 }
|
snippet = { codegen-units = 1 }
|
||||||
snippets_ui = { codegen-units = 1 }
|
snippets_ui = { codegen-units = 1 }
|
||||||
@@ -873,6 +866,10 @@ debug = "full"
|
|||||||
lto = false
|
lto = false
|
||||||
codegen-units = 16
|
codegen-units = 16
|
||||||
|
|
||||||
|
[profile.profiling]
|
||||||
|
inherits = "release"
|
||||||
|
debug = "full"
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
unexpected_cfgs = { level = "allow" }
|
unexpected_cfgs = { level = "allow" }
|
||||||
|
|
||||||
|
|||||||
@@ -239,13 +239,11 @@
|
|||||||
"ctrl-alt-l": "agent::OpenRulesLibrary",
|
"ctrl-alt-l": "agent::OpenRulesLibrary",
|
||||||
"ctrl-i": "agent::ToggleProfileSelector",
|
"ctrl-i": "agent::ToggleProfileSelector",
|
||||||
"ctrl-alt-/": "agent::ToggleModelSelector",
|
"ctrl-alt-/": "agent::ToggleModelSelector",
|
||||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
|
||||||
"ctrl-shift-j": "agent::ToggleNavigationMenu",
|
"ctrl-shift-j": "agent::ToggleNavigationMenu",
|
||||||
"ctrl-alt-i": "agent::ToggleOptionsMenu",
|
"ctrl-alt-i": "agent::ToggleOptionsMenu",
|
||||||
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
|
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
|
||||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||||
"ctrl->": "agent::AddSelectionToThread",
|
"ctrl->": "agent::AddSelectionToThread",
|
||||||
"ctrl-alt-e": "agent::RemoveAllContext",
|
|
||||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||||
"ctrl-shift-enter": "agent::ContinueThread",
|
"ctrl-shift-enter": "agent::ContinueThread",
|
||||||
"super-ctrl-b": "agent::ToggleBurnMode",
|
"super-ctrl-b": "agent::ToggleBurnMode",
|
||||||
@@ -322,17 +320,6 @@
|
|||||||
"alt-enter": "editor::Newline"
|
"alt-enter": "editor::Newline"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "ContextStrip",
|
|
||||||
"bindings": {
|
|
||||||
"up": "agent::FocusUp",
|
|
||||||
"right": "agent::FocusRight",
|
|
||||||
"left": "agent::FocusLeft",
|
|
||||||
"down": "agent::FocusDown",
|
|
||||||
"backspace": "agent::RemoveFocusedContext",
|
|
||||||
"enter": "agent::AcceptSuggestedContext"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"context": "AcpThread > ModeSelector",
|
"context": "AcpThread > ModeSelector",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
@@ -824,8 +811,7 @@
|
|||||||
"context": "PromptEditor",
|
"context": "PromptEditor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||||
"ctrl-]": "agent::CycleNextInlineAssist",
|
"ctrl-]": "agent::CycleNextInlineAssist"
|
||||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1251,11 +1237,25 @@
|
|||||||
"context": "Onboarding",
|
"context": "Onboarding",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
"ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }],
|
||||||
|
"ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
|
||||||
|
"ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
|
||||||
|
"ctrl-0": ["zed::ResetUiFontSize", { "persist": false }],
|
||||||
"ctrl-enter": "onboarding::Finish",
|
"ctrl-enter": "onboarding::Finish",
|
||||||
"alt-shift-l": "onboarding::SignIn",
|
"alt-shift-l": "onboarding::SignIn",
|
||||||
"alt-shift-a": "onboarding::OpenAccount"
|
"alt-shift-a": "onboarding::OpenAccount"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "Welcome",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }],
|
||||||
|
"ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
|
||||||
|
"ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
|
||||||
|
"ctrl-0": ["zed::ResetUiFontSize", { "persist": false }]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "InvalidBuffer",
|
"context": "InvalidBuffer",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
|||||||
@@ -278,13 +278,11 @@
|
|||||||
"cmd-alt-p": "agent::ManageProfiles",
|
"cmd-alt-p": "agent::ManageProfiles",
|
||||||
"cmd-i": "agent::ToggleProfileSelector",
|
"cmd-i": "agent::ToggleProfileSelector",
|
||||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||||
"cmd-shift-a": "agent::ToggleContextPicker",
|
|
||||||
"cmd-shift-j": "agent::ToggleNavigationMenu",
|
"cmd-shift-j": "agent::ToggleNavigationMenu",
|
||||||
"cmd-alt-m": "agent::ToggleOptionsMenu",
|
"cmd-alt-m": "agent::ToggleOptionsMenu",
|
||||||
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
|
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
|
||||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||||
"cmd->": "agent::AddSelectionToThread",
|
"cmd->": "agent::AddSelectionToThread",
|
||||||
"cmd-alt-e": "agent::RemoveAllContext",
|
|
||||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||||
"cmd-ctrl-b": "agent::ToggleBurnMode",
|
"cmd-ctrl-b": "agent::ToggleBurnMode",
|
||||||
"cmd-shift-enter": "agent::ContinueThread",
|
"cmd-shift-enter": "agent::ContinueThread",
|
||||||
@@ -365,18 +363,6 @@
|
|||||||
"alt-enter": "editor::Newline"
|
"alt-enter": "editor::Newline"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "ContextStrip",
|
|
||||||
"use_key_equivalents": true,
|
|
||||||
"bindings": {
|
|
||||||
"up": "agent::FocusUp",
|
|
||||||
"right": "agent::FocusRight",
|
|
||||||
"left": "agent::FocusLeft",
|
|
||||||
"down": "agent::FocusDown",
|
|
||||||
"backspace": "agent::RemoveFocusedContext",
|
|
||||||
"enter": "agent::AcceptSuggestedContext"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"context": "AgentConfiguration",
|
"context": "AgentConfiguration",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
@@ -889,9 +875,7 @@
|
|||||||
"context": "PromptEditor",
|
"context": "PromptEditor",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-shift-a": "agent::ToggleContextPicker",
|
|
||||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||||
"cmd-alt-e": "agent::RemoveAllContext",
|
|
||||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||||
"ctrl-]": "agent::CycleNextInlineAssist"
|
"ctrl-]": "agent::CycleNextInlineAssist"
|
||||||
}
|
}
|
||||||
@@ -1234,23 +1218,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "RateCompletionModal",
|
"context": "RatePredictionsModal",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
|
"cmd-shift-enter": "zeta::ThumbsUpActivePrediction",
|
||||||
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion",
|
"cmd-shift-backspace": "zeta::ThumbsDownActivePrediction",
|
||||||
"shift-down": "zeta::NextEdit",
|
"shift-down": "zeta::NextEdit",
|
||||||
"shift-up": "zeta::PreviousEdit",
|
"shift-up": "zeta::PreviousEdit",
|
||||||
"right": "zeta::PreviewCompletion"
|
"right": "zeta::PreviewPrediction"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "RateCompletionModal > Editor",
|
"context": "RatePredictionsModal > Editor",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"escape": "zeta::FocusCompletions",
|
"escape": "zeta::FocusPredictions",
|
||||||
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
|
"cmd-shift-enter": "zeta::ThumbsUpActivePrediction",
|
||||||
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
|
"cmd-shift-backspace": "zeta::ThumbsDownActivePrediction"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1356,11 +1340,25 @@
|
|||||||
"context": "Onboarding",
|
"context": "Onboarding",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
"cmd-=": ["zed::IncreaseUiFontSize", { "persist": false }],
|
||||||
|
"cmd-+": ["zed::IncreaseUiFontSize", { "persist": false }],
|
||||||
|
"cmd--": ["zed::DecreaseUiFontSize", { "persist": false }],
|
||||||
|
"cmd-0": ["zed::ResetUiFontSize", { "persist": false }],
|
||||||
"cmd-enter": "onboarding::Finish",
|
"cmd-enter": "onboarding::Finish",
|
||||||
"alt-tab": "onboarding::SignIn",
|
"alt-tab": "onboarding::SignIn",
|
||||||
"alt-shift-a": "onboarding::OpenAccount"
|
"alt-shift-a": "onboarding::OpenAccount"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "Welcome",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"cmd-=": ["zed::IncreaseUiFontSize", { "persist": false }],
|
||||||
|
"cmd-+": ["zed::IncreaseUiFontSize", { "persist": false }],
|
||||||
|
"cmd--": ["zed::DecreaseUiFontSize", { "persist": false }],
|
||||||
|
"cmd-0": ["zed::ResetUiFontSize", { "persist": false }]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "InvalidBuffer",
|
"context": "InvalidBuffer",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
|||||||
@@ -240,13 +240,11 @@
|
|||||||
"shift-alt-p": "agent::ManageProfiles",
|
"shift-alt-p": "agent::ManageProfiles",
|
||||||
"ctrl-i": "agent::ToggleProfileSelector",
|
"ctrl-i": "agent::ToggleProfileSelector",
|
||||||
"shift-alt-/": "agent::ToggleModelSelector",
|
"shift-alt-/": "agent::ToggleModelSelector",
|
||||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
|
||||||
"ctrl-shift-j": "agent::ToggleNavigationMenu",
|
"ctrl-shift-j": "agent::ToggleNavigationMenu",
|
||||||
"ctrl-alt-i": "agent::ToggleOptionsMenu",
|
"ctrl-alt-i": "agent::ToggleOptionsMenu",
|
||||||
// "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
|
// "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
|
||||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||||
"ctrl-shift-.": "agent::AddSelectionToThread",
|
"ctrl-shift-.": "agent::AddSelectionToThread",
|
||||||
"shift-alt-e": "agent::RemoveAllContext",
|
|
||||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||||
"ctrl-shift-enter": "agent::ContinueThread",
|
"ctrl-shift-enter": "agent::ContinueThread",
|
||||||
"super-ctrl-b": "agent::ToggleBurnMode",
|
"super-ctrl-b": "agent::ToggleBurnMode",
|
||||||
@@ -328,18 +326,6 @@
|
|||||||
"alt-enter": "editor::Newline"
|
"alt-enter": "editor::Newline"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "ContextStrip",
|
|
||||||
"use_key_equivalents": true,
|
|
||||||
"bindings": {
|
|
||||||
"up": "agent::FocusUp",
|
|
||||||
"right": "agent::FocusRight",
|
|
||||||
"left": "agent::FocusLeft",
|
|
||||||
"down": "agent::FocusDown",
|
|
||||||
"backspace": "agent::RemoveFocusedContext",
|
|
||||||
"enter": "agent::AcceptSuggestedContext"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"context": "AcpThread > ModeSelector",
|
"context": "AcpThread > ModeSelector",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
@@ -837,8 +823,7 @@
|
|||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||||
"ctrl-]": "agent::CycleNextInlineAssist",
|
"ctrl-]": "agent::CycleNextInlineAssist"
|
||||||
"shift-alt-e": "agent::RemoveAllContext"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1285,11 +1270,25 @@
|
|||||||
"context": "Onboarding",
|
"context": "Onboarding",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
"ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }],
|
||||||
|
"ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
|
||||||
|
"ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
|
||||||
|
"ctrl-0": ["zed::ResetUiFontSize", { "persist": false }],
|
||||||
"ctrl-enter": "onboarding::Finish",
|
"ctrl-enter": "onboarding::Finish",
|
||||||
"alt-shift-l": "onboarding::SignIn",
|
"alt-shift-l": "onboarding::SignIn",
|
||||||
"shift-alt-a": "onboarding::OpenAccount"
|
"shift-alt-a": "onboarding::OpenAccount"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "Welcome",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }],
|
||||||
|
"ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
|
||||||
|
"ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
|
||||||
|
"ctrl-0": ["zed::ResetUiFontSize", { "persist": false }]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)",
|
"context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
|||||||
@@ -414,8 +414,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_mode == helix_normal && !menu",
|
"context": "VimControl && vim_mode == helix_normal && !menu",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
"escape": "vim::SwitchToHelixNormalMode",
|
||||||
"i": "vim::HelixInsert",
|
"i": "vim::HelixInsert",
|
||||||
"a": "vim::HelixAppend",
|
"a": "vim::HelixAppend",
|
||||||
"ctrl-[": "editor::Cancel"
|
"ctrl-[": "editor::Cancel"
|
||||||
|
|||||||
@@ -175,6 +175,16 @@
|
|||||||
//
|
//
|
||||||
// Default: true
|
// Default: true
|
||||||
"zoomed_padding": true,
|
"zoomed_padding": true,
|
||||||
|
// What draws Zed's window decorations (titlebar):
|
||||||
|
// 1. Client application (Zed) draws its own window decorations
|
||||||
|
// "client"
|
||||||
|
// 2. Display server draws the window decorations. Not supported by GNOME Wayland.
|
||||||
|
// "server"
|
||||||
|
//
|
||||||
|
// This requires restarting Zed for changes to take effect.
|
||||||
|
//
|
||||||
|
// Default: "client"
|
||||||
|
"window_decorations": "client",
|
||||||
// Whether to use the system provided dialogs for Open and Save As.
|
// Whether to use the system provided dialogs for Open and Save As.
|
||||||
// When set to false, Zed will use the built-in keyboard-first pickers.
|
// When set to false, Zed will use the built-in keyboard-first pickers.
|
||||||
"use_system_path_prompts": true,
|
"use_system_path_prompts": true,
|
||||||
@@ -1431,7 +1441,7 @@
|
|||||||
"default_height": 320,
|
"default_height": 320,
|
||||||
// What working directory to use when launching the terminal.
|
// What working directory to use when launching the terminal.
|
||||||
// May take 4 values:
|
// May take 4 values:
|
||||||
// 1. Use the current file's project directory. Will Fallback to the
|
// 1. Use the current file's project directory. Fallback to the
|
||||||
// first project directory strategy if unsuccessful
|
// first project directory strategy if unsuccessful
|
||||||
// "working_directory": "current_project_directory"
|
// "working_directory": "current_project_directory"
|
||||||
// 2. Use the first project in this workspace's directory
|
// 2. Use the first project in this workspace's directory
|
||||||
@@ -1585,7 +1595,59 @@
|
|||||||
//
|
//
|
||||||
// Most terminal themes have APCA values of 40-70.
|
// Most terminal themes have APCA values of 40-70.
|
||||||
// A value of 45 preserves colorful themes while ensuring legibility.
|
// A value of 45 preserves colorful themes while ensuring legibility.
|
||||||
"minimum_contrast": 45
|
"minimum_contrast": 45,
|
||||||
|
// Regexes used to identify paths for hyperlink navigation. Supports optional named capture
|
||||||
|
// groups `path`, `line`, `column`, and `link`. If none of these are present, the entire match
|
||||||
|
// is the hyperlink target. If `path` is present, it is the hyperlink target, along with `line`
|
||||||
|
// and `column` if present. `link` may be used to customize what text in terminal is part of the
|
||||||
|
// hyperlink. If `link` is not present, the text of the entire match is used. If `line` and
|
||||||
|
// `column` are not present, the default built-in line and column suffix processing is used
|
||||||
|
// which parses `line:column` and `(line,column)` variants. The default value handles Python
|
||||||
|
// diagnostics and common path, line, column syntaxes. This can be extended or replaced to
|
||||||
|
// handle specific scenarios. For example, to enable support for hyperlinking paths which
|
||||||
|
// contain spaces in rust output,
|
||||||
|
//
|
||||||
|
// [
|
||||||
|
// "\\s+(-->|:::|at) (?<link>(?<path>.+?))(:$|$)",
|
||||||
|
// "\\s+(Compiling|Checking|Documenting) [^(]+\\((?<link>(?<path>.+))\\)"
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// could be used. Processing stops at the first regex with a match, even if no link is
|
||||||
|
// produced which is the case when the cursor is not over the hyperlinked text. For best
|
||||||
|
// performance it is recommended to order regexes from most common to least common. For
|
||||||
|
// readability and documentation, each regex may be an array of strings which are collected
|
||||||
|
// into one multi-line regex string for use in terminal path hyperlink detection.
|
||||||
|
"path_hyperlink_regexes": [
|
||||||
|
// Python-style diagnostics
|
||||||
|
"File \"(?<path>[^\"]+)\", line (?<line>[0-9]+)",
|
||||||
|
// Common path syntax with optional line, column, description, trailing punctuation, or
|
||||||
|
// surrounding symbols or quotes
|
||||||
|
[
|
||||||
|
"(?x)",
|
||||||
|
"# optionally starts with 0-2 opening prefix symbols",
|
||||||
|
"[({\\[<]{0,2}",
|
||||||
|
"# which may be followed by an opening quote",
|
||||||
|
"(?<quote>[\"'`])?",
|
||||||
|
"# `path` is the shortest sequence of any non-space character",
|
||||||
|
"(?<link>(?<path>[^ ]+?",
|
||||||
|
" # which may end with a line and optionally a column,",
|
||||||
|
" (?<line_column>:+[0-9]+(:[0-9]+)?|:?\\([0-9]+([,:][0-9]+)?\\))?",
|
||||||
|
"))",
|
||||||
|
"# which must be followed by a matching quote",
|
||||||
|
"(?(<quote>)\\k<quote>)",
|
||||||
|
"# and optionally a single closing symbol",
|
||||||
|
"[)}\\]>]?",
|
||||||
|
"# if line/column matched, may be followed by a description",
|
||||||
|
"(?(<line_column>):[^ 0-9][^ ]*)?",
|
||||||
|
"# which may be followed by trailing punctuation",
|
||||||
|
"[.,:)}\\]>]*",
|
||||||
|
"# and always includes trailing whitespace or end of line",
|
||||||
|
"([ ]+|$)"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
// Timeout for hover and Cmd-click path hyperlink discovery in milliseconds. Specifying a
|
||||||
|
// timeout of `0` will disable path hyperlinking in terminal.
|
||||||
|
"path_hyperlink_timeout_ms": 1
|
||||||
},
|
},
|
||||||
"code_actions_on_format": {},
|
"code_actions_on_format": {},
|
||||||
// Settings related to running tasks.
|
// Settings related to running tasks.
|
||||||
@@ -1827,7 +1889,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"PHP": {
|
"PHP": {
|
||||||
"language_servers": ["phpactor", "!intelephense", "..."],
|
"language_servers": ["phpactor", "!intelephense", "!phptools", "..."],
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true,
|
"allowed": true,
|
||||||
"plugins": ["@prettier/plugin-php"],
|
"plugins": ["@prettier/plugin-php"],
|
||||||
@@ -2076,7 +2138,7 @@
|
|||||||
"windows": {
|
"windows": {
|
||||||
"languages": {
|
"languages": {
|
||||||
"PHP": {
|
"PHP": {
|
||||||
"language_servers": ["intelephense", "!phpactor", "..."]
|
"language_servers": ["intelephense", "!phpactor", "!phptools", "..."]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -197,6 +197,11 @@ pub trait AgentModelSelector: 'static {
|
|||||||
fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
|
fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether the model picker should render a footer.
|
||||||
|
fn should_render_footer(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ gpui.workspace = true
|
|||||||
language.workspace = true
|
language.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
proto.workspace = true
|
proto.workspace = true
|
||||||
|
semver.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
|
|||||||
@@ -925,15 +925,15 @@ impl StatusItemView for ActivityIndicator {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use gpui::SemanticVersion;
|
|
||||||
use release_channel::AppCommitSha;
|
use release_channel::AppCommitSha;
|
||||||
|
use semver::Version;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_version_tooltip_message() {
|
fn test_version_tooltip_message() {
|
||||||
let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Semantic(
|
let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Semantic(
|
||||||
SemanticVersion::new(1, 0, 0),
|
Version::new(1, 0, 0),
|
||||||
));
|
));
|
||||||
|
|
||||||
assert_eq!(message, "Version: 1.0.0");
|
assert_eq!(message, "Version: 1.0.0");
|
||||||
|
|||||||
@@ -961,6 +961,10 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
|
|||||||
fn watch(&self, cx: &mut App) -> Option<watch::Receiver<()>> {
|
fn watch(&self, cx: &mut App) -> Option<watch::Receiver<()>> {
|
||||||
Some(self.connection.0.read(cx).models.watch())
|
Some(self.connection.0.read(cx).models.watch())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_render_footer(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl acp_thread::AgentConnection for NativeAgentConnection {
|
impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ impl DbThread {
|
|||||||
crate::Message::Agent(AgentMessage {
|
crate::Message::Agent(AgentMessage {
|
||||||
content,
|
content,
|
||||||
tool_results,
|
tool_results,
|
||||||
|
reasoning_details: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
language_model::Role::System => {
|
language_model::Role::System => {
|
||||||
|
|||||||
@@ -703,6 +703,7 @@ impl EditAgent {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![MessageContent::Text(prompt)],
|
content: vec![MessageContent::Text(prompt)],
|
||||||
cache: false,
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Include tools in the request so that we can take advantage of
|
// Include tools in the request so that we can take advantage of
|
||||||
|
|||||||
@@ -1081,6 +1081,7 @@ fn message(
|
|||||||
role,
|
role,
|
||||||
content: contents.into_iter().collect(),
|
content: contents.into_iter().collect(),
|
||||||
cache: false,
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1268,6 +1269,7 @@ impl EvalAssertion {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![prompt.into()],
|
content: vec![prompt.into()],
|
||||||
cache: false,
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
}],
|
}],
|
||||||
thinking_allowed: true,
|
thinking_allowed: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -1594,6 +1596,7 @@ impl EditAgentTest {
|
|||||||
role: Role::System,
|
role: Role::System,
|
||||||
content: vec![MessageContent::Text(system_prompt)],
|
content: vec![MessageContent::Text(system_prompt)],
|
||||||
cache: true,
|
cache: true,
|
||||||
|
reasoning_details: None,
|
||||||
}]
|
}]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(eval.conversation)
|
.chain(eval.conversation)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ pub async fn get_buffer_content_or_outline(
|
|||||||
if outline_items.is_empty() {
|
if outline_items.is_empty() {
|
||||||
let text = buffer.read_with(cx, |buffer, _| {
|
let text = buffer.read_with(cx, |buffer, _| {
|
||||||
let snapshot = buffer.snapshot();
|
let snapshot = buffer.snapshot();
|
||||||
let len = snapshot.len().min(1024);
|
let len = snapshot.len().min(snapshot.as_rope().floor_char_boundary(1024));
|
||||||
let content = snapshot.text_for_range(0..len).collect::<String>();
|
let content = snapshot.text_for_range(0..len).collect::<String>();
|
||||||
if let Some(path) = path {
|
if let Some(path) = path {
|
||||||
format!("# First 1KB of {path} (file too large to show full content, and no outline available)\n\n{content}")
|
format!("# First 1KB of {path} (file too large to show full content, and no outline available)\n\n{content}")
|
||||||
@@ -178,7 +178,7 @@ mod tests {
|
|||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
let project = Project::test(fs, [], cx).await;
|
let project = Project::test(fs, [], cx).await;
|
||||||
|
|
||||||
let content = "A".repeat(100 * 1024); // 100KB
|
let content = "⚡".repeat(100 * 1024); // 100KB
|
||||||
let content_len = content.len();
|
let content_len = content.len();
|
||||||
let buffer = project
|
let buffer = project
|
||||||
.update(cx, |project, cx| project.create_buffer(true, cx))
|
.update(cx, |project, cx| project.create_buffer(true, cx))
|
||||||
@@ -194,7 +194,7 @@ mod tests {
|
|||||||
|
|
||||||
// Should contain some of the actual file content
|
// Should contain some of the actual file content
|
||||||
assert!(
|
assert!(
|
||||||
result.text.contains("AAAAAAAAAA"),
|
result.text.contains("⚡⚡⚡⚡⚡⚡⚡"),
|
||||||
"Result did not contain content subset"
|
"Result did not contain content subset"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -215,7 +215,8 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
|
|||||||
vec![LanguageModelRequestMessage {
|
vec![LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Message 1".into()],
|
content: vec!["Message 1".into()],
|
||||||
cache: true
|
cache: true,
|
||||||
|
reasoning_details: None,
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text(
|
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text(
|
||||||
@@ -239,17 +240,20 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
|
|||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Message 1".into()],
|
content: vec!["Message 1".into()],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: vec!["Response to Message 1".into()],
|
content: vec!["Response to Message 1".into()],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Message 2".into()],
|
content: vec!["Message 2".into()],
|
||||||
cache: true
|
cache: true,
|
||||||
|
reasoning_details: None,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -295,37 +299,44 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
|
|||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Message 1".into()],
|
content: vec!["Message 1".into()],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: vec!["Response to Message 1".into()],
|
content: vec!["Response to Message 1".into()],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Message 2".into()],
|
content: vec!["Message 2".into()],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: vec!["Response to Message 2".into()],
|
content: vec!["Response to Message 2".into()],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Use the echo tool".into()],
|
content: vec!["Use the echo tool".into()],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: vec![MessageContent::ToolUse(tool_use)],
|
content: vec![MessageContent::ToolUse(tool_use)],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![MessageContent::ToolResult(tool_result)],
|
content: vec![MessageContent::ToolResult(tool_result)],
|
||||||
cache: true
|
cache: true,
|
||||||
|
reasoning_details: None,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -648,17 +659,20 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
|
|||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["abc".into()],
|
content: vec!["abc".into()],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: vec![MessageContent::ToolUse(tool_use.clone())],
|
content: vec![MessageContent::ToolUse(tool_use.clone())],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![MessageContent::ToolResult(tool_result.clone())],
|
content: vec![MessageContent::ToolResult(tool_result.clone())],
|
||||||
cache: true
|
cache: true,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -682,22 +696,26 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
|
|||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["abc".into()],
|
content: vec!["abc".into()],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: vec![MessageContent::ToolUse(tool_use)],
|
content: vec![MessageContent::ToolUse(tool_use)],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![MessageContent::ToolResult(tool_result)],
|
content: vec![MessageContent::ToolResult(tool_result)],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Continue where you left off".into()],
|
content: vec!["Continue where you left off".into()],
|
||||||
cache: true
|
cache: true,
|
||||||
|
reasoning_details: None,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -769,22 +787,26 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
|
|||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["abc".into()],
|
content: vec!["abc".into()],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: vec![MessageContent::ToolUse(tool_use)],
|
content: vec![MessageContent::ToolUse(tool_use)],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![MessageContent::ToolResult(tool_result)],
|
content: vec![MessageContent::ToolResult(tool_result)],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["ghi".into()],
|
content: vec!["ghi".into()],
|
||||||
cache: true
|
cache: true,
|
||||||
|
reasoning_details: None,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -1827,7 +1849,8 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
|
|||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Hey!".into()],
|
content: vec!["Hey!".into()],
|
||||||
cache: true
|
cache: true,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
@@ -1835,7 +1858,8 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
|
|||||||
MessageContent::Text("Hi!".into()),
|
MessageContent::Text("Hi!".into()),
|
||||||
MessageContent::ToolUse(echo_tool_use.clone())
|
MessageContent::ToolUse(echo_tool_use.clone())
|
||||||
],
|
],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
@@ -1846,7 +1870,8 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
|
|||||||
content: "test".into(),
|
content: "test".into(),
|
||||||
output: Some("test".into())
|
output: Some("test".into())
|
||||||
})],
|
})],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -2244,12 +2269,14 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
|
|||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Call the echo tool!".into()],
|
content: vec!["Call the echo tool!".into()],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())],
|
content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())],
|
||||||
cache: false
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
@@ -2262,7 +2289,8 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
|
|||||||
output: Some("test".into())
|
output: Some("test".into())
|
||||||
}
|
}
|
||||||
)],
|
)],
|
||||||
cache: true
|
cache: true,
|
||||||
|
reasoning_details: None,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -2276,7 +2304,8 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
|
|||||||
thread.last_message(),
|
thread.last_message(),
|
||||||
Some(Message::Agent(AgentMessage {
|
Some(Message::Agent(AgentMessage {
|
||||||
content: vec![AgentMessageContent::Text("Done".into())],
|
content: vec![AgentMessageContent::Text("Done".into())],
|
||||||
tool_results: IndexMap::default()
|
tool_results: IndexMap::default(),
|
||||||
|
reasoning_details: None,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ impl Message {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Continue where you left off".into()],
|
content: vec!["Continue where you left off".into()],
|
||||||
cache: false,
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,6 +178,7 @@ impl UserMessage {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: Vec::with_capacity(self.content.len()),
|
content: Vec::with_capacity(self.content.len()),
|
||||||
cache: false,
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
const OPEN_CONTEXT: &str = "<context>\n\
|
const OPEN_CONTEXT: &str = "<context>\n\
|
||||||
@@ -444,6 +446,7 @@ impl AgentMessage {
|
|||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: Vec::with_capacity(self.content.len()),
|
content: Vec::with_capacity(self.content.len()),
|
||||||
cache: false,
|
cache: false,
|
||||||
|
reasoning_details: self.reasoning_details.clone(),
|
||||||
};
|
};
|
||||||
for chunk in &self.content {
|
for chunk in &self.content {
|
||||||
match chunk {
|
match chunk {
|
||||||
@@ -479,6 +482,7 @@ impl AgentMessage {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: Vec::new(),
|
content: Vec::new(),
|
||||||
cache: false,
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
for tool_result in self.tool_results.values() {
|
for tool_result in self.tool_results.values() {
|
||||||
@@ -508,6 +512,7 @@ impl AgentMessage {
|
|||||||
pub struct AgentMessage {
|
pub struct AgentMessage {
|
||||||
pub content: Vec<AgentMessageContent>,
|
pub content: Vec<AgentMessageContent>,
|
||||||
pub tool_results: IndexMap<LanguageModelToolUseId, LanguageModelToolResult>,
|
pub tool_results: IndexMap<LanguageModelToolUseId, LanguageModelToolResult>,
|
||||||
|
pub reasoning_details: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -1398,6 +1403,18 @@ impl Thread {
|
|||||||
self.handle_thinking_event(text, signature, event_stream, cx)
|
self.handle_thinking_event(text, signature, event_stream, cx)
|
||||||
}
|
}
|
||||||
RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx),
|
RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx),
|
||||||
|
ReasoningDetails(details) => {
|
||||||
|
let last_message = self.pending_message();
|
||||||
|
// Store the last non-empty reasoning_details (overwrites earlier ones)
|
||||||
|
// This ensures we keep the encrypted reasoning with signatures, not the early text reasoning
|
||||||
|
if let serde_json::Value::Array(ref arr) = details {
|
||||||
|
if !arr.is_empty() {
|
||||||
|
last_message.reasoning_details = Some(details);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
last_message.reasoning_details = Some(details);
|
||||||
|
}
|
||||||
|
}
|
||||||
ToolUse(tool_use) => {
|
ToolUse(tool_use) => {
|
||||||
return Ok(self.handle_tool_use_event(tool_use, event_stream, cx));
|
return Ok(self.handle_tool_use_event(tool_use, event_stream, cx));
|
||||||
}
|
}
|
||||||
@@ -1673,6 +1690,7 @@ impl Thread {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()],
|
content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()],
|
||||||
cache: false,
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
let task = cx
|
let task = cx
|
||||||
@@ -1737,6 +1755,7 @@ impl Thread {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![SUMMARIZE_THREAD_PROMPT.into()],
|
content: vec![SUMMARIZE_THREAD_PROMPT.into()],
|
||||||
cache: false,
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
});
|
});
|
||||||
self.pending_title_generation = Some(cx.spawn(async move |this, cx| {
|
self.pending_title_generation = Some(cx.spawn(async move |this, cx| {
|
||||||
let mut title = String::new();
|
let mut title = String::new();
|
||||||
@@ -1984,6 +2003,7 @@ impl Thread {
|
|||||||
role: Role::System,
|
role: Role::System,
|
||||||
content: vec![system_prompt.into()],
|
content: vec![system_prompt.into()],
|
||||||
cache: false,
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
}];
|
}];
|
||||||
for message in &self.messages {
|
for message in &self.messages {
|
||||||
messages.extend(message.to_request());
|
messages.extend(message.to_request());
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ postage.workspace = true
|
|||||||
project.workspace = true
|
project.workspace = true
|
||||||
prompt_store.workspace = true
|
prompt_store.workspace = true
|
||||||
proto.workspace = true
|
proto.workspace = true
|
||||||
ref-cast.workspace = true
|
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
rope.workspace = true
|
rope.workspace = true
|
||||||
rules_library.workspace = true
|
rules_library.workspace = true
|
||||||
@@ -93,7 +92,6 @@ time_format.workspace = true
|
|||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
ui_input.workspace = true
|
ui_input.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
urlencoding.workspace = true
|
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
watch.workspace = true
|
watch.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
@@ -115,6 +113,7 @@ languages = { workspace = true, features = ["test-support"] }
|
|||||||
language_model = { workspace = true, "features" = ["test-support"] }
|
language_model = { workspace = true, "features" = ["test-support"] }
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
project = { workspace = true, features = ["test-support"] }
|
project = { workspace = true, features = ["test-support"] }
|
||||||
|
semver.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
tree-sitter-md.workspace = true
|
tree-sitter-md.workspace = true
|
||||||
unindent.workspace = true
|
unindent.workspace = true
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
mod completion_provider;
|
|
||||||
mod entry_view_state;
|
mod entry_view_state;
|
||||||
mod message_editor;
|
mod message_editor;
|
||||||
mod mode_selector;
|
mod mode_selector;
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ mod tests {
|
|||||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||||
use editor::RowInfo;
|
use editor::RowInfo;
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use gpui::{AppContext as _, SemanticVersion, TestAppContext};
|
use gpui::{AppContext as _, TestAppContext};
|
||||||
|
|
||||||
use crate::acp::entry_view_state::EntryViewState;
|
use crate::acp::entry_view_state::EntryViewState;
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
@@ -539,7 +539,7 @@ mod tests {
|
|||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
theme::init(theme::LoadThemes::JustBase, cx);
|
theme::init(theme::LoadThemes::JustBase, cx);
|
||||||
release_channel::init(SemanticVersion::default(), cx);
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,14 +7,17 @@ use collections::IndexMap;
|
|||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use fuzzy::{StringMatchCandidate, match_strings};
|
use fuzzy::{StringMatchCandidate, match_strings};
|
||||||
use gpui::{AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity};
|
use gpui::{
|
||||||
|
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity,
|
||||||
|
};
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use ui::{
|
use ui::{
|
||||||
DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, ListItem,
|
DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, KeyBinding, ListItem,
|
||||||
ListItemSpacing, prelude::*,
|
ListItemSpacing, prelude::*,
|
||||||
};
|
};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
use zed_actions::agent::OpenSettings;
|
||||||
|
|
||||||
use crate::ui::HoldForDefault;
|
use crate::ui::HoldForDefault;
|
||||||
|
|
||||||
@@ -24,10 +27,12 @@ pub fn acp_model_selector(
|
|||||||
selector: Rc<dyn AgentModelSelector>,
|
selector: Rc<dyn AgentModelSelector>,
|
||||||
agent_server: Rc<dyn AgentServer>,
|
agent_server: Rc<dyn AgentServer>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<AcpModelSelector>,
|
cx: &mut Context<AcpModelSelector>,
|
||||||
) -> AcpModelSelector {
|
) -> AcpModelSelector {
|
||||||
let delegate = AcpModelPickerDelegate::new(selector, agent_server, fs, window, cx);
|
let delegate =
|
||||||
|
AcpModelPickerDelegate::new(selector, agent_server, fs, focus_handle, window, cx);
|
||||||
Picker::list(delegate, window, cx)
|
Picker::list(delegate, window, cx)
|
||||||
.show_scrollbar(true)
|
.show_scrollbar(true)
|
||||||
.width(rems(20.))
|
.width(rems(20.))
|
||||||
@@ -49,6 +54,7 @@ pub struct AcpModelPickerDelegate {
|
|||||||
selected_description: Option<(usize, SharedString, bool)>,
|
selected_description: Option<(usize, SharedString, bool)>,
|
||||||
selected_model: Option<AgentModelInfo>,
|
selected_model: Option<AgentModelInfo>,
|
||||||
_refresh_models_task: Task<()>,
|
_refresh_models_task: Task<()>,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AcpModelPickerDelegate {
|
impl AcpModelPickerDelegate {
|
||||||
@@ -56,6 +62,7 @@ impl AcpModelPickerDelegate {
|
|||||||
selector: Rc<dyn AgentModelSelector>,
|
selector: Rc<dyn AgentModelSelector>,
|
||||||
agent_server: Rc<dyn AgentServer>,
|
agent_server: Rc<dyn AgentServer>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<AcpModelSelector>,
|
cx: &mut Context<AcpModelSelector>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -104,6 +111,7 @@ impl AcpModelPickerDelegate {
|
|||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
selected_description: None,
|
selected_description: None,
|
||||||
_refresh_models_task: refresh_models_task,
|
_refresh_models_task: refresh_models_task,
|
||||||
|
focus_handle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,6 +339,39 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_footer(
|
||||||
|
&self,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> Option<AnyElement> {
|
||||||
|
let focus_handle = self.focus_handle.clone();
|
||||||
|
|
||||||
|
if !self.selector.should_render_footer() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.p_1p5()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().colors().border_variant)
|
||||||
|
.child(
|
||||||
|
Button::new("configure", "Configure")
|
||||||
|
.full_width()
|
||||||
|
.style(ButtonStyle::Outlined)
|
||||||
|
.key_binding(
|
||||||
|
KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx)
|
||||||
|
.map(|kb| kb.size(rems_from_px(12.))),
|
||||||
|
)
|
||||||
|
.on_click(|_, window, cx| {
|
||||||
|
window.dispatch_action(OpenSettings.boxed_clone(), cx);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn info_list_to_picker_entries(
|
fn info_list_to_picker_entries(
|
||||||
|
|||||||
@@ -30,8 +30,18 @@ impl AcpModelSelectorPopover {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let focus_handle_clone = focus_handle.clone();
|
||||||
Self {
|
Self {
|
||||||
selector: cx.new(move |cx| acp_model_selector(selector, agent_server, fs, window, cx)),
|
selector: cx.new(move |cx| {
|
||||||
|
acp_model_selector(
|
||||||
|
selector,
|
||||||
|
agent_server,
|
||||||
|
fs,
|
||||||
|
focus_handle_clone.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}),
|
||||||
menu_handle,
|
menu_handle,
|
||||||
focus_handle,
|
focus_handle,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ pub struct AcpThreadView {
|
|||||||
_cancel_task: Option<Task<()>>,
|
_cancel_task: Option<Task<()>>,
|
||||||
_subscriptions: [Subscription; 5],
|
_subscriptions: [Subscription; 5],
|
||||||
show_codex_windows_warning: bool,
|
show_codex_windows_warning: bool,
|
||||||
|
in_flight_prompt: Option<Vec<acp::ContentBlock>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ThreadState {
|
enum ThreadState {
|
||||||
@@ -437,6 +438,7 @@ impl AcpThreadView {
|
|||||||
new_server_version_available: None,
|
new_server_version_available: None,
|
||||||
resume_thread_metadata: resume_thread,
|
resume_thread_metadata: resume_thread,
|
||||||
show_codex_windows_warning,
|
show_codex_windows_warning,
|
||||||
|
in_flight_prompt: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,7 +653,6 @@ impl AcpThreadView {
|
|||||||
mode_selector,
|
mode_selector,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
};
|
};
|
||||||
this.message_editor.focus_handle(cx).focus(window);
|
|
||||||
|
|
||||||
this.profile_selector = this.as_native_thread(cx).map(|thread| {
|
this.profile_selector = this.as_native_thread(cx).map(|thread| {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
@@ -1155,6 +1156,7 @@ impl AcpThreadView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.in_flight_prompt = Some(contents.clone());
|
||||||
this.set_editor_is_expanded(false, cx);
|
this.set_editor_is_expanded(false, cx);
|
||||||
this.scroll_to_bottom(cx);
|
this.scroll_to_bottom(cx);
|
||||||
this.message_editor.update(cx, |message_editor, cx| {
|
this.message_editor.update(cx, |message_editor, cx| {
|
||||||
@@ -1182,7 +1184,12 @@ impl AcpThreadView {
|
|||||||
})?;
|
})?;
|
||||||
let res = send.await;
|
let res = send.await;
|
||||||
let turn_time_ms = turn_start_time.elapsed().as_millis();
|
let turn_time_ms = turn_start_time.elapsed().as_millis();
|
||||||
let status = if res.is_ok() { "success" } else { "failure" };
|
let status = if res.is_ok() {
|
||||||
|
this.update(cx, |this, _| this.in_flight_prompt.take()).ok();
|
||||||
|
"success"
|
||||||
|
} else {
|
||||||
|
"failure"
|
||||||
|
};
|
||||||
telemetry::event!(
|
telemetry::event!(
|
||||||
"Agent Turn Completed",
|
"Agent Turn Completed",
|
||||||
agent = agent_telemetry_id,
|
agent = agent_telemetry_id,
|
||||||
@@ -1265,6 +1272,28 @@ impl AcpThreadView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
// Check if there are any edits from prompts before the one being regenerated.
|
||||||
|
//
|
||||||
|
// If there are, we keep/accept them since we're not regenerating the prompt that created them.
|
||||||
|
//
|
||||||
|
// If editing the prompt that generated the edits, they are auto-rejected
|
||||||
|
// through the `rewind` function in the `acp_thread`.
|
||||||
|
let has_earlier_edits = thread.read_with(cx, |thread, _| {
|
||||||
|
thread
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.take(entry_ix)
|
||||||
|
.any(|entry| entry.diffs().next().is_some())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if has_earlier_edits {
|
||||||
|
thread.update(cx, |thread, cx| {
|
||||||
|
thread.action_log().update(cx, |action_log, cx| {
|
||||||
|
action_log.keep_all_edits(None, cx);
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
thread
|
thread
|
||||||
.update(cx, |thread, cx| thread.rewind(user_message_id, cx))?
|
.update(cx, |thread, cx| thread.rewind(user_message_id, cx))?
|
||||||
.await?;
|
.await?;
|
||||||
@@ -4767,7 +4796,7 @@ impl AcpThreadView {
|
|||||||
buffer.update(cx, |buffer, cx| {
|
buffer.update(cx, |buffer, cx| {
|
||||||
buffer.set_text(markdown, cx);
|
buffer.set_text(markdown, cx);
|
||||||
buffer.set_language(Some(markdown_language), cx);
|
buffer.set_language(Some(markdown_language), cx);
|
||||||
buffer.set_capability(language::Capability::ReadOnly, cx);
|
buffer.set_capability(language::Capability::ReadWrite, cx);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
workspace.update_in(cx, |workspace, window, cx| {
|
workspace.update_in(cx, |workspace, window, cx| {
|
||||||
@@ -5694,6 +5723,11 @@ impl AcpThreadView {
|
|||||||
provider_id: None,
|
provider_id: None,
|
||||||
};
|
};
|
||||||
this.clear_thread_error(cx);
|
this.clear_thread_error(cx);
|
||||||
|
if let Some(message) = this.in_flight_prompt.take() {
|
||||||
|
this.message_editor.update(cx, |editor, cx| {
|
||||||
|
editor.set_message(message, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
let this = cx.weak_entity();
|
let this = cx.weak_entity();
|
||||||
window.defer(cx, |window, cx| {
|
window.defer(cx, |window, cx| {
|
||||||
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
||||||
@@ -6073,7 +6107,7 @@ pub(crate) mod tests {
|
|||||||
use assistant_text_thread::TextThreadStore;
|
use assistant_text_thread::TextThreadStore;
|
||||||
use editor::MultiBufferOffset;
|
use editor::MultiBufferOffset;
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext};
|
use gpui::{EventEmitter, TestAppContext, VisualTestContext};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
@@ -6590,7 +6624,7 @@ pub(crate) mod tests {
|
|||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
theme::init(theme::LoadThemes::JustBase, cx);
|
theme::init(theme::LoadThemes::JustBase, cx);
|
||||||
release_channel::init(SemanticVersion::default(), cx);
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||||
prompt_store::init(cx)
|
prompt_store::init(cx)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ impl ManageProfilesModal {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
false, // Do not use popover styles for the model picker
|
false, // Do not use popover styles for the model picker
|
||||||
|
self.focus_handle.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ impl AgentModelSelector {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let focus_handle_clone = focus_handle.clone();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
selector: cx.new(move |cx| {
|
selector: cx.new(move |cx| {
|
||||||
let fs = fs.clone();
|
let fs = fs.clone();
|
||||||
@@ -48,6 +50,7 @@ impl AgentModelSelector {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
true, // Use popover styles for picker
|
true, // Use popover styles for picker
|
||||||
|
focus_handle_clone,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use settings::{
|
|||||||
|
|
||||||
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
|
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
|
||||||
|
|
||||||
|
use crate::ManageProfiles;
|
||||||
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
|
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
|
||||||
use crate::{
|
use crate::{
|
||||||
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
|
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
|
||||||
@@ -34,7 +35,6 @@ use crate::{
|
|||||||
acp::{AcpThreadHistory, ThreadHistoryEvent},
|
acp::{AcpThreadHistory, ThreadHistoryEvent},
|
||||||
};
|
};
|
||||||
use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary};
|
use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary};
|
||||||
use crate::{ManageProfiles, context_store::ContextStore};
|
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use ai_onboarding::AgentPanelOnboarding;
|
use ai_onboarding::AgentPanelOnboarding;
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
@@ -431,7 +431,6 @@ pub struct AgentPanel {
|
|||||||
text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
|
text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
context_server_registry: Entity<ContextServerRegistry>,
|
context_server_registry: Entity<ContextServerRegistry>,
|
||||||
inline_assist_context_store: Entity<ContextStore>,
|
|
||||||
configuration: Option<Entity<AgentConfiguration>>,
|
configuration: Option<Entity<AgentConfiguration>>,
|
||||||
configuration_subscription: Option<Subscription>,
|
configuration_subscription: Option<Subscription>,
|
||||||
active_view: ActiveView,
|
active_view: ActiveView,
|
||||||
@@ -543,7 +542,6 @@ impl AgentPanel {
|
|||||||
let client = workspace.client().clone();
|
let client = workspace.client().clone();
|
||||||
let workspace = workspace.weak_handle();
|
let workspace = workspace.weak_handle();
|
||||||
|
|
||||||
let inline_assist_context_store = cx.new(|_cx| ContextStore::new(project.downgrade()));
|
|
||||||
let context_server_registry =
|
let context_server_registry =
|
||||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||||
|
|
||||||
@@ -680,7 +678,6 @@ impl AgentPanel {
|
|||||||
configuration: None,
|
configuration: None,
|
||||||
configuration_subscription: None,
|
configuration_subscription: None,
|
||||||
context_server_registry,
|
context_server_registry,
|
||||||
inline_assist_context_store,
|
|
||||||
previous_view: None,
|
previous_view: None,
|
||||||
new_thread_menu_handle: PopoverMenuHandle::default(),
|
new_thread_menu_handle: PopoverMenuHandle::default(),
|
||||||
agent_panel_menu_handle: PopoverMenuHandle::default(),
|
agent_panel_menu_handle: PopoverMenuHandle::default(),
|
||||||
@@ -721,10 +718,6 @@ impl AgentPanel {
|
|||||||
&self.prompt_store
|
&self.prompt_store
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn inline_assist_context_store(&self) -> &Entity<ContextStore> {
|
|
||||||
&self.inline_assist_context_store
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn thread_store(&self) -> &Entity<HistoryStore> {
|
pub(crate) fn thread_store(&self) -> &Entity<HistoryStore> {
|
||||||
&self.history_store
|
&self.history_store
|
||||||
}
|
}
|
||||||
@@ -823,6 +816,7 @@ impl AgentPanel {
|
|||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
),
|
),
|
||||||
|
true,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -918,7 +912,12 @@ impl AgentPanel {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.set_active_view(ActiveView::ExternalAgentThread { thread_view }, window, cx);
|
this.set_active_view(
|
||||||
|
ActiveView::ExternalAgentThread { thread_view },
|
||||||
|
!loading,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
@@ -960,10 +959,10 @@ impl AgentPanel {
|
|||||||
fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if matches!(self.active_view, ActiveView::History) {
|
if matches!(self.active_view, ActiveView::History) {
|
||||||
if let Some(previous_view) = self.previous_view.take() {
|
if let Some(previous_view) = self.previous_view.take() {
|
||||||
self.set_active_view(previous_view, window, cx);
|
self.set_active_view(previous_view, true, window, cx);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.set_active_view(ActiveView::History, window, cx);
|
self.set_active_view(ActiveView::History, true, window, cx);
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -1019,6 +1018,7 @@ impl AgentPanel {
|
|||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
),
|
),
|
||||||
|
true,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -1164,7 +1164,7 @@ impl AgentPanel {
|
|||||||
let context_server_store = self.project.read(cx).context_server_store();
|
let context_server_store = self.project.read(cx).context_server_store();
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
|
|
||||||
self.set_active_view(ActiveView::Configuration, window, cx);
|
self.set_active_view(ActiveView::Configuration, true, window, cx);
|
||||||
self.configuration = Some(cx.new(|cx| {
|
self.configuration = Some(cx.new(|cx| {
|
||||||
AgentConfiguration::new(
|
AgentConfiguration::new(
|
||||||
fs,
|
fs,
|
||||||
@@ -1281,6 +1281,7 @@ impl AgentPanel {
|
|||||||
fn set_active_view(
|
fn set_active_view(
|
||||||
&mut self,
|
&mut self,
|
||||||
new_view: ActiveView,
|
new_view: ActiveView,
|
||||||
|
focus: bool,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
@@ -1319,7 +1320,9 @@ impl AgentPanel {
|
|||||||
self.active_view = new_view;
|
self.active_view = new_view;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.focus_handle(cx).focus(window);
|
if focus {
|
||||||
|
self.focus_handle(cx).focus(window);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn populate_recently_opened_menu_section(
|
fn populate_recently_opened_menu_section(
|
||||||
@@ -2664,23 +2667,19 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
|
|||||||
cx: &mut Context<RulesLibrary>,
|
cx: &mut Context<RulesLibrary>,
|
||||||
) {
|
) {
|
||||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||||
let Some(project) = self
|
let Some(workspace) = self.workspace.upgrade() else {
|
||||||
.workspace
|
|
||||||
.upgrade()
|
|
||||||
.map(|workspace| workspace.read(cx).project().downgrade())
|
|
||||||
else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let prompt_store = None;
|
let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
|
||||||
let thread_store = None;
|
return;
|
||||||
let context_store = cx.new(|_| ContextStore::new(project.clone()));
|
};
|
||||||
|
let project = workspace.read(cx).project().downgrade();
|
||||||
assistant.assist(
|
assistant.assist(
|
||||||
prompt_editor,
|
prompt_editor,
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
context_store,
|
|
||||||
project,
|
project,
|
||||||
prompt_store,
|
panel.read(cx).thread_store().clone(),
|
||||||
thread_store,
|
None,
|
||||||
initial_prompt,
|
initial_prompt,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|||||||
@@ -4,14 +4,13 @@ mod agent_diff;
|
|||||||
mod agent_model_selector;
|
mod agent_model_selector;
|
||||||
mod agent_panel;
|
mod agent_panel;
|
||||||
mod buffer_codegen;
|
mod buffer_codegen;
|
||||||
|
mod completion_provider;
|
||||||
mod context;
|
mod context;
|
||||||
mod context_picker;
|
|
||||||
mod context_server_configuration;
|
mod context_server_configuration;
|
||||||
mod context_store;
|
|
||||||
mod context_strip;
|
|
||||||
mod inline_assistant;
|
mod inline_assistant;
|
||||||
mod inline_prompt_editor;
|
mod inline_prompt_editor;
|
||||||
mod language_model_selector;
|
mod language_model_selector;
|
||||||
|
mod mention_set;
|
||||||
mod profile_selector;
|
mod profile_selector;
|
||||||
mod slash_command;
|
mod slash_command;
|
||||||
mod slash_command_picker;
|
mod slash_command_picker;
|
||||||
@@ -35,7 +34,7 @@ use language::{
|
|||||||
language_settings::{AllLanguageSettings, EditPredictionProvider},
|
language_settings::{AllLanguageSettings, EditPredictionProvider},
|
||||||
};
|
};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
ConfiguredModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
||||||
};
|
};
|
||||||
use project::DisableAiSettings;
|
use project::DisableAiSettings;
|
||||||
use prompt_store::PromptBuilder;
|
use prompt_store::PromptBuilder;
|
||||||
@@ -56,8 +55,6 @@ actions!(
|
|||||||
[
|
[
|
||||||
/// Creates a new text-based conversation thread.
|
/// Creates a new text-based conversation thread.
|
||||||
NewTextThread,
|
NewTextThread,
|
||||||
/// Toggles the context picker interface for adding files, symbols, or other context.
|
|
||||||
ToggleContextPicker,
|
|
||||||
/// Toggles the menu to create new agent threads.
|
/// Toggles the menu to create new agent threads.
|
||||||
ToggleNewThreadMenu,
|
ToggleNewThreadMenu,
|
||||||
/// Toggles the navigation menu for switching between threads and views.
|
/// Toggles the navigation menu for switching between threads and views.
|
||||||
@@ -70,8 +67,6 @@ actions!(
|
|||||||
ToggleProfileSelector,
|
ToggleProfileSelector,
|
||||||
/// Cycles through available session modes.
|
/// Cycles through available session modes.
|
||||||
CycleModeSelector,
|
CycleModeSelector,
|
||||||
/// Removes all added context from the current conversation.
|
|
||||||
RemoveAllContext,
|
|
||||||
/// Expands the message editor to full size.
|
/// Expands the message editor to full size.
|
||||||
ExpandMessageEditor,
|
ExpandMessageEditor,
|
||||||
/// Opens the conversation history view.
|
/// Opens the conversation history view.
|
||||||
@@ -94,10 +89,6 @@ actions!(
|
|||||||
FocusLeft,
|
FocusLeft,
|
||||||
/// Moves focus right in the interface.
|
/// Moves focus right in the interface.
|
||||||
FocusRight,
|
FocusRight,
|
||||||
/// Removes the currently focused context item.
|
|
||||||
RemoveFocusedContext,
|
|
||||||
/// Accepts the suggested context item.
|
|
||||||
AcceptSuggestedContext,
|
|
||||||
/// Opens the active thread as a markdown file.
|
/// Opens the active thread as a markdown file.
|
||||||
OpenActiveThreadAsMarkdown,
|
OpenActiveThreadAsMarkdown,
|
||||||
/// Opens the agent diff view to review changes.
|
/// Opens the agent diff view to review changes.
|
||||||
@@ -220,11 +211,6 @@ impl ModelUsageContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn language_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
|
||||||
self.configured_model(cx)
|
|
||||||
.map(|configured_model| configured_model.model)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes the `agent` crate.
|
/// Initializes the `agent` crate.
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
use crate::{
|
use crate::{context::LoadedContext, inline_prompt_editor::CodegenStatus};
|
||||||
context::load_context, context_store::ContextStore, inline_prompt_editor::CodegenStatus,
|
|
||||||
};
|
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use client::telemetry::Telemetry;
|
use client::telemetry::Telemetry;
|
||||||
@@ -8,9 +6,12 @@ use cloud_llm_client::CompletionIntent;
|
|||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||||
use futures::{
|
use futures::{
|
||||||
SinkExt, Stream, StreamExt, TryStreamExt as _, channel::mpsc, future::LocalBoxFuture, join,
|
SinkExt, Stream, StreamExt, TryStreamExt as _,
|
||||||
|
channel::mpsc,
|
||||||
|
future::{LocalBoxFuture, Shared},
|
||||||
|
join,
|
||||||
};
|
};
|
||||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task, WeakEntity};
|
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task};
|
||||||
use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
|
use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||||
@@ -18,8 +19,7 @@ use language_model::{
|
|||||||
};
|
};
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::Project;
|
use prompt_store::PromptBuilder;
|
||||||
use prompt_store::{PromptBuilder, PromptStore};
|
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use smol::future::FutureExt;
|
use smol::future::FutureExt;
|
||||||
use std::{
|
use std::{
|
||||||
@@ -43,9 +43,6 @@ pub struct BufferCodegen {
|
|||||||
buffer: Entity<MultiBuffer>,
|
buffer: Entity<MultiBuffer>,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
initial_transaction_id: Option<TransactionId>,
|
initial_transaction_id: Option<TransactionId>,
|
||||||
context_store: Entity<ContextStore>,
|
|
||||||
project: WeakEntity<Project>,
|
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
telemetry: Arc<Telemetry>,
|
telemetry: Arc<Telemetry>,
|
||||||
builder: Arc<PromptBuilder>,
|
builder: Arc<PromptBuilder>,
|
||||||
pub is_insertion: bool,
|
pub is_insertion: bool,
|
||||||
@@ -56,9 +53,6 @@ impl BufferCodegen {
|
|||||||
buffer: Entity<MultiBuffer>,
|
buffer: Entity<MultiBuffer>,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
initial_transaction_id: Option<TransactionId>,
|
initial_transaction_id: Option<TransactionId>,
|
||||||
context_store: Entity<ContextStore>,
|
|
||||||
project: WeakEntity<Project>,
|
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
telemetry: Arc<Telemetry>,
|
telemetry: Arc<Telemetry>,
|
||||||
builder: Arc<PromptBuilder>,
|
builder: Arc<PromptBuilder>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
@@ -68,9 +62,6 @@ impl BufferCodegen {
|
|||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
false,
|
false,
|
||||||
Some(context_store.clone()),
|
|
||||||
project.clone(),
|
|
||||||
prompt_store.clone(),
|
|
||||||
Some(telemetry.clone()),
|
Some(telemetry.clone()),
|
||||||
builder.clone(),
|
builder.clone(),
|
||||||
cx,
|
cx,
|
||||||
@@ -85,9 +76,6 @@ impl BufferCodegen {
|
|||||||
buffer,
|
buffer,
|
||||||
range,
|
range,
|
||||||
initial_transaction_id,
|
initial_transaction_id,
|
||||||
context_store,
|
|
||||||
project,
|
|
||||||
prompt_store,
|
|
||||||
telemetry,
|
telemetry,
|
||||||
builder,
|
builder,
|
||||||
};
|
};
|
||||||
@@ -148,6 +136,7 @@ impl BufferCodegen {
|
|||||||
&mut self,
|
&mut self,
|
||||||
primary_model: Arc<dyn LanguageModel>,
|
primary_model: Arc<dyn LanguageModel>,
|
||||||
user_prompt: String,
|
user_prompt: String,
|
||||||
|
context_task: Shared<Task<Option<LoadedContext>>>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let alternative_models = LanguageModelRegistry::read_global(cx)
|
let alternative_models = LanguageModelRegistry::read_global(cx)
|
||||||
@@ -165,9 +154,6 @@ impl BufferCodegen {
|
|||||||
self.buffer.clone(),
|
self.buffer.clone(),
|
||||||
self.range.clone(),
|
self.range.clone(),
|
||||||
false,
|
false,
|
||||||
Some(self.context_store.clone()),
|
|
||||||
self.project.clone(),
|
|
||||||
self.prompt_store.clone(),
|
|
||||||
Some(self.telemetry.clone()),
|
Some(self.telemetry.clone()),
|
||||||
self.builder.clone(),
|
self.builder.clone(),
|
||||||
cx,
|
cx,
|
||||||
@@ -180,7 +166,7 @@ impl BufferCodegen {
|
|||||||
.zip(&self.alternatives)
|
.zip(&self.alternatives)
|
||||||
{
|
{
|
||||||
alternative.update(cx, |alternative, cx| {
|
alternative.update(cx, |alternative, cx| {
|
||||||
alternative.start(user_prompt.clone(), model.clone(), cx)
|
alternative.start(user_prompt.clone(), context_task.clone(), model.clone(), cx)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,9 +229,6 @@ pub struct CodegenAlternative {
|
|||||||
status: CodegenStatus,
|
status: CodegenStatus,
|
||||||
generation: Task<()>,
|
generation: Task<()>,
|
||||||
diff: Diff,
|
diff: Diff,
|
||||||
context_store: Option<Entity<ContextStore>>,
|
|
||||||
project: WeakEntity<Project>,
|
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
telemetry: Option<Arc<Telemetry>>,
|
telemetry: Option<Arc<Telemetry>>,
|
||||||
_subscription: gpui::Subscription,
|
_subscription: gpui::Subscription,
|
||||||
builder: Arc<PromptBuilder>,
|
builder: Arc<PromptBuilder>,
|
||||||
@@ -264,9 +247,6 @@ impl CodegenAlternative {
|
|||||||
buffer: Entity<MultiBuffer>,
|
buffer: Entity<MultiBuffer>,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
active: bool,
|
active: bool,
|
||||||
context_store: Option<Entity<ContextStore>>,
|
|
||||||
project: WeakEntity<Project>,
|
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
telemetry: Option<Arc<Telemetry>>,
|
telemetry: Option<Arc<Telemetry>>,
|
||||||
builder: Arc<PromptBuilder>,
|
builder: Arc<PromptBuilder>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
@@ -307,9 +287,6 @@ impl CodegenAlternative {
|
|||||||
status: CodegenStatus::Idle,
|
status: CodegenStatus::Idle,
|
||||||
generation: Task::ready(()),
|
generation: Task::ready(()),
|
||||||
diff: Diff::default(),
|
diff: Diff::default(),
|
||||||
context_store,
|
|
||||||
project,
|
|
||||||
prompt_store,
|
|
||||||
telemetry,
|
telemetry,
|
||||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||||
builder,
|
builder,
|
||||||
@@ -366,6 +343,7 @@ impl CodegenAlternative {
|
|||||||
pub fn start(
|
pub fn start(
|
||||||
&mut self,
|
&mut self,
|
||||||
user_prompt: String,
|
user_prompt: String,
|
||||||
|
context_task: Shared<Task<Option<LoadedContext>>>,
|
||||||
model: Arc<dyn LanguageModel>,
|
model: Arc<dyn LanguageModel>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -384,7 +362,7 @@ impl CodegenAlternative {
|
|||||||
if user_prompt.trim().to_lowercase() == "delete" {
|
if user_prompt.trim().to_lowercase() == "delete" {
|
||||||
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
||||||
} else {
|
} else {
|
||||||
let request = self.build_request(&model, user_prompt, cx)?;
|
let request = self.build_request(&model, user_prompt, context_task, cx)?;
|
||||||
cx.spawn(async move |_, cx| {
|
cx.spawn(async move |_, cx| {
|
||||||
Ok(model.stream_completion_text(request.await, cx).await?)
|
Ok(model.stream_completion_text(request.await, cx).await?)
|
||||||
})
|
})
|
||||||
@@ -398,6 +376,7 @@ impl CodegenAlternative {
|
|||||||
&self,
|
&self,
|
||||||
model: &Arc<dyn LanguageModel>,
|
model: &Arc<dyn LanguageModel>,
|
||||||
user_prompt: String,
|
user_prompt: String,
|
||||||
|
context_task: Shared<Task<Option<LoadedContext>>>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Result<Task<LanguageModelRequest>> {
|
) -> Result<Task<LanguageModelRequest>> {
|
||||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||||
@@ -437,19 +416,6 @@ impl CodegenAlternative {
|
|||||||
)
|
)
|
||||||
.context("generating content prompt")?;
|
.context("generating content prompt")?;
|
||||||
|
|
||||||
let context_task = self.context_store.as_ref().and_then(|context_store| {
|
|
||||||
if let Some(project) = self.project.upgrade() {
|
|
||||||
let context = context_store
|
|
||||||
.read(cx)
|
|
||||||
.context()
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
Some(load_context(context, &project, &self.prompt_store, cx))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let temperature = AgentSettings::temperature_for_model(model, cx);
|
let temperature = AgentSettings::temperature_for_model(model, cx);
|
||||||
|
|
||||||
Ok(cx.spawn(async move |_cx| {
|
Ok(cx.spawn(async move |_cx| {
|
||||||
@@ -457,12 +423,11 @@ impl CodegenAlternative {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: Vec::new(),
|
content: Vec::new(),
|
||||||
cache: false,
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(context_task) = context_task {
|
if let Some(context) = context_task.await {
|
||||||
context_task
|
context.add_to_request_message(&mut request_message);
|
||||||
.await
|
|
||||||
.add_to_request_message(&mut request_message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request_message.content.push(prompt.into());
|
request_message.content.push(prompt.into());
|
||||||
@@ -1088,7 +1053,6 @@ impl Diff {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use fs::FakeFs;
|
|
||||||
use futures::{
|
use futures::{
|
||||||
Stream,
|
Stream,
|
||||||
stream::{self},
|
stream::{self},
|
||||||
@@ -1120,17 +1084,12 @@ mod tests {
|
|||||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||||
});
|
});
|
||||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
let project = Project::test(fs, vec![], cx).await;
|
|
||||||
let codegen = cx.new(|cx| {
|
let codegen = cx.new(|cx| {
|
||||||
CodegenAlternative::new(
|
CodegenAlternative::new(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
true,
|
true,
|
||||||
None,
|
None,
|
||||||
project.downgrade(),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -1187,17 +1146,12 @@ mod tests {
|
|||||||
snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
|
snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
|
||||||
});
|
});
|
||||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
let project = Project::test(fs, vec![], cx).await;
|
|
||||||
let codegen = cx.new(|cx| {
|
let codegen = cx.new(|cx| {
|
||||||
CodegenAlternative::new(
|
CodegenAlternative::new(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
true,
|
true,
|
||||||
None,
|
None,
|
||||||
project.downgrade(),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -1256,17 +1210,12 @@ mod tests {
|
|||||||
snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
|
snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
|
||||||
});
|
});
|
||||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
let project = Project::test(fs, vec![], cx).await;
|
|
||||||
let codegen = cx.new(|cx| {
|
let codegen = cx.new(|cx| {
|
||||||
CodegenAlternative::new(
|
CodegenAlternative::new(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
true,
|
true,
|
||||||
None,
|
None,
|
||||||
project.downgrade(),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -1325,17 +1274,12 @@ mod tests {
|
|||||||
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2))
|
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2))
|
||||||
});
|
});
|
||||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
let project = Project::test(fs, vec![], cx).await;
|
|
||||||
let codegen = cx.new(|cx| {
|
let codegen = cx.new(|cx| {
|
||||||
CodegenAlternative::new(
|
CodegenAlternative::new(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
true,
|
true,
|
||||||
None,
|
None,
|
||||||
project.downgrade(),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -1382,17 +1326,12 @@ mod tests {
|
|||||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 14))
|
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 14))
|
||||||
});
|
});
|
||||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
let project = Project::test(fs, vec![], cx).await;
|
|
||||||
let codegen = cx.new(|cx| {
|
let codegen = cx.new(|cx| {
|
||||||
CodegenAlternative::new(
|
CodegenAlternative::new(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
project.downgrade(),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,931 +0,0 @@
|
|||||||
mod completion_provider;
|
|
||||||
pub(crate) mod fetch_context_picker;
|
|
||||||
pub(crate) mod file_context_picker;
|
|
||||||
pub(crate) mod rules_context_picker;
|
|
||||||
pub(crate) mod symbol_context_picker;
|
|
||||||
pub(crate) mod thread_context_picker;
|
|
||||||
|
|
||||||
use std::ops::Range;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use agent::{HistoryEntry, HistoryEntryId, HistoryStore};
|
|
||||||
use agent_client_protocol as acp;
|
|
||||||
use anyhow::{Result, anyhow};
|
|
||||||
use collections::HashSet;
|
|
||||||
pub use completion_provider::ContextPickerCompletionProvider;
|
|
||||||
use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId};
|
|
||||||
use editor::{Anchor, Editor, ExcerptId, FoldPlaceholder, ToOffset};
|
|
||||||
use fetch_context_picker::FetchContextPicker;
|
|
||||||
use file_context_picker::FileContextPicker;
|
|
||||||
use file_context_picker::render_file_context_entry;
|
|
||||||
use gpui::{
|
|
||||||
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
|
|
||||||
WeakEntity,
|
|
||||||
};
|
|
||||||
use language::Buffer;
|
|
||||||
use multi_buffer::MultiBufferRow;
|
|
||||||
use project::ProjectPath;
|
|
||||||
use prompt_store::PromptStore;
|
|
||||||
use rules_context_picker::{RulesContextEntry, RulesContextPicker};
|
|
||||||
use symbol_context_picker::SymbolContextPicker;
|
|
||||||
use thread_context_picker::render_thread_context_entry;
|
|
||||||
use ui::{
|
|
||||||
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
|
|
||||||
};
|
|
||||||
use util::paths::PathStyle;
|
|
||||||
use util::rel_path::RelPath;
|
|
||||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
|
||||||
|
|
||||||
use crate::context_picker::thread_context_picker::ThreadContextPicker;
|
|
||||||
use crate::{context::RULES_ICON, context_store::ContextStore};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub(crate) enum ContextPickerEntry {
|
|
||||||
Mode(ContextPickerMode),
|
|
||||||
Action(ContextPickerAction),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContextPickerEntry {
|
|
||||||
pub fn keyword(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Mode(mode) => mode.keyword(),
|
|
||||||
Self::Action(action) => action.keyword(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn label(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Mode(mode) => mode.label(),
|
|
||||||
Self::Action(action) => action.label(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn icon(&self) -> IconName {
|
|
||||||
match self {
|
|
||||||
Self::Mode(mode) => mode.icon(),
|
|
||||||
Self::Action(action) => action.icon(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub(crate) enum ContextPickerMode {
|
|
||||||
File,
|
|
||||||
Symbol,
|
|
||||||
Fetch,
|
|
||||||
Thread,
|
|
||||||
Rules,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub(crate) enum ContextPickerAction {
|
|
||||||
AddSelections,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContextPickerAction {
|
|
||||||
pub fn keyword(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::AddSelections => "selection",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn label(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::AddSelections => "Selection",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn icon(&self) -> IconName {
|
|
||||||
match self {
|
|
||||||
Self::AddSelections => IconName::Reader,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&str> for ContextPickerMode {
|
|
||||||
type Error = String;
|
|
||||||
|
|
||||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
|
||||||
match value {
|
|
||||||
"file" => Ok(Self::File),
|
|
||||||
"symbol" => Ok(Self::Symbol),
|
|
||||||
"fetch" => Ok(Self::Fetch),
|
|
||||||
"thread" => Ok(Self::Thread),
|
|
||||||
"rule" => Ok(Self::Rules),
|
|
||||||
_ => Err(format!("Invalid context picker mode: {}", value)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContextPickerMode {
|
|
||||||
pub fn keyword(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::File => "file",
|
|
||||||
Self::Symbol => "symbol",
|
|
||||||
Self::Fetch => "fetch",
|
|
||||||
Self::Thread => "thread",
|
|
||||||
Self::Rules => "rule",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn label(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::File => "Files & Directories",
|
|
||||||
Self::Symbol => "Symbols",
|
|
||||||
Self::Fetch => "Fetch",
|
|
||||||
Self::Thread => "Threads",
|
|
||||||
Self::Rules => "Rules",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn icon(&self) -> IconName {
|
|
||||||
match self {
|
|
||||||
Self::File => IconName::File,
|
|
||||||
Self::Symbol => IconName::Code,
|
|
||||||
Self::Fetch => IconName::ToolWeb,
|
|
||||||
Self::Thread => IconName::Thread,
|
|
||||||
Self::Rules => RULES_ICON,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
enum ContextPickerState {
|
|
||||||
Default(Entity<ContextMenu>),
|
|
||||||
File(Entity<FileContextPicker>),
|
|
||||||
Symbol(Entity<SymbolContextPicker>),
|
|
||||||
Fetch(Entity<FetchContextPicker>),
|
|
||||||
Thread(Entity<ThreadContextPicker>),
|
|
||||||
Rules(Entity<RulesContextPicker>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) struct ContextPicker {
|
|
||||||
mode: ContextPickerState,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
|
||||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
|
||||||
prompt_store: Option<WeakEntity<PromptStore>>,
|
|
||||||
_subscriptions: Vec<Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContextPicker {
|
|
||||||
pub fn new(
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
|
||||||
prompt_store: Option<WeakEntity<PromptStore>>,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let subscriptions = context_store
|
|
||||||
.upgrade()
|
|
||||||
.map(|context_store| {
|
|
||||||
cx.observe(&context_store, |this, _, cx| this.notify_current_picker(cx))
|
|
||||||
})
|
|
||||||
.into_iter()
|
|
||||||
.chain(
|
|
||||||
thread_store
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|thread_store| thread_store.upgrade())
|
|
||||||
.map(|thread_store| {
|
|
||||||
cx.observe(&thread_store, |this, _, cx| this.notify_current_picker(cx))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.collect::<Vec<Subscription>>();
|
|
||||||
|
|
||||||
ContextPicker {
|
|
||||||
mode: ContextPickerState::Default(ContextMenu::build(
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
|menu, _window, _cx| menu,
|
|
||||||
)),
|
|
||||||
workspace,
|
|
||||||
context_store,
|
|
||||||
thread_store,
|
|
||||||
prompt_store,
|
|
||||||
_subscriptions: subscriptions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.mode = ContextPickerState::Default(self.build_menu(window, cx));
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<ContextMenu> {
|
|
||||||
let context_picker = cx.entity();
|
|
||||||
|
|
||||||
let menu = ContextMenu::build(window, cx, move |menu, _window, cx| {
|
|
||||||
let Some(workspace) = self.workspace.upgrade() else {
|
|
||||||
return menu;
|
|
||||||
};
|
|
||||||
let path_style = workspace.read(cx).path_style(cx);
|
|
||||||
let recent = self.recent_entries(cx);
|
|
||||||
let has_recent = !recent.is_empty();
|
|
||||||
let recent_entries = recent
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(ix, entry)| {
|
|
||||||
self.recent_menu_item(context_picker.clone(), ix, entry, path_style)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let entries = self
|
|
||||||
.workspace
|
|
||||||
.upgrade()
|
|
||||||
.map(|workspace| {
|
|
||||||
available_context_picker_entries(
|
|
||||||
&self.prompt_store,
|
|
||||||
&self.thread_store,
|
|
||||||
&workspace,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
menu.when(has_recent, |menu| {
|
|
||||||
menu.custom_row(|_, _| {
|
|
||||||
div()
|
|
||||||
.mb_1()
|
|
||||||
.child(
|
|
||||||
Label::new("Recent")
|
|
||||||
.color(Color::Muted)
|
|
||||||
.size(LabelSize::Small),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.extend(recent_entries)
|
|
||||||
.when(has_recent, |menu| menu.separator())
|
|
||||||
.extend(entries.into_iter().map(|entry| {
|
|
||||||
let context_picker = context_picker.clone();
|
|
||||||
|
|
||||||
ContextMenuEntry::new(entry.label())
|
|
||||||
.icon(entry.icon())
|
|
||||||
.icon_size(IconSize::XSmall)
|
|
||||||
.icon_color(Color::Muted)
|
|
||||||
.handler(move |window, cx| {
|
|
||||||
context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx))
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
.keep_open_on_confirm(true)
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
|
|
||||||
cx.emit(DismissEvent);
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
menu
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether threads are allowed as context.
|
|
||||||
pub fn allow_threads(&self) -> bool {
|
|
||||||
self.thread_store.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn select_entry(
|
|
||||||
&mut self,
|
|
||||||
entry: ContextPickerEntry,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let context_picker = cx.entity().downgrade();
|
|
||||||
|
|
||||||
match entry {
|
|
||||||
ContextPickerEntry::Mode(mode) => match mode {
|
|
||||||
ContextPickerMode::File => {
|
|
||||||
self.mode = ContextPickerState::File(cx.new(|cx| {
|
|
||||||
FileContextPicker::new(
|
|
||||||
context_picker.clone(),
|
|
||||||
self.workspace.clone(),
|
|
||||||
self.context_store.clone(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
ContextPickerMode::Symbol => {
|
|
||||||
self.mode = ContextPickerState::Symbol(cx.new(|cx| {
|
|
||||||
SymbolContextPicker::new(
|
|
||||||
context_picker.clone(),
|
|
||||||
self.workspace.clone(),
|
|
||||||
self.context_store.clone(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
ContextPickerMode::Rules => {
|
|
||||||
if let Some(prompt_store) = self.prompt_store.as_ref() {
|
|
||||||
self.mode = ContextPickerState::Rules(cx.new(|cx| {
|
|
||||||
RulesContextPicker::new(
|
|
||||||
prompt_store.clone(),
|
|
||||||
context_picker.clone(),
|
|
||||||
self.context_store.clone(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ContextPickerMode::Fetch => {
|
|
||||||
self.mode = ContextPickerState::Fetch(cx.new(|cx| {
|
|
||||||
FetchContextPicker::new(
|
|
||||||
context_picker.clone(),
|
|
||||||
self.workspace.clone(),
|
|
||||||
self.context_store.clone(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
ContextPickerMode::Thread => {
|
|
||||||
if let Some(thread_store) = self.thread_store.clone() {
|
|
||||||
self.mode = ContextPickerState::Thread(cx.new(|cx| {
|
|
||||||
ThreadContextPicker::new(
|
|
||||||
thread_store,
|
|
||||||
context_picker.clone(),
|
|
||||||
self.context_store.clone(),
|
|
||||||
self.workspace.clone(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ContextPickerEntry::Action(action) => match action {
|
|
||||||
ContextPickerAction::AddSelections => {
|
|
||||||
if let Some((context_store, workspace)) =
|
|
||||||
self.context_store.upgrade().zip(self.workspace.upgrade())
|
|
||||||
{
|
|
||||||
add_selections_as_context(&context_store, &workspace, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.emit(DismissEvent);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
cx.focus_self(window);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_first(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
// Other variants already select their first entry on open automatically
|
|
||||||
if let ContextPickerState::Default(entity) = &self.mode {
|
|
||||||
entity.update(cx, |entity, cx| {
|
|
||||||
entity.select_first(&Default::default(), window, cx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recent_menu_item(
|
|
||||||
&self,
|
|
||||||
context_picker: Entity<ContextPicker>,
|
|
||||||
ix: usize,
|
|
||||||
entry: RecentEntry,
|
|
||||||
path_style: PathStyle,
|
|
||||||
) -> ContextMenuItem {
|
|
||||||
match entry {
|
|
||||||
RecentEntry::File {
|
|
||||||
project_path,
|
|
||||||
path_prefix,
|
|
||||||
} => {
|
|
||||||
let context_store = self.context_store.clone();
|
|
||||||
let worktree_id = project_path.worktree_id;
|
|
||||||
let path = project_path.path.clone();
|
|
||||||
|
|
||||||
ContextMenuItem::custom_entry(
|
|
||||||
move |_window, cx| {
|
|
||||||
render_file_context_entry(
|
|
||||||
ElementId::named_usize("ctx-recent", ix),
|
|
||||||
worktree_id,
|
|
||||||
&path,
|
|
||||||
&path_prefix,
|
|
||||||
false,
|
|
||||||
path_style,
|
|
||||||
context_store.clone(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.into_any()
|
|
||||||
},
|
|
||||||
move |window, cx| {
|
|
||||||
context_picker.update(cx, |this, cx| {
|
|
||||||
this.add_recent_file(project_path.clone(), window, cx);
|
|
||||||
})
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
RecentEntry::Thread(thread) => {
|
|
||||||
let context_store = self.context_store.clone();
|
|
||||||
let view_thread = thread.clone();
|
|
||||||
|
|
||||||
ContextMenuItem::custom_entry(
|
|
||||||
move |_window, cx| {
|
|
||||||
render_thread_context_entry(&view_thread, context_store.clone(), cx)
|
|
||||||
.into_any()
|
|
||||||
},
|
|
||||||
move |window, cx| {
|
|
||||||
context_picker.update(cx, |this, cx| {
|
|
||||||
this.add_recent_thread(thread.clone(), window, cx)
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
})
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_recent_file(
|
|
||||||
&self,
|
|
||||||
project_path: ProjectPath,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let Some(context_store) = self.context_store.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let task = context_store.update(cx, |context_store, cx| {
|
|
||||||
context_store.add_file_from_path(project_path.clone(), true, cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx))
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_recent_thread(
|
|
||||||
&self,
|
|
||||||
entry: HistoryEntry,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Task<Result<()>> {
|
|
||||||
let Some(context_store) = self.context_store.upgrade() else {
|
|
||||||
return Task::ready(Err(anyhow!("context store not available")));
|
|
||||||
};
|
|
||||||
let Some(project) = self
|
|
||||||
.workspace
|
|
||||||
.upgrade()
|
|
||||||
.map(|workspace| workspace.read(cx).project().clone())
|
|
||||||
else {
|
|
||||||
return Task::ready(Err(anyhow!("project not available")));
|
|
||||||
};
|
|
||||||
|
|
||||||
match entry {
|
|
||||||
HistoryEntry::AcpThread(thread) => {
|
|
||||||
let Some(thread_store) = self
|
|
||||||
.thread_store
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|thread_store| thread_store.upgrade())
|
|
||||||
else {
|
|
||||||
return Task::ready(Err(anyhow!("thread store not available")));
|
|
||||||
};
|
|
||||||
let load_thread_task =
|
|
||||||
agent::load_agent_thread(thread.id, thread_store, project, cx);
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
let thread = load_thread_task.await?;
|
|
||||||
context_store.update(cx, |context_store, cx| {
|
|
||||||
context_store.add_thread(thread, true, cx);
|
|
||||||
})?;
|
|
||||||
this.update(cx, |_this, cx| cx.notify())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
HistoryEntry::TextThread(thread) => {
|
|
||||||
let Some(thread_store) = self
|
|
||||||
.thread_store
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|thread_store| thread_store.upgrade())
|
|
||||||
else {
|
|
||||||
return Task::ready(Err(anyhow!("text thread store not available")));
|
|
||||||
};
|
|
||||||
|
|
||||||
let task = thread_store.update(cx, |this, cx| {
|
|
||||||
this.load_text_thread(thread.path.clone(), cx)
|
|
||||||
});
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
let thread = task.await?;
|
|
||||||
context_store.update(cx, |context_store, cx| {
|
|
||||||
context_store.add_text_thread(thread, true, cx);
|
|
||||||
})?;
|
|
||||||
this.update(cx, |_this, cx| cx.notify())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recent_entries(&self, cx: &mut App) -> Vec<RecentEntry> {
|
|
||||||
let Some(workspace) = self.workspace.upgrade() else {
|
|
||||||
return vec![];
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(context_store) = self.context_store.upgrade() else {
|
|
||||||
return vec![];
|
|
||||||
};
|
|
||||||
|
|
||||||
recent_context_picker_entries_with_store(
|
|
||||||
context_store,
|
|
||||||
self.thread_store.clone(),
|
|
||||||
workspace,
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn notify_current_picker(&mut self, cx: &mut Context<Self>) {
|
|
||||||
match &self.mode {
|
|
||||||
ContextPickerState::Default(entity) => entity.update(cx, |_, cx| cx.notify()),
|
|
||||||
ContextPickerState::File(entity) => entity.update(cx, |_, cx| cx.notify()),
|
|
||||||
ContextPickerState::Symbol(entity) => entity.update(cx, |_, cx| cx.notify()),
|
|
||||||
ContextPickerState::Fetch(entity) => entity.update(cx, |_, cx| cx.notify()),
|
|
||||||
ContextPickerState::Thread(entity) => entity.update(cx, |_, cx| cx.notify()),
|
|
||||||
ContextPickerState::Rules(entity) => entity.update(cx, |_, cx| cx.notify()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for ContextPicker {}
|
|
||||||
|
|
||||||
impl Focusable for ContextPicker {
|
|
||||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
|
||||||
match &self.mode {
|
|
||||||
ContextPickerState::Default(menu) => menu.focus_handle(cx),
|
|
||||||
ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
|
|
||||||
ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
|
|
||||||
ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
|
|
||||||
ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
|
|
||||||
ContextPickerState::Rules(user_rules_picker) => user_rules_picker.focus_handle(cx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ContextPicker {
|
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.w(px(400.))
|
|
||||||
.min_w(px(400.))
|
|
||||||
.map(|parent| match &self.mode {
|
|
||||||
ContextPickerState::Default(menu) => parent.child(menu.clone()),
|
|
||||||
ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
|
|
||||||
ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
|
|
||||||
ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
|
|
||||||
ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
|
|
||||||
ContextPickerState::Rules(user_rules_picker) => {
|
|
||||||
parent.child(user_rules_picker.clone())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) enum RecentEntry {
|
|
||||||
File {
|
|
||||||
project_path: ProjectPath,
|
|
||||||
path_prefix: Arc<RelPath>,
|
|
||||||
},
|
|
||||||
Thread(HistoryEntry),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn available_context_picker_entries(
|
|
||||||
prompt_store: &Option<WeakEntity<PromptStore>>,
|
|
||||||
thread_store: &Option<WeakEntity<HistoryStore>>,
|
|
||||||
workspace: &Entity<Workspace>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Vec<ContextPickerEntry> {
|
|
||||||
let mut entries = vec![
|
|
||||||
ContextPickerEntry::Mode(ContextPickerMode::File),
|
|
||||||
ContextPickerEntry::Mode(ContextPickerMode::Symbol),
|
|
||||||
];
|
|
||||||
|
|
||||||
let has_selection = workspace
|
|
||||||
.read(cx)
|
|
||||||
.active_item(cx)
|
|
||||||
.and_then(|item| item.downcast::<Editor>())
|
|
||||||
.is_some_and(|editor| {
|
|
||||||
editor.update(cx, |editor, cx| {
|
|
||||||
editor.has_non_empty_selection(&editor.display_snapshot(cx))
|
|
||||||
})
|
|
||||||
});
|
|
||||||
if has_selection {
|
|
||||||
entries.push(ContextPickerEntry::Action(
|
|
||||||
ContextPickerAction::AddSelections,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if thread_store.is_some() {
|
|
||||||
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
|
|
||||||
}
|
|
||||||
|
|
||||||
if prompt_store.is_some() {
|
|
||||||
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
|
|
||||||
|
|
||||||
entries
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recent_context_picker_entries_with_store(
|
|
||||||
context_store: Entity<ContextStore>,
|
|
||||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
|
||||||
workspace: Entity<Workspace>,
|
|
||||||
exclude_path: Option<ProjectPath>,
|
|
||||||
cx: &App,
|
|
||||||
) -> Vec<RecentEntry> {
|
|
||||||
let project = workspace.read(cx).project();
|
|
||||||
|
|
||||||
let mut exclude_paths = context_store.read(cx).file_paths(cx);
|
|
||||||
exclude_paths.extend(exclude_path);
|
|
||||||
|
|
||||||
let exclude_paths = exclude_paths
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|project_path| project.read(cx).absolute_path(&project_path, cx))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let exclude_threads = context_store.read(cx).thread_ids();
|
|
||||||
|
|
||||||
recent_context_picker_entries(thread_store, workspace, &exclude_paths, exclude_threads, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn recent_context_picker_entries(
|
|
||||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
|
||||||
workspace: Entity<Workspace>,
|
|
||||||
exclude_paths: &HashSet<PathBuf>,
|
|
||||||
exclude_threads: &HashSet<acp::SessionId>,
|
|
||||||
cx: &App,
|
|
||||||
) -> Vec<RecentEntry> {
|
|
||||||
let mut recent = Vec::with_capacity(6);
|
|
||||||
let workspace = workspace.read(cx);
|
|
||||||
let project = workspace.project().read(cx);
|
|
||||||
let include_root_name = workspace.visible_worktrees(cx).count() > 1;
|
|
||||||
|
|
||||||
recent.extend(
|
|
||||||
workspace
|
|
||||||
.recent_navigation_history_iter(cx)
|
|
||||||
.filter(|(_, abs_path)| {
|
|
||||||
abs_path
|
|
||||||
.as_ref()
|
|
||||||
.is_none_or(|path| !exclude_paths.contains(path.as_path()))
|
|
||||||
})
|
|
||||||
.take(4)
|
|
||||||
.filter_map(|(project_path, _)| {
|
|
||||||
project
|
|
||||||
.worktree_for_id(project_path.worktree_id, cx)
|
|
||||||
.map(|worktree| {
|
|
||||||
let path_prefix = if include_root_name {
|
|
||||||
worktree.read(cx).root_name().into()
|
|
||||||
} else {
|
|
||||||
RelPath::empty().into()
|
|
||||||
};
|
|
||||||
RecentEntry::File {
|
|
||||||
project_path,
|
|
||||||
path_prefix,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(thread_store) = thread_store.and_then(|store| store.upgrade()) {
|
|
||||||
const RECENT_THREADS_COUNT: usize = 2;
|
|
||||||
recent.extend(
|
|
||||||
thread_store
|
|
||||||
.read(cx)
|
|
||||||
.recently_opened_entries(cx)
|
|
||||||
.iter()
|
|
||||||
.filter(|e| match e.id() {
|
|
||||||
HistoryEntryId::AcpThread(session_id) => !exclude_threads.contains(&session_id),
|
|
||||||
HistoryEntryId::TextThread(path) => {
|
|
||||||
!exclude_paths.contains(&path.to_path_buf())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.take(RECENT_THREADS_COUNT)
|
|
||||||
.map(|thread| RecentEntry::Thread(thread.clone())),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
recent
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_selections_as_context(
|
|
||||||
context_store: &Entity<ContextStore>,
|
|
||||||
workspace: &Entity<Workspace>,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
let selection_ranges = selection_ranges(workspace, cx);
|
|
||||||
context_store.update(cx, |context_store, cx| {
|
|
||||||
for (buffer, range) in selection_ranges {
|
|
||||||
context_store.add_selection(buffer, range, cx);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn selection_ranges(
|
|
||||||
workspace: &Entity<Workspace>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
|
|
||||||
let Some(editor) = workspace
|
|
||||||
.read(cx)
|
|
||||||
.active_item(cx)
|
|
||||||
.and_then(|item| item.act_as::<Editor>(cx))
|
|
||||||
else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.update(cx, |editor, cx| {
|
|
||||||
let selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
|
|
||||||
|
|
||||||
let buffer = editor.buffer().clone().read(cx);
|
|
||||||
let snapshot = buffer.snapshot(cx);
|
|
||||||
|
|
||||||
selections
|
|
||||||
.into_iter()
|
|
||||||
.map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
|
|
||||||
.flat_map(|range| {
|
|
||||||
let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
|
|
||||||
let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
|
|
||||||
if start_buffer != end_buffer {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some((start_buffer, start..end))
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn insert_crease_for_mention(
|
|
||||||
excerpt_id: ExcerptId,
|
|
||||||
crease_start: text::Anchor,
|
|
||||||
content_len: usize,
|
|
||||||
crease_label: SharedString,
|
|
||||||
crease_icon_path: SharedString,
|
|
||||||
editor_entity: Entity<Editor>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Option<CreaseId> {
|
|
||||||
editor_entity.update(cx, |editor, cx| {
|
|
||||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
||||||
|
|
||||||
let start = snapshot.anchor_in_excerpt(excerpt_id, crease_start)?;
|
|
||||||
|
|
||||||
let start = start.bias_right(&snapshot);
|
|
||||||
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
|
|
||||||
|
|
||||||
let crease = crease_for_mention(
|
|
||||||
crease_label,
|
|
||||||
crease_icon_path,
|
|
||||||
start..end,
|
|
||||||
editor_entity.downgrade(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let ids = editor.insert_creases(vec![crease.clone()], cx);
|
|
||||||
editor.fold_creases(vec![crease], false, window, cx);
|
|
||||||
|
|
||||||
Some(ids[0])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn crease_for_mention(
|
|
||||||
label: SharedString,
|
|
||||||
icon_path: SharedString,
|
|
||||||
range: Range<Anchor>,
|
|
||||||
editor_entity: WeakEntity<Editor>,
|
|
||||||
) -> Crease<Anchor> {
|
|
||||||
let placeholder = FoldPlaceholder {
|
|
||||||
render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
|
|
||||||
merge_adjacent: false,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
|
|
||||||
|
|
||||||
Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
|
|
||||||
.with_metadata(CreaseMetadata { icon_path, label })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_fold_icon_button(
|
|
||||||
icon_path: SharedString,
|
|
||||||
label: SharedString,
|
|
||||||
editor: WeakEntity<Editor>,
|
|
||||||
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
|
|
||||||
Arc::new({
|
|
||||||
move |fold_id, fold_range, cx| {
|
|
||||||
let is_in_text_selection = editor
|
|
||||||
.update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
ButtonLike::new(fold_id)
|
|
||||||
.style(ButtonStyle::Filled)
|
|
||||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
|
||||||
.toggle_state(is_in_text_selection)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
Icon::from_path(icon_path.clone())
|
|
||||||
.size(IconSize::XSmall)
|
|
||||||
.color(Color::Muted),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Label::new(label.clone())
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.buffer_font(cx)
|
|
||||||
.single_line(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fold_toggle(
|
|
||||||
name: &'static str,
|
|
||||||
) -> impl Fn(
|
|
||||||
MultiBufferRow,
|
|
||||||
bool,
|
|
||||||
Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
|
|
||||||
&mut Window,
|
|
||||||
&mut App,
|
|
||||||
) -> AnyElement {
|
|
||||||
move |row, is_folded, fold, _window, _cx| {
|
|
||||||
Disclosure::new((name, row.0 as u64), !is_folded)
|
|
||||||
.toggle_state(is_folded)
|
|
||||||
.on_click(move |_e, window, cx| fold(!is_folded, window, cx))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct MentionLink;
|
|
||||||
|
|
||||||
impl MentionLink {
|
|
||||||
const FILE: &str = "@file";
|
|
||||||
const SYMBOL: &str = "@symbol";
|
|
||||||
const SELECTION: &str = "@selection";
|
|
||||||
const THREAD: &str = "@thread";
|
|
||||||
const FETCH: &str = "@fetch";
|
|
||||||
const RULE: &str = "@rule";
|
|
||||||
|
|
||||||
const TEXT_THREAD_URL_PREFIX: &str = "text-thread://";
|
|
||||||
|
|
||||||
pub fn for_file(file_name: &str, full_path: &str) -> String {
|
|
||||||
format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn for_symbol(symbol_name: &str, full_path: &str) -> String {
|
|
||||||
format!(
|
|
||||||
"[@{}]({}:{}:{})",
|
|
||||||
symbol_name,
|
|
||||||
Self::SYMBOL,
|
|
||||||
full_path,
|
|
||||||
symbol_name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn for_selection(file_name: &str, full_path: &str, line_range: Range<usize>) -> String {
|
|
||||||
format!(
|
|
||||||
"[@{} ({}-{})]({}:{}:{}-{})",
|
|
||||||
file_name,
|
|
||||||
line_range.start + 1,
|
|
||||||
line_range.end + 1,
|
|
||||||
Self::SELECTION,
|
|
||||||
full_path,
|
|
||||||
line_range.start,
|
|
||||||
line_range.end
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn for_thread(thread: &HistoryEntry) -> String {
|
|
||||||
match thread {
|
|
||||||
HistoryEntry::AcpThread(thread) => {
|
|
||||||
format!("[@{}]({}:{})", thread.title, Self::THREAD, thread.id)
|
|
||||||
}
|
|
||||||
HistoryEntry::TextThread(thread) => {
|
|
||||||
let filename = thread
|
|
||||||
.path
|
|
||||||
.file_name()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_string_lossy();
|
|
||||||
let escaped_filename = urlencoding::encode(&filename);
|
|
||||||
format!(
|
|
||||||
"[@{}]({}:{}{})",
|
|
||||||
thread.title,
|
|
||||||
Self::THREAD,
|
|
||||||
Self::TEXT_THREAD_URL_PREFIX,
|
|
||||||
escaped_filename
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn for_fetch(url: &str) -> String {
|
|
||||||
format!("[@{}]({}:{})", url, Self::FETCH, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn for_rule(rule: &RulesContextEntry) -> String {
|
|
||||||
format!("[@{}]({}:{})", rule.title, Self::RULE, rule.prompt_id.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,252 +0,0 @@
|
|||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use anyhow::{Context as _, Result, bail};
|
|
||||||
use futures::AsyncReadExt as _;
|
|
||||||
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
|
||||||
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
|
|
||||||
use http_client::{AsyncBody, HttpClientWithUrl};
|
|
||||||
use picker::{Picker, PickerDelegate};
|
|
||||||
use ui::{Context, ListItem, Window, prelude::*};
|
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
use crate::{context_picker::ContextPicker, context_store::ContextStore};
|
|
||||||
|
|
||||||
pub struct FetchContextPicker {
|
|
||||||
picker: Entity<Picker<FetchContextPickerDelegate>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FetchContextPicker {
|
|
||||||
pub fn new(
|
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
|
|
||||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
|
||||||
|
|
||||||
Self { picker }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Focusable for FetchContextPicker {
|
|
||||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
|
||||||
self.picker.focus_handle(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for FetchContextPicker {
|
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
self.picker.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
|
||||||
enum ContentType {
|
|
||||||
Html,
|
|
||||||
Plaintext,
|
|
||||||
Json,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FetchContextPickerDelegate {
|
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
|
||||||
url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FetchContextPickerDelegate {
|
|
||||||
pub fn new(
|
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
|
||||||
) -> Self {
|
|
||||||
FetchContextPickerDelegate {
|
|
||||||
context_picker,
|
|
||||||
workspace,
|
|
||||||
context_store,
|
|
||||||
url: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn fetch_url_content(
|
|
||||||
http_client: Arc<HttpClientWithUrl>,
|
|
||||||
url: String,
|
|
||||||
) -> Result<String> {
|
|
||||||
let url = if !url.starts_with("https://") && !url.starts_with("http://") {
|
|
||||||
format!("https://{url}")
|
|
||||||
} else {
|
|
||||||
url
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
|
|
||||||
|
|
||||||
let mut body = Vec::new();
|
|
||||||
response
|
|
||||||
.body_mut()
|
|
||||||
.read_to_end(&mut body)
|
|
||||||
.await
|
|
||||||
.context("error reading response body")?;
|
|
||||||
|
|
||||||
if response.status().is_client_error() {
|
|
||||||
let text = String::from_utf8_lossy(body.as_slice());
|
|
||||||
bail!(
|
|
||||||
"status error {}, response: {text:?}",
|
|
||||||
response.status().as_u16()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(content_type) = response.headers().get("content-type") else {
|
|
||||||
bail!("missing Content-Type header");
|
|
||||||
};
|
|
||||||
let content_type = content_type
|
|
||||||
.to_str()
|
|
||||||
.context("invalid Content-Type header")?;
|
|
||||||
let content_type = match content_type {
|
|
||||||
"text/html" => ContentType::Html,
|
|
||||||
"text/plain" => ContentType::Plaintext,
|
|
||||||
"application/json" => ContentType::Json,
|
|
||||||
_ => ContentType::Html,
|
|
||||||
};
|
|
||||||
|
|
||||||
match content_type {
|
|
||||||
ContentType::Html => {
|
|
||||||
let mut handlers: Vec<TagHandler> = vec![
|
|
||||||
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
|
|
||||||
Rc::new(RefCell::new(markdown::ParagraphHandler)),
|
|
||||||
Rc::new(RefCell::new(markdown::HeadingHandler)),
|
|
||||||
Rc::new(RefCell::new(markdown::ListHandler)),
|
|
||||||
Rc::new(RefCell::new(markdown::TableHandler::new())),
|
|
||||||
Rc::new(RefCell::new(markdown::StyledTextHandler)),
|
|
||||||
];
|
|
||||||
if url.contains("wikipedia.org") {
|
|
||||||
use html_to_markdown::structure::wikipedia;
|
|
||||||
|
|
||||||
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
|
|
||||||
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
|
|
||||||
handlers.push(Rc::new(
|
|
||||||
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
|
|
||||||
}
|
|
||||||
|
|
||||||
convert_html_to_markdown(&body[..], &mut handlers)
|
|
||||||
}
|
|
||||||
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
|
|
||||||
ContentType::Json => {
|
|
||||||
let json: serde_json::Value = serde_json::from_slice(&body)?;
|
|
||||||
|
|
||||||
Ok(format!(
|
|
||||||
"```json\n{}\n```",
|
|
||||||
serde_json::to_string_pretty(&json)?
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PickerDelegate for FetchContextPickerDelegate {
|
|
||||||
type ListItem = ListItem;
|
|
||||||
|
|
||||||
fn match_count(&self) -> usize {
|
|
||||||
if self.url.is_empty() { 0 } else { 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
|
||||||
Some("Enter the URL that you would like to fetch".into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn selected_index(&self) -> usize {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_selected_index(
|
|
||||||
&mut self,
|
|
||||||
_ix: usize,
|
|
||||||
_window: &mut Window,
|
|
||||||
_cx: &mut Context<Picker<Self>>,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
|
||||||
"Enter a URL…".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_matches(
|
|
||||||
&mut self,
|
|
||||||
query: String,
|
|
||||||
_window: &mut Window,
|
|
||||||
_cx: &mut Context<Picker<Self>>,
|
|
||||||
) -> Task<()> {
|
|
||||||
self.url = query;
|
|
||||||
|
|
||||||
Task::ready(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
|
||||||
let Some(workspace) = self.workspace.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let http_client = workspace.read(cx).client().http_client();
|
|
||||||
let url = self.url.clone();
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
let text = cx
|
|
||||||
.background_spawn(fetch_url_content(http_client, url.clone()))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.delegate.context_store.update(cx, |context_store, cx| {
|
|
||||||
context_store.add_fetched_url(url, text, cx)
|
|
||||||
})
|
|
||||||
})??;
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
|
||||||
self.context_picker
|
|
||||||
.update(cx, |_, cx| {
|
|
||||||
cx.emit(DismissEvent);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_match(
|
|
||||||
&self,
|
|
||||||
ix: usize,
|
|
||||||
selected: bool,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Picker<Self>>,
|
|
||||||
) -> Option<Self::ListItem> {
|
|
||||||
let added = self
|
|
||||||
.context_store
|
|
||||||
.upgrade()
|
|
||||||
.is_some_and(|context_store| context_store.read(cx).includes_url(&self.url));
|
|
||||||
|
|
||||||
Some(
|
|
||||||
ListItem::new(ix)
|
|
||||||
.inset(true)
|
|
||||||
.toggle_state(selected)
|
|
||||||
.child(Label::new(self.url.clone()))
|
|
||||||
.when(added, |child| {
|
|
||||||
child.disabled(true).end_slot(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::Check)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.color(Color::Success),
|
|
||||||
)
|
|
||||||
.child(Label::new("Added").size(LabelSize::Small)),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,392 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
|
|
||||||
use file_icons::FileIcons;
|
|
||||||
use fuzzy::PathMatch;
|
|
||||||
use gpui::{
|
|
||||||
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
|
|
||||||
};
|
|
||||||
use picker::{Picker, PickerDelegate};
|
|
||||||
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
|
|
||||||
use ui::{ListItem, Tooltip, prelude::*};
|
|
||||||
use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
|
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
context_picker::ContextPicker,
|
|
||||||
context_store::{ContextStore, FileInclusion},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct FileContextPicker {
|
|
||||||
picker: Entity<Picker<FileContextPickerDelegate>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileContextPicker {
|
|
||||||
pub fn new(
|
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store);
|
|
||||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
|
||||||
|
|
||||||
Self { picker }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Focusable for FileContextPicker {
|
|
||||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
|
||||||
self.picker.focus_handle(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for FileContextPicker {
|
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
self.picker.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FileContextPickerDelegate {
|
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
|
||||||
matches: Vec<FileMatch>,
|
|
||||||
selected_index: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileContextPickerDelegate {
|
|
||||||
pub fn new(
|
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
context_picker,
|
|
||||||
workspace,
|
|
||||||
context_store,
|
|
||||||
matches: Vec::new(),
|
|
||||||
selected_index: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PickerDelegate for FileContextPickerDelegate {
|
|
||||||
type ListItem = ListItem;
|
|
||||||
|
|
||||||
fn match_count(&self) -> usize {
|
|
||||||
self.matches.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn selected_index(&self) -> usize {
|
|
||||||
self.selected_index
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_selected_index(
|
|
||||||
&mut self,
|
|
||||||
ix: usize,
|
|
||||||
_window: &mut Window,
|
|
||||||
_cx: &mut Context<Picker<Self>>,
|
|
||||||
) {
|
|
||||||
self.selected_index = ix;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
|
||||||
"Search files & directories…".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_matches(
|
|
||||||
&mut self,
|
|
||||||
query: String,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Picker<Self>>,
|
|
||||||
) -> Task<()> {
|
|
||||||
let Some(workspace) = self.workspace.upgrade() else {
|
|
||||||
return Task::ready(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let search_task = search_files(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
// TODO: This should be probably be run in the background.
|
|
||||||
let paths = search_task.await;
|
|
||||||
|
|
||||||
this.update(cx, |this, _cx| {
|
|
||||||
this.delegate.matches = paths;
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
|
||||||
let Some(FileMatch { mat, .. }) = self.matches.get(self.selected_index) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let project_path = ProjectPath {
|
|
||||||
worktree_id: WorktreeId::from_usize(mat.worktree_id),
|
|
||||||
path: mat.path.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let is_directory = mat.is_dir;
|
|
||||||
|
|
||||||
self.context_store
|
|
||||||
.update(cx, |context_store, cx| {
|
|
||||||
if is_directory {
|
|
||||||
context_store
|
|
||||||
.add_directory(&project_path, true, cx)
|
|
||||||
.log_err();
|
|
||||||
} else {
|
|
||||||
context_store
|
|
||||||
.add_file_from_path(project_path.clone(), true, cx)
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
|
||||||
self.context_picker
|
|
||||||
.update(cx, |_, cx| {
|
|
||||||
cx.emit(DismissEvent);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_match(
|
|
||||||
&self,
|
|
||||||
ix: usize,
|
|
||||||
selected: bool,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Picker<Self>>,
|
|
||||||
) -> Option<Self::ListItem> {
|
|
||||||
let FileMatch { mat, .. } = &self.matches.get(ix)?;
|
|
||||||
let workspace = self.workspace.upgrade()?;
|
|
||||||
let path_style = workspace.read(cx).path_style(cx);
|
|
||||||
|
|
||||||
Some(
|
|
||||||
ListItem::new(ix)
|
|
||||||
.inset(true)
|
|
||||||
.toggle_state(selected)
|
|
||||||
.child(render_file_context_entry(
|
|
||||||
ElementId::named_usize("file-ctx-picker", ix),
|
|
||||||
WorktreeId::from_usize(mat.worktree_id),
|
|
||||||
&mat.path,
|
|
||||||
&mat.path_prefix,
|
|
||||||
mat.is_dir,
|
|
||||||
path_style,
|
|
||||||
self.context_store.clone(),
|
|
||||||
cx,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FileMatch {
|
|
||||||
pub mat: PathMatch,
|
|
||||||
pub is_recent: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn search_files(
|
|
||||||
query: String,
|
|
||||||
cancellation_flag: Arc<AtomicBool>,
|
|
||||||
workspace: &Entity<Workspace>,
|
|
||||||
cx: &App,
|
|
||||||
) -> Task<Vec<FileMatch>> {
|
|
||||||
if query.is_empty() {
|
|
||||||
let workspace = workspace.read(cx);
|
|
||||||
let project = workspace.project().read(cx);
|
|
||||||
let visible_worktrees = workspace.visible_worktrees(cx).collect::<Vec<_>>();
|
|
||||||
let include_root_name = visible_worktrees.len() > 1;
|
|
||||||
|
|
||||||
let recent_matches = workspace
|
|
||||||
.recent_navigation_history(Some(10), cx)
|
|
||||||
.into_iter()
|
|
||||||
.map(|(project_path, _)| {
|
|
||||||
let path_prefix = if include_root_name {
|
|
||||||
project
|
|
||||||
.worktree_for_id(project_path.worktree_id, cx)
|
|
||||||
.map(|wt| wt.read(cx).root_name().into())
|
|
||||||
.unwrap_or_else(|| RelPath::empty().into())
|
|
||||||
} else {
|
|
||||||
RelPath::empty().into()
|
|
||||||
};
|
|
||||||
|
|
||||||
FileMatch {
|
|
||||||
mat: PathMatch {
|
|
||||||
score: 0.,
|
|
||||||
positions: Vec::new(),
|
|
||||||
worktree_id: project_path.worktree_id.to_usize(),
|
|
||||||
path: project_path.path,
|
|
||||||
path_prefix,
|
|
||||||
distance_to_relative_ancestor: 0,
|
|
||||||
is_dir: false,
|
|
||||||
},
|
|
||||||
is_recent: true,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let file_matches = visible_worktrees.into_iter().flat_map(|worktree| {
|
|
||||||
let worktree = worktree.read(cx);
|
|
||||||
let path_prefix: Arc<RelPath> = if include_root_name {
|
|
||||||
worktree.root_name().into()
|
|
||||||
} else {
|
|
||||||
RelPath::empty().into()
|
|
||||||
};
|
|
||||||
worktree.entries(false, 0).map(move |entry| FileMatch {
|
|
||||||
mat: PathMatch {
|
|
||||||
score: 0.,
|
|
||||||
positions: Vec::new(),
|
|
||||||
worktree_id: worktree.id().to_usize(),
|
|
||||||
path: entry.path.clone(),
|
|
||||||
path_prefix: path_prefix.clone(),
|
|
||||||
distance_to_relative_ancestor: 0,
|
|
||||||
is_dir: entry.is_dir(),
|
|
||||||
},
|
|
||||||
is_recent: false,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
Task::ready(recent_matches.chain(file_matches).collect())
|
|
||||||
} else {
|
|
||||||
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
|
||||||
let include_root_name = worktrees.len() > 1;
|
|
||||||
let candidate_sets = worktrees
|
|
||||||
.into_iter()
|
|
||||||
.map(|worktree| {
|
|
||||||
let worktree = worktree.read(cx);
|
|
||||||
|
|
||||||
PathMatchCandidateSet {
|
|
||||||
snapshot: worktree.snapshot(),
|
|
||||||
include_ignored: worktree.root_entry().is_some_and(|entry| entry.is_ignored),
|
|
||||||
include_root_name,
|
|
||||||
candidates: project::Candidates::Entries,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let executor = cx.background_executor().clone();
|
|
||||||
cx.foreground_executor().spawn(async move {
|
|
||||||
fuzzy::match_path_sets(
|
|
||||||
candidate_sets.as_slice(),
|
|
||||||
query.as_str(),
|
|
||||||
&None,
|
|
||||||
false,
|
|
||||||
100,
|
|
||||||
&cancellation_flag,
|
|
||||||
executor,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.map(|mat| FileMatch {
|
|
||||||
mat,
|
|
||||||
is_recent: false,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extract_file_name_and_directory(
|
|
||||||
path: &RelPath,
|
|
||||||
path_prefix: &RelPath,
|
|
||||||
path_style: PathStyle,
|
|
||||||
) -> (SharedString, Option<SharedString>) {
|
|
||||||
// If path is empty, this means we're matching with the root directory itself
|
|
||||||
// so we use the path_prefix as the name
|
|
||||||
if path.is_empty() && !path_prefix.is_empty() {
|
|
||||||
return (path_prefix.display(path_style).to_string().into(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let full_path = path_prefix.join(path);
|
|
||||||
let file_name = full_path.file_name().unwrap_or_default();
|
|
||||||
let display_path = full_path.display(path_style);
|
|
||||||
let (directory, file_name) = display_path.split_at(display_path.len() - file_name.len());
|
|
||||||
(
|
|
||||||
file_name.to_string().into(),
|
|
||||||
Some(SharedString::new(directory)).filter(|dir| !dir.is_empty()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_file_context_entry(
|
|
||||||
id: ElementId,
|
|
||||||
worktree_id: WorktreeId,
|
|
||||||
path: &Arc<RelPath>,
|
|
||||||
path_prefix: &Arc<RelPath>,
|
|
||||||
is_directory: bool,
|
|
||||||
path_style: PathStyle,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
|
||||||
cx: &App,
|
|
||||||
) -> Stateful<Div> {
|
|
||||||
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix, path_style);
|
|
||||||
|
|
||||||
let added = context_store.upgrade().and_then(|context_store| {
|
|
||||||
let project_path = ProjectPath {
|
|
||||||
worktree_id,
|
|
||||||
path: path.clone(),
|
|
||||||
};
|
|
||||||
if is_directory {
|
|
||||||
context_store
|
|
||||||
.read(cx)
|
|
||||||
.path_included_in_directory(&project_path, cx)
|
|
||||||
} else {
|
|
||||||
context_store.read(cx).file_path_included(&project_path, cx)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let file_icon = if is_directory {
|
|
||||||
FileIcons::get_folder_icon(false, path.as_std_path(), cx)
|
|
||||||
} else {
|
|
||||||
FileIcons::get_icon(path.as_std_path(), cx)
|
|
||||||
}
|
|
||||||
.map(Icon::from_path)
|
|
||||||
.unwrap_or_else(|| Icon::new(IconName::File));
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.id(id)
|
|
||||||
.gap_1p5()
|
|
||||||
.w_full()
|
|
||||||
.child(file_icon.size(IconSize::Small).color(Color::Muted))
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(Label::new(file_name))
|
|
||||||
.children(directory.map(|directory| {
|
|
||||||
Label::new(directory)
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.color(Color::Muted)
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.when_some(added, |el, added| match added {
|
|
||||||
FileInclusion::Direct => el.child(
|
|
||||||
h_flex()
|
|
||||||
.w_full()
|
|
||||||
.justify_end()
|
|
||||||
.gap_0p5()
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::Check)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.color(Color::Success),
|
|
||||||
)
|
|
||||||
.child(Label::new("Added").size(LabelSize::Small)),
|
|
||||||
),
|
|
||||||
FileInclusion::InDirectory { full_path } => {
|
|
||||||
let directory_full_path = full_path.to_string_lossy().into_owned();
|
|
||||||
|
|
||||||
el.child(
|
|
||||||
h_flex()
|
|
||||||
.w_full()
|
|
||||||
.justify_end()
|
|
||||||
.gap_0p5()
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::Check)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.color(Color::Success),
|
|
||||||
)
|
|
||||||
.child(Label::new("Included").size(LabelSize::Small)),
|
|
||||||
)
|
|
||||||
.tooltip(Tooltip::text(format!("in {directory_full_path}")))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
|
|
||||||
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
|
||||||
use picker::{Picker, PickerDelegate};
|
|
||||||
use prompt_store::{PromptId, PromptStore, UserPromptId};
|
|
||||||
use ui::{ListItem, prelude::*};
|
|
||||||
use util::ResultExt as _;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
context::RULES_ICON,
|
|
||||||
context_picker::ContextPicker,
|
|
||||||
context_store::{self, ContextStore},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct RulesContextPicker {
|
|
||||||
picker: Entity<Picker<RulesContextPickerDelegate>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RulesContextPicker {
|
|
||||||
pub fn new(
|
|
||||||
prompt_store: WeakEntity<PromptStore>,
|
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
|
||||||
context_store: WeakEntity<context_store::ContextStore>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let delegate = RulesContextPickerDelegate::new(prompt_store, context_picker, context_store);
|
|
||||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
|
||||||
|
|
||||||
RulesContextPicker { picker }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Focusable for RulesContextPicker {
|
|
||||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
|
||||||
self.picker.focus_handle(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for RulesContextPicker {
|
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
self.picker.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct RulesContextEntry {
|
|
||||||
pub prompt_id: UserPromptId,
|
|
||||||
pub title: SharedString,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RulesContextPickerDelegate {
|
|
||||||
prompt_store: WeakEntity<PromptStore>,
|
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
|
||||||
context_store: WeakEntity<context_store::ContextStore>,
|
|
||||||
matches: Vec<RulesContextEntry>,
|
|
||||||
selected_index: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RulesContextPickerDelegate {
|
|
||||||
pub fn new(
|
|
||||||
prompt_store: WeakEntity<PromptStore>,
|
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
|
||||||
context_store: WeakEntity<context_store::ContextStore>,
|
|
||||||
) -> Self {
|
|
||||||
RulesContextPickerDelegate {
|
|
||||||
prompt_store,
|
|
||||||
context_picker,
|
|
||||||
context_store,
|
|
||||||
matches: Vec::new(),
|
|
||||||
selected_index: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PickerDelegate for RulesContextPickerDelegate {
|
|
||||||
type ListItem = ListItem;
|
|
||||||
|
|
||||||
fn match_count(&self) -> usize {
|
|
||||||
self.matches.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn selected_index(&self) -> usize {
|
|
||||||
self.selected_index
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_selected_index(
|
|
||||||
&mut self,
|
|
||||||
ix: usize,
|
|
||||||
_window: &mut Window,
|
|
||||||
_cx: &mut Context<Picker<Self>>,
|
|
||||||
) {
|
|
||||||
self.selected_index = ix;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
|
||||||
"Search available rules…".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_matches(
|
|
||||||
&mut self,
|
|
||||||
query: String,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Picker<Self>>,
|
|
||||||
) -> Task<()> {
|
|
||||||
let Some(prompt_store) = self.prompt_store.upgrade() else {
|
|
||||||
return Task::ready(());
|
|
||||||
};
|
|
||||||
let search_task = search_rules(query, Arc::new(AtomicBool::default()), &prompt_store, cx);
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
let matches = search_task.await;
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.delegate.matches = matches;
|
|
||||||
this.delegate.selected_index = 0;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
|
||||||
let Some(entry) = self.matches.get(self.selected_index) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.context_store
|
|
||||||
.update(cx, |context_store, cx| {
|
|
||||||
context_store.add_rules(entry.prompt_id, true, cx)
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
|
||||||
self.context_picker
|
|
||||||
.update(cx, |_, cx| {
|
|
||||||
cx.emit(DismissEvent);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_match(
|
|
||||||
&self,
|
|
||||||
ix: usize,
|
|
||||||
selected: bool,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Picker<Self>>,
|
|
||||||
) -> Option<Self::ListItem> {
|
|
||||||
let thread = &self.matches.get(ix)?;
|
|
||||||
|
|
||||||
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
|
|
||||||
render_thread_context_entry(thread, self.context_store.clone(), cx),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_thread_context_entry(
|
|
||||||
user_rules: &RulesContextEntry,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Div {
|
|
||||||
let added = context_store.upgrade().is_some_and(|context_store| {
|
|
||||||
context_store
|
|
||||||
.read(cx)
|
|
||||||
.includes_user_rules(user_rules.prompt_id)
|
|
||||||
});
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.w_full()
|
|
||||||
.justify_between()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.max_w_72()
|
|
||||||
.child(
|
|
||||||
Icon::new(RULES_ICON)
|
|
||||||
.size(IconSize::XSmall)
|
|
||||||
.color(Color::Muted),
|
|
||||||
)
|
|
||||||
.child(Label::new(user_rules.title.clone()).truncate()),
|
|
||||||
)
|
|
||||||
.when(added, |el| {
|
|
||||||
el.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::Check)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.color(Color::Success),
|
|
||||||
)
|
|
||||||
.child(Label::new("Added").size(LabelSize::Small)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn search_rules(
|
|
||||||
query: String,
|
|
||||||
cancellation_flag: Arc<AtomicBool>,
|
|
||||||
prompt_store: &Entity<PromptStore>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Task<Vec<RulesContextEntry>> {
|
|
||||||
let search_task = prompt_store.read(cx).search(query, cancellation_flag, cx);
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
search_task
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|metadata| {
|
|
||||||
// Default prompts are filtered out as they are automatically included.
|
|
||||||
if metadata.default {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
match metadata.id {
|
|
||||||
PromptId::EditWorkflow => None,
|
|
||||||
PromptId::User { uuid } => Some(RulesContextEntry {
|
|
||||||
prompt_id: uuid,
|
|
||||||
title: metadata.title?,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,415 +0,0 @@
|
|||||||
use std::cmp::Reverse;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
|
||||||
use gpui::{
|
|
||||||
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
|
|
||||||
};
|
|
||||||
use ordered_float::OrderedFloat;
|
|
||||||
use picker::{Picker, PickerDelegate};
|
|
||||||
use project::lsp_store::SymbolLocation;
|
|
||||||
use project::{DocumentSymbol, Symbol};
|
|
||||||
use ui::{ListItem, prelude::*};
|
|
||||||
use util::ResultExt as _;
|
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
context::AgentContextHandle, context_picker::ContextPicker, context_store::ContextStore,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct SymbolContextPicker {
|
|
||||||
picker: Entity<Picker<SymbolContextPickerDelegate>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SymbolContextPicker {
|
|
||||||
pub fn new(
|
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let delegate = SymbolContextPickerDelegate::new(context_picker, workspace, context_store);
|
|
||||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
|
||||||
|
|
||||||
Self { picker }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Focusable for SymbolContextPicker {
|
|
||||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
|
||||||
self.picker.focus_handle(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for SymbolContextPicker {
|
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
self.picker.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SymbolContextPickerDelegate {
|
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
|
||||||
matches: Vec<SymbolEntry>,
|
|
||||||
selected_index: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SymbolContextPickerDelegate {
|
|
||||||
pub fn new(
|
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
context_picker,
|
|
||||||
workspace,
|
|
||||||
context_store,
|
|
||||||
matches: Vec::new(),
|
|
||||||
selected_index: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PickerDelegate for SymbolContextPickerDelegate {
|
|
||||||
type ListItem = ListItem;
|
|
||||||
|
|
||||||
fn match_count(&self) -> usize {
|
|
||||||
self.matches.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn selected_index(&self) -> usize {
|
|
||||||
self.selected_index
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_selected_index(
|
|
||||||
&mut self,
|
|
||||||
ix: usize,
|
|
||||||
_window: &mut Window,
|
|
||||||
_cx: &mut Context<Picker<Self>>,
|
|
||||||
) {
|
|
||||||
self.selected_index = ix;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
|
||||||
"Search symbols…".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_matches(
|
|
||||||
&mut self,
|
|
||||||
query: String,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Picker<Self>>,
|
|
||||||
) -> Task<()> {
|
|
||||||
let Some(workspace) = self.workspace.upgrade() else {
|
|
||||||
return Task::ready(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
|
||||||
let context_store = self.context_store.clone();
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
let symbols = search_task.await;
|
|
||||||
|
|
||||||
let symbol_entries = context_store
|
|
||||||
.read_with(cx, |context_store, cx| {
|
|
||||||
compute_symbol_entries(symbols, context_store, cx)
|
|
||||||
})
|
|
||||||
.log_err()
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
this.update(cx, |this, _cx| {
|
|
||||||
this.delegate.matches = symbol_entries;
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
|
||||||
let Some(mat) = self.matches.get(self.selected_index) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(workspace) = self.workspace.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let add_symbol_task = add_symbol(
|
|
||||||
mat.symbol.clone(),
|
|
||||||
true,
|
|
||||||
workspace,
|
|
||||||
self.context_store.clone(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
let selected_index = self.selected_index;
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
let (_, included) = add_symbol_task.await?;
|
|
||||||
this.update(cx, |this, _| {
|
|
||||||
if let Some(mat) = this.delegate.matches.get_mut(selected_index) {
|
|
||||||
mat.is_included = included;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
|
||||||
self.context_picker
|
|
||||||
.update(cx, |_, cx| {
|
|
||||||
cx.emit(DismissEvent);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_match(
|
|
||||||
&self,
|
|
||||||
ix: usize,
|
|
||||||
selected: bool,
|
|
||||||
_window: &mut Window,
|
|
||||||
_: &mut Context<Picker<Self>>,
|
|
||||||
) -> Option<Self::ListItem> {
|
|
||||||
let mat = &self.matches.get(ix)?;
|
|
||||||
|
|
||||||
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
|
|
||||||
render_symbol_context_entry(ElementId::named_usize("symbol-ctx-picker", ix), mat),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct SymbolEntry {
|
|
||||||
pub symbol: Symbol,
|
|
||||||
pub is_included: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn add_symbol(
|
|
||||||
symbol: Symbol,
|
|
||||||
remove_if_exists: bool,
|
|
||||||
workspace: Entity<Workspace>,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Task<Result<(Option<AgentContextHandle>, bool)>> {
|
|
||||||
let project = workspace.read(cx).project().clone();
|
|
||||||
let open_buffer_task = project.update(cx, |project, cx| {
|
|
||||||
let SymbolLocation::InProject(symbol_path) = &symbol.path else {
|
|
||||||
return Task::ready(Err(anyhow!("can't add symbol from outside of project")));
|
|
||||||
};
|
|
||||||
project.open_buffer(symbol_path.clone(), cx)
|
|
||||||
});
|
|
||||||
cx.spawn(async move |cx| {
|
|
||||||
let buffer = open_buffer_task.await?;
|
|
||||||
let document_symbols = project
|
|
||||||
.update(cx, |project, cx| project.document_symbols(&buffer, cx))?
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Try to find a matching document symbol. Document symbols include
|
|
||||||
// not only the symbol itself (e.g. function name), but they also
|
|
||||||
// include the context that they contain (e.g. function body).
|
|
||||||
let (name, range, enclosing_range) = if let Some(DocumentSymbol {
|
|
||||||
name,
|
|
||||||
range,
|
|
||||||
selection_range,
|
|
||||||
..
|
|
||||||
}) =
|
|
||||||
find_matching_symbol(&symbol, document_symbols.as_slice())
|
|
||||||
{
|
|
||||||
(name, selection_range, range)
|
|
||||||
} else {
|
|
||||||
// If we do not find a matching document symbol, fall back to
|
|
||||||
// just the symbol itself
|
|
||||||
(symbol.name, symbol.range.clone(), symbol.range)
|
|
||||||
};
|
|
||||||
|
|
||||||
let (range, enclosing_range) = buffer.read_with(cx, |buffer, _| {
|
|
||||||
(
|
|
||||||
buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
|
|
||||||
buffer.anchor_after(enclosing_range.start)
|
|
||||||
..buffer.anchor_before(enclosing_range.end),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
context_store.update(cx, move |context_store, cx| {
|
|
||||||
context_store.add_symbol(
|
|
||||||
buffer,
|
|
||||||
name.into(),
|
|
||||||
range,
|
|
||||||
enclosing_range,
|
|
||||||
remove_if_exists,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Option<DocumentSymbol> {
|
|
||||||
let mut candidates = candidates.iter();
|
|
||||||
let mut candidate = candidates.next()?;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if candidate.range.start > symbol.range.end {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if candidate.range.end < symbol.range.start {
|
|
||||||
candidate = candidates.next()?;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if candidate.selection_range == symbol.range {
|
|
||||||
return Some(candidate.clone());
|
|
||||||
}
|
|
||||||
if candidate.range.start <= symbol.range.start && symbol.range.end <= candidate.range.end {
|
|
||||||
candidates = candidate.children.iter();
|
|
||||||
candidate = candidates.next()?;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SymbolMatch {
|
|
||||||
pub symbol: Symbol,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn search_symbols(
|
|
||||||
query: String,
|
|
||||||
cancellation_flag: Arc<AtomicBool>,
|
|
||||||
workspace: &Entity<Workspace>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Task<Vec<SymbolMatch>> {
|
|
||||||
let symbols_task = workspace.update(cx, |workspace, cx| {
|
|
||||||
workspace
|
|
||||||
.project()
|
|
||||||
.update(cx, |project, cx| project.symbols(&query, cx))
|
|
||||||
});
|
|
||||||
let project = workspace.read(cx).project().clone();
|
|
||||||
cx.spawn(async move |cx| {
|
|
||||||
let Some(symbols) = symbols_task.await.log_err() else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> =
|
|
||||||
project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
symbols
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(id, symbol)| {
|
|
||||||
StringMatchCandidate::new(id, symbol.label.filter_text())
|
|
||||||
})
|
|
||||||
.partition(|candidate| match &symbols[candidate.id].path {
|
|
||||||
SymbolLocation::InProject(project_path) => project
|
|
||||||
.entry_for_path(project_path, cx)
|
|
||||||
.is_some_and(|e| !e.is_ignored),
|
|
||||||
SymbolLocation::OutsideProject { .. } => false,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.log_err()
|
|
||||||
else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
const MAX_MATCHES: usize = 100;
|
|
||||||
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
|
|
||||||
&visible_match_candidates,
|
|
||||||
&query,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
MAX_MATCHES,
|
|
||||||
&cancellation_flag,
|
|
||||||
cx.background_executor().clone(),
|
|
||||||
));
|
|
||||||
let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
|
|
||||||
&external_match_candidates,
|
|
||||||
&query,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
|
|
||||||
&cancellation_flag,
|
|
||||||
cx.background_executor().clone(),
|
|
||||||
));
|
|
||||||
let sort_key_for_match = |mat: &StringMatch| {
|
|
||||||
let symbol = &symbols[mat.candidate_id];
|
|
||||||
(Reverse(OrderedFloat(mat.score)), symbol.label.filter_text())
|
|
||||||
};
|
|
||||||
|
|
||||||
visible_matches.sort_unstable_by_key(sort_key_for_match);
|
|
||||||
external_matches.sort_unstable_by_key(sort_key_for_match);
|
|
||||||
let mut matches = visible_matches;
|
|
||||||
matches.append(&mut external_matches);
|
|
||||||
|
|
||||||
matches
|
|
||||||
.into_iter()
|
|
||||||
.map(|mut mat| {
|
|
||||||
let symbol = symbols[mat.candidate_id].clone();
|
|
||||||
let filter_start = symbol.label.filter_range.start;
|
|
||||||
for position in &mut mat.positions {
|
|
||||||
*position += filter_start;
|
|
||||||
}
|
|
||||||
SymbolMatch { symbol }
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_symbol_entries(
|
|
||||||
symbols: Vec<SymbolMatch>,
|
|
||||||
context_store: &ContextStore,
|
|
||||||
cx: &App,
|
|
||||||
) -> Vec<SymbolEntry> {
|
|
||||||
symbols
|
|
||||||
.into_iter()
|
|
||||||
.map(|SymbolMatch { symbol, .. }| SymbolEntry {
|
|
||||||
is_included: context_store.includes_symbol(&symbol, cx),
|
|
||||||
symbol,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
|
|
||||||
let path = match &entry.symbol.path {
|
|
||||||
SymbolLocation::InProject(project_path) => {
|
|
||||||
project_path.path.file_name().unwrap_or_default().into()
|
|
||||||
}
|
|
||||||
SymbolLocation::OutsideProject {
|
|
||||||
abs_path,
|
|
||||||
signature: _,
|
|
||||||
} => abs_path
|
|
||||||
.file_name()
|
|
||||||
.map(|f| f.to_string_lossy())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
};
|
|
||||||
let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.id(id)
|
|
||||||
.gap_1p5()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::Code)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.color(Color::Muted),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(Label::new(&entry.symbol.name))
|
|
||||||
.child(
|
|
||||||
Label::new(symbol_location)
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.color(Color::Muted),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.when(entry.is_included, |el| {
|
|
||||||
el.child(
|
|
||||||
h_flex()
|
|
||||||
.w_full()
|
|
||||||
.justify_end()
|
|
||||||
.gap_0p5()
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::Check)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.color(Color::Success),
|
|
||||||
)
|
|
||||||
.child(Label::new("Added").size(LabelSize::Small)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
context_picker::ContextPicker,
|
|
||||||
context_store::{self, ContextStore},
|
|
||||||
};
|
|
||||||
use agent::{HistoryEntry, HistoryStore};
|
|
||||||
use fuzzy::StringMatchCandidate;
|
|
||||||
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
|
||||||
use picker::{Picker, PickerDelegate};
|
|
||||||
use ui::{ListItem, prelude::*};
|
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
pub struct ThreadContextPicker {
|
|
||||||
picker: Entity<Picker<ThreadContextPickerDelegate>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ThreadContextPicker {
|
|
||||||
pub fn new(
|
|
||||||
thread_store: WeakEntity<HistoryStore>,
|
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
|
||||||
context_store: WeakEntity<context_store::ContextStore>,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let delegate = ThreadContextPickerDelegate::new(
|
|
||||||
thread_store,
|
|
||||||
context_picker,
|
|
||||||
context_store,
|
|
||||||
workspace,
|
|
||||||
);
|
|
||||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
|
||||||
|
|
||||||
ThreadContextPicker { picker }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Focusable for ThreadContextPicker {
|
|
||||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
|
||||||
self.picker.focus_handle(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ThreadContextPicker {
|
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
self.picker.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ThreadContextPickerDelegate {
|
|
||||||
thread_store: WeakEntity<HistoryStore>,
|
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
|
||||||
context_store: WeakEntity<context_store::ContextStore>,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
matches: Vec<HistoryEntry>,
|
|
||||||
selected_index: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ThreadContextPickerDelegate {
|
|
||||||
pub fn new(
|
|
||||||
thread_store: WeakEntity<HistoryStore>,
|
|
||||||
context_picker: WeakEntity<ContextPicker>,
|
|
||||||
context_store: WeakEntity<context_store::ContextStore>,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
) -> Self {
|
|
||||||
ThreadContextPickerDelegate {
|
|
||||||
thread_store,
|
|
||||||
context_picker,
|
|
||||||
context_store,
|
|
||||||
workspace,
|
|
||||||
matches: Vec::new(),
|
|
||||||
selected_index: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PickerDelegate for ThreadContextPickerDelegate {
|
|
||||||
type ListItem = ListItem;
|
|
||||||
|
|
||||||
fn match_count(&self) -> usize {
|
|
||||||
self.matches.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn selected_index(&self) -> usize {
|
|
||||||
self.selected_index
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_selected_index(
|
|
||||||
&mut self,
|
|
||||||
ix: usize,
|
|
||||||
_window: &mut Window,
|
|
||||||
_cx: &mut Context<Picker<Self>>,
|
|
||||||
) {
|
|
||||||
self.selected_index = ix;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
|
||||||
"Search threads…".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_matches(
|
|
||||||
&mut self,
|
|
||||||
query: String,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Picker<Self>>,
|
|
||||||
) -> Task<()> {
|
|
||||||
let Some(thread_store) = self.thread_store.upgrade() else {
|
|
||||||
return Task::ready(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let search_task = search_threads(query, Arc::new(AtomicBool::default()), &thread_store, cx);
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
let matches = search_task.await;
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.delegate.matches = matches;
|
|
||||||
this.delegate.selected_index = 0;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
|
||||||
let Some(project) = self
|
|
||||||
.workspace
|
|
||||||
.upgrade()
|
|
||||||
.map(|w| w.read(cx).project().clone())
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some((entry, thread_store)) = self
|
|
||||||
.matches
|
|
||||||
.get(self.selected_index)
|
|
||||||
.zip(self.thread_store.upgrade())
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
match entry {
|
|
||||||
HistoryEntry::AcpThread(thread) => {
|
|
||||||
let load_thread_task =
|
|
||||||
agent::load_agent_thread(thread.id.clone(), thread_store, project, cx);
|
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
let thread = load_thread_task.await?;
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.delegate
|
|
||||||
.context_store
|
|
||||||
.update(cx, |context_store, cx| {
|
|
||||||
context_store.add_thread(thread, true, cx)
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
HistoryEntry::TextThread(thread) => {
|
|
||||||
let task = thread_store.update(cx, |this, cx| {
|
|
||||||
this.load_text_thread(thread.path.clone(), cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
let thread = task.await?;
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.delegate
|
|
||||||
.context_store
|
|
||||||
.update(cx, |context_store, cx| {
|
|
||||||
context_store.add_text_thread(thread, true, cx)
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
|
||||||
self.context_picker
|
|
||||||
.update(cx, |_, cx| {
|
|
||||||
cx.emit(DismissEvent);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_match(
|
|
||||||
&self,
|
|
||||||
ix: usize,
|
|
||||||
selected: bool,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Picker<Self>>,
|
|
||||||
) -> Option<Self::ListItem> {
|
|
||||||
let thread = &self.matches.get(ix)?;
|
|
||||||
|
|
||||||
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
|
|
||||||
render_thread_context_entry(thread, self.context_store.clone(), cx),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_thread_context_entry(
|
|
||||||
entry: &HistoryEntry,
|
|
||||||
context_store: WeakEntity<ContextStore>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Div {
|
|
||||||
let is_added = match entry {
|
|
||||||
HistoryEntry::AcpThread(thread) => context_store
|
|
||||||
.upgrade()
|
|
||||||
.is_some_and(|ctx_store| ctx_store.read(cx).includes_thread(&thread.id)),
|
|
||||||
HistoryEntry::TextThread(thread) => context_store
|
|
||||||
.upgrade()
|
|
||||||
.is_some_and(|ctx_store| ctx_store.read(cx).includes_text_thread(&thread.path)),
|
|
||||||
};
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.w_full()
|
|
||||||
.justify_between()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.max_w_72()
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::Thread)
|
|
||||||
.size(IconSize::XSmall)
|
|
||||||
.color(Color::Muted),
|
|
||||||
)
|
|
||||||
.child(Label::new(entry.title().clone()).truncate()),
|
|
||||||
)
|
|
||||||
.when(is_added, |el| {
|
|
||||||
el.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::Check)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.color(Color::Success),
|
|
||||||
)
|
|
||||||
.child(Label::new("Added").size(LabelSize::Small)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn search_threads(
|
|
||||||
query: String,
|
|
||||||
cancellation_flag: Arc<AtomicBool>,
|
|
||||||
thread_store: &Entity<HistoryStore>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Task<Vec<HistoryEntry>> {
|
|
||||||
let threads = thread_store.read(cx).entries().collect();
|
|
||||||
if query.is_empty() {
|
|
||||||
return Task::ready(threads);
|
|
||||||
}
|
|
||||||
|
|
||||||
let executor = cx.background_executor().clone();
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let candidates = threads
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(id, thread)| StringMatchCandidate::new(id, thread.title()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let matches = fuzzy::match_strings(
|
|
||||||
&candidates,
|
|
||||||
&query,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
100,
|
|
||||||
&cancellation_flag,
|
|
||||||
executor,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
matches
|
|
||||||
.into_iter()
|
|
||||||
.map(|mat| threads[mat.candidate_id].clone())
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,614 +0,0 @@
|
|||||||
use crate::context::{
|
|
||||||
AgentContextHandle, AgentContextKey, ContextId, ContextKind, DirectoryContextHandle,
|
|
||||||
FetchedUrlContext, FileContextHandle, ImageContext, RulesContextHandle, SelectionContextHandle,
|
|
||||||
SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
|
|
||||||
};
|
|
||||||
use agent_client_protocol as acp;
|
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
|
||||||
use assistant_text_thread::TextThread;
|
|
||||||
use collections::{HashSet, IndexSet};
|
|
||||||
use futures::{self, FutureExt};
|
|
||||||
use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
|
|
||||||
use language::{Buffer, File as _};
|
|
||||||
use language_model::LanguageModelImage;
|
|
||||||
use project::{
|
|
||||||
Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file,
|
|
||||||
lsp_store::SymbolLocation,
|
|
||||||
};
|
|
||||||
use prompt_store::UserPromptId;
|
|
||||||
use ref_cast::RefCast as _;
|
|
||||||
use std::{
|
|
||||||
ops::Range,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
use text::{Anchor, OffsetRangeExt};
|
|
||||||
|
|
||||||
pub struct ContextStore {
|
|
||||||
project: WeakEntity<Project>,
|
|
||||||
next_context_id: ContextId,
|
|
||||||
context_set: IndexSet<AgentContextKey>,
|
|
||||||
context_thread_ids: HashSet<acp::SessionId>,
|
|
||||||
context_text_thread_paths: HashSet<Arc<Path>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ContextStoreEvent {
|
|
||||||
ContextRemoved(AgentContextKey),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<ContextStoreEvent> for ContextStore {}
|
|
||||||
|
|
||||||
impl ContextStore {
|
|
||||||
pub fn new(project: WeakEntity<Project>) -> Self {
|
|
||||||
Self {
|
|
||||||
project,
|
|
||||||
next_context_id: ContextId::zero(),
|
|
||||||
context_set: IndexSet::default(),
|
|
||||||
context_thread_ids: HashSet::default(),
|
|
||||||
context_text_thread_paths: HashSet::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn context(&self) -> impl Iterator<Item = &AgentContextHandle> {
|
|
||||||
self.context_set.iter().map(|entry| entry.as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
|
||||||
self.context_set.clear();
|
|
||||||
self.context_thread_ids.clear();
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_file_from_path(
|
|
||||||
&mut self,
|
|
||||||
project_path: ProjectPath,
|
|
||||||
remove_if_exists: bool,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Task<Result<Option<AgentContextHandle>>> {
|
|
||||||
let Some(project) = self.project.upgrade() else {
|
|
||||||
return Task::ready(Err(anyhow!("failed to read project")));
|
|
||||||
};
|
|
||||||
|
|
||||||
if is_image_file(&project, &project_path, cx) {
|
|
||||||
self.add_image_from_path(project_path, remove_if_exists, cx)
|
|
||||||
} else {
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
let open_buffer_task = project.update(cx, |project, cx| {
|
|
||||||
project.open_buffer(project_path.clone(), cx)
|
|
||||||
})?;
|
|
||||||
let buffer = open_buffer_task.await?;
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.add_file_from_buffer(&project_path, buffer, remove_if_exists, cx)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_file_from_buffer(
|
|
||||||
&mut self,
|
|
||||||
project_path: &ProjectPath,
|
|
||||||
buffer: Entity<Buffer>,
|
|
||||||
remove_if_exists: bool,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Option<AgentContextHandle> {
|
|
||||||
let context_id = self.next_context_id.post_inc();
|
|
||||||
let context = AgentContextHandle::File(FileContextHandle { buffer, context_id });
|
|
||||||
|
|
||||||
if let Some(key) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
|
|
||||||
if remove_if_exists {
|
|
||||||
self.remove_context(&context, cx);
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(key.as_ref().clone())
|
|
||||||
}
|
|
||||||
} else if self.path_included_in_directory(project_path, cx).is_some() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
self.insert_context(context.clone(), cx);
|
|
||||||
Some(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_directory(
|
|
||||||
&mut self,
|
|
||||||
project_path: &ProjectPath,
|
|
||||||
remove_if_exists: bool,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Result<Option<AgentContextHandle>> {
|
|
||||||
let project = self.project.upgrade().context("failed to read project")?;
|
|
||||||
let entry_id = project
|
|
||||||
.read(cx)
|
|
||||||
.entry_for_path(project_path, cx)
|
|
||||||
.map(|entry| entry.id)
|
|
||||||
.context("no entry found for directory context")?;
|
|
||||||
|
|
||||||
let context_id = self.next_context_id.post_inc();
|
|
||||||
let context = AgentContextHandle::Directory(DirectoryContextHandle {
|
|
||||||
entry_id,
|
|
||||||
context_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
let context =
|
|
||||||
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
|
|
||||||
if remove_if_exists {
|
|
||||||
self.remove_context(&context, cx);
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(existing.as_ref().clone())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.insert_context(context.clone(), cx);
|
|
||||||
Some(context)
|
|
||||||
};
|
|
||||||
|
|
||||||
anyhow::Ok(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_symbol(
|
|
||||||
&mut self,
|
|
||||||
buffer: Entity<Buffer>,
|
|
||||||
symbol: SharedString,
|
|
||||||
range: Range<Anchor>,
|
|
||||||
enclosing_range: Range<Anchor>,
|
|
||||||
remove_if_exists: bool,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> (Option<AgentContextHandle>, bool) {
|
|
||||||
let context_id = self.next_context_id.post_inc();
|
|
||||||
let context = AgentContextHandle::Symbol(SymbolContextHandle {
|
|
||||||
buffer,
|
|
||||||
symbol,
|
|
||||||
range,
|
|
||||||
enclosing_range,
|
|
||||||
context_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(key) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
|
|
||||||
let handle = if remove_if_exists {
|
|
||||||
self.remove_context(&context, cx);
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(key.as_ref().clone())
|
|
||||||
};
|
|
||||||
return (handle, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
let included = self.insert_context(context.clone(), cx);
|
|
||||||
(Some(context), included)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_thread(
|
|
||||||
&mut self,
|
|
||||||
thread: Entity<agent::Thread>,
|
|
||||||
remove_if_exists: bool,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Option<AgentContextHandle> {
|
|
||||||
let context_id = self.next_context_id.post_inc();
|
|
||||||
let context = AgentContextHandle::Thread(ThreadContextHandle { thread, context_id });
|
|
||||||
|
|
||||||
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
|
|
||||||
if remove_if_exists {
|
|
||||||
self.remove_context(&context, cx);
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(existing.as_ref().clone())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.insert_context(context.clone(), cx);
|
|
||||||
Some(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_text_thread(
|
|
||||||
&mut self,
|
|
||||||
text_thread: Entity<TextThread>,
|
|
||||||
remove_if_exists: bool,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Option<AgentContextHandle> {
|
|
||||||
let context_id = self.next_context_id.post_inc();
|
|
||||||
let context = AgentContextHandle::TextThread(TextThreadContextHandle {
|
|
||||||
text_thread,
|
|
||||||
context_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
|
|
||||||
if remove_if_exists {
|
|
||||||
self.remove_context(&context, cx);
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(existing.as_ref().clone())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.insert_context(context.clone(), cx);
|
|
||||||
Some(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_rules(
|
|
||||||
&mut self,
|
|
||||||
prompt_id: UserPromptId,
|
|
||||||
remove_if_exists: bool,
|
|
||||||
cx: &mut Context<ContextStore>,
|
|
||||||
) -> Option<AgentContextHandle> {
|
|
||||||
let context_id = self.next_context_id.post_inc();
|
|
||||||
let context = AgentContextHandle::Rules(RulesContextHandle {
|
|
||||||
prompt_id,
|
|
||||||
context_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
|
|
||||||
if remove_if_exists {
|
|
||||||
self.remove_context(&context, cx);
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(existing.as_ref().clone())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.insert_context(context.clone(), cx);
|
|
||||||
Some(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_fetched_url(
|
|
||||||
&mut self,
|
|
||||||
url: String,
|
|
||||||
text: impl Into<SharedString>,
|
|
||||||
cx: &mut Context<ContextStore>,
|
|
||||||
) -> AgentContextHandle {
|
|
||||||
let context = AgentContextHandle::FetchedUrl(FetchedUrlContext {
|
|
||||||
url: url.into(),
|
|
||||||
text: text.into(),
|
|
||||||
context_id: self.next_context_id.post_inc(),
|
|
||||||
});
|
|
||||||
|
|
||||||
self.insert_context(context.clone(), cx);
|
|
||||||
context
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_image_from_path(
|
|
||||||
&mut self,
|
|
||||||
project_path: ProjectPath,
|
|
||||||
remove_if_exists: bool,
|
|
||||||
cx: &mut Context<ContextStore>,
|
|
||||||
) -> Task<Result<Option<AgentContextHandle>>> {
|
|
||||||
let project = self.project.clone();
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
let open_image_task = project.update(cx, |project, cx| {
|
|
||||||
project.open_image(project_path.clone(), cx)
|
|
||||||
})?;
|
|
||||||
let image_item = open_image_task.await?;
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
let item = image_item.read(cx);
|
|
||||||
this.insert_image(
|
|
||||||
Some(item.project_path(cx)),
|
|
||||||
Some(item.file.full_path(cx).to_string_lossy().into_owned()),
|
|
||||||
item.image.clone(),
|
|
||||||
remove_if_exists,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_image_instance(&mut self, image: Arc<Image>, cx: &mut Context<ContextStore>) {
|
|
||||||
self.insert_image(None, None, image, false, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_image(
|
|
||||||
&mut self,
|
|
||||||
project_path: Option<ProjectPath>,
|
|
||||||
full_path: Option<String>,
|
|
||||||
image: Arc<Image>,
|
|
||||||
remove_if_exists: bool,
|
|
||||||
cx: &mut Context<ContextStore>,
|
|
||||||
) -> Option<AgentContextHandle> {
|
|
||||||
let image_task = LanguageModelImage::from_image(image.clone(), cx).shared();
|
|
||||||
let context = AgentContextHandle::Image(ImageContext {
|
|
||||||
project_path,
|
|
||||||
full_path,
|
|
||||||
original_image: image,
|
|
||||||
image_task,
|
|
||||||
context_id: self.next_context_id.post_inc(),
|
|
||||||
});
|
|
||||||
if self.has_context(&context) && remove_if_exists {
|
|
||||||
self.remove_context(&context, cx);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.insert_context(context.clone(), cx);
|
|
||||||
Some(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_selection(
|
|
||||||
&mut self,
|
|
||||||
buffer: Entity<Buffer>,
|
|
||||||
range: Range<Anchor>,
|
|
||||||
cx: &mut Context<ContextStore>,
|
|
||||||
) {
|
|
||||||
let context_id = self.next_context_id.post_inc();
|
|
||||||
let context = AgentContextHandle::Selection(SelectionContextHandle {
|
|
||||||
buffer,
|
|
||||||
range,
|
|
||||||
context_id,
|
|
||||||
});
|
|
||||||
self.insert_context(context, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_suggested_context(
|
|
||||||
&mut self,
|
|
||||||
suggested: &SuggestedContext,
|
|
||||||
cx: &mut Context<ContextStore>,
|
|
||||||
) {
|
|
||||||
match suggested {
|
|
||||||
SuggestedContext::File {
|
|
||||||
buffer,
|
|
||||||
icon_path: _,
|
|
||||||
name: _,
|
|
||||||
} => {
|
|
||||||
if let Some(buffer) = buffer.upgrade() {
|
|
||||||
let context_id = self.next_context_id.post_inc();
|
|
||||||
self.insert_context(
|
|
||||||
AgentContextHandle::File(FileContextHandle { buffer, context_id }),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
SuggestedContext::TextThread {
|
|
||||||
text_thread,
|
|
||||||
name: _,
|
|
||||||
} => {
|
|
||||||
if let Some(text_thread) = text_thread.upgrade() {
|
|
||||||
let context_id = self.next_context_id.post_inc();
|
|
||||||
self.insert_context(
|
|
||||||
AgentContextHandle::TextThread(TextThreadContextHandle {
|
|
||||||
text_thread,
|
|
||||||
context_id,
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_context(&mut self, context: AgentContextHandle, cx: &mut Context<Self>) -> bool {
|
|
||||||
match &context {
|
|
||||||
// AgentContextHandle::Thread(thread_context) => {
|
|
||||||
// if let Some(thread_store) = self.thread_store.clone() {
|
|
||||||
// thread_context.thread.update(cx, |thread, cx| {
|
|
||||||
// thread.start_generating_detailed_summary_if_needed(thread_store, cx);
|
|
||||||
// });
|
|
||||||
// self.context_thread_ids
|
|
||||||
// .insert(thread_context.thread.read(cx).id().clone());
|
|
||||||
// } else {
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
AgentContextHandle::TextThread(text_thread_context) => {
|
|
||||||
self.context_text_thread_paths
|
|
||||||
.extend(text_thread_context.text_thread.read(cx).path().cloned());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
let inserted = self.context_set.insert(AgentContextKey(context));
|
|
||||||
if inserted {
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
inserted
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_context(&mut self, context: &AgentContextHandle, cx: &mut Context<Self>) {
|
|
||||||
if let Some((_, key)) = self
|
|
||||||
.context_set
|
|
||||||
.shift_remove_full(AgentContextKey::ref_cast(context))
|
|
||||||
{
|
|
||||||
match context {
|
|
||||||
AgentContextHandle::Thread(thread_context) => {
|
|
||||||
self.context_thread_ids
|
|
||||||
.remove(thread_context.thread.read(cx).id());
|
|
||||||
}
|
|
||||||
AgentContextHandle::TextThread(text_thread_context) => {
|
|
||||||
if let Some(path) = text_thread_context.text_thread.read(cx).path() {
|
|
||||||
self.context_text_thread_paths.remove(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
cx.emit(ContextStoreEvent::ContextRemoved(key));
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_context(&mut self, context: &AgentContextHandle) -> bool {
|
|
||||||
self.context_set
|
|
||||||
.contains(AgentContextKey::ref_cast(context))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether this file path is already included directly in the context, or if it will be
|
|
||||||
/// included in the context via a directory.
|
|
||||||
pub fn file_path_included(&self, path: &ProjectPath, cx: &App) -> Option<FileInclusion> {
|
|
||||||
let project = self.project.upgrade()?.read(cx);
|
|
||||||
self.context().find_map(|context| match context {
|
|
||||||
AgentContextHandle::File(file_context) => {
|
|
||||||
FileInclusion::check_file(file_context, path, cx)
|
|
||||||
}
|
|
||||||
AgentContextHandle::Image(image_context) => {
|
|
||||||
FileInclusion::check_image(image_context, path)
|
|
||||||
}
|
|
||||||
AgentContextHandle::Directory(directory_context) => {
|
|
||||||
FileInclusion::check_directory(directory_context, path, project, cx)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn path_included_in_directory(
|
|
||||||
&self,
|
|
||||||
path: &ProjectPath,
|
|
||||||
cx: &App,
|
|
||||||
) -> Option<FileInclusion> {
|
|
||||||
let project = self.project.upgrade()?.read(cx);
|
|
||||||
self.context().find_map(|context| match context {
|
|
||||||
AgentContextHandle::Directory(directory_context) => {
|
|
||||||
FileInclusion::check_directory(directory_context, path, project, cx)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn includes_symbol(&self, symbol: &Symbol, cx: &App) -> bool {
|
|
||||||
self.context().any(|context| match context {
|
|
||||||
AgentContextHandle::Symbol(context) => {
|
|
||||||
if context.symbol != symbol.name {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let buffer = context.buffer.read(cx);
|
|
||||||
let Some(context_path) = buffer.project_path(cx) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if symbol.path != SymbolLocation::InProject(context_path) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let context_range = context.range.to_point_utf16(&buffer.snapshot());
|
|
||||||
context_range.start == symbol.range.start.0
|
|
||||||
&& context_range.end == symbol.range.end.0
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn includes_thread(&self, thread_id: &acp::SessionId) -> bool {
|
|
||||||
self.context_thread_ids.contains(thread_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn includes_text_thread(&self, path: &Arc<Path>) -> bool {
|
|
||||||
self.context_text_thread_paths.contains(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn includes_user_rules(&self, prompt_id: UserPromptId) -> bool {
|
|
||||||
self.context_set
|
|
||||||
.contains(&RulesContextHandle::lookup_key(prompt_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn includes_url(&self, url: impl Into<SharedString>) -> bool {
|
|
||||||
self.context_set
|
|
||||||
.contains(&FetchedUrlContext::lookup_key(url.into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_url_context(&self, url: SharedString) -> Option<AgentContextHandle> {
|
|
||||||
self.context_set
|
|
||||||
.get(&FetchedUrlContext::lookup_key(url))
|
|
||||||
.map(|key| key.as_ref().clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
|
|
||||||
self.context()
|
|
||||||
.filter_map(|context| match context {
|
|
||||||
AgentContextHandle::File(file) => {
|
|
||||||
let buffer = file.buffer.read(cx);
|
|
||||||
buffer.project_path(cx)
|
|
||||||
}
|
|
||||||
AgentContextHandle::Directory(_)
|
|
||||||
| AgentContextHandle::Symbol(_)
|
|
||||||
| AgentContextHandle::Thread(_)
|
|
||||||
| AgentContextHandle::Selection(_)
|
|
||||||
| AgentContextHandle::FetchedUrl(_)
|
|
||||||
| AgentContextHandle::TextThread(_)
|
|
||||||
| AgentContextHandle::Rules(_)
|
|
||||||
| AgentContextHandle::Image(_) => None,
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn thread_ids(&self) -> &HashSet<acp::SessionId> {
|
|
||||||
&self.context_thread_ids
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum SuggestedContext {
|
|
||||||
File {
|
|
||||||
name: SharedString,
|
|
||||||
icon_path: Option<SharedString>,
|
|
||||||
buffer: WeakEntity<Buffer>,
|
|
||||||
},
|
|
||||||
TextThread {
|
|
||||||
name: SharedString,
|
|
||||||
text_thread: WeakEntity<TextThread>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SuggestedContext {
|
|
||||||
pub fn name(&self) -> &SharedString {
|
|
||||||
match self {
|
|
||||||
Self::File { name, .. } => name,
|
|
||||||
Self::TextThread { name, .. } => name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn icon_path(&self) -> Option<SharedString> {
|
|
||||||
match self {
|
|
||||||
Self::File { icon_path, .. } => icon_path.clone(),
|
|
||||||
Self::TextThread { .. } => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn kind(&self) -> ContextKind {
|
|
||||||
match self {
|
|
||||||
Self::File { .. } => ContextKind::File,
|
|
||||||
Self::TextThread { .. } => ContextKind::TextThread,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum FileInclusion {
|
|
||||||
Direct,
|
|
||||||
InDirectory { full_path: PathBuf },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileInclusion {
|
|
||||||
fn check_file(file_context: &FileContextHandle, path: &ProjectPath, cx: &App) -> Option<Self> {
|
|
||||||
let file_path = file_context.buffer.read(cx).project_path(cx)?;
|
|
||||||
if path == &file_path {
|
|
||||||
Some(FileInclusion::Direct)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_image(image_context: &ImageContext, path: &ProjectPath) -> Option<Self> {
|
|
||||||
let image_path = image_context.project_path.as_ref()?;
|
|
||||||
if path == image_path {
|
|
||||||
Some(FileInclusion::Direct)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_directory(
|
|
||||||
directory_context: &DirectoryContextHandle,
|
|
||||||
path: &ProjectPath,
|
|
||||||
project: &Project,
|
|
||||||
cx: &App,
|
|
||||||
) -> Option<Self> {
|
|
||||||
let worktree = project
|
|
||||||
.worktree_for_entry(directory_context.entry_id, cx)?
|
|
||||||
.read(cx);
|
|
||||||
let entry = worktree.entry_for_id(directory_context.entry_id)?;
|
|
||||||
let directory_path = ProjectPath {
|
|
||||||
worktree_id: worktree.id(),
|
|
||||||
path: entry.path.clone(),
|
|
||||||
};
|
|
||||||
if path.starts_with(&directory_path) {
|
|
||||||
if path == &directory_path {
|
|
||||||
Some(FileInclusion::Direct)
|
|
||||||
} else {
|
|
||||||
Some(FileInclusion::InDirectory {
|
|
||||||
full_path: worktree.full_path(&entry.path),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,619 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
AcceptSuggestedContext, AgentPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
|
|
||||||
ModelUsageContext, RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
|
|
||||||
context_picker::ContextPicker,
|
|
||||||
ui::{AddedContext, ContextPill},
|
|
||||||
};
|
|
||||||
use crate::{
|
|
||||||
context::AgentContextHandle,
|
|
||||||
context_store::{ContextStore, SuggestedContext},
|
|
||||||
};
|
|
||||||
use agent::HistoryStore;
|
|
||||||
use collections::HashSet;
|
|
||||||
use editor::Editor;
|
|
||||||
use gpui::{
|
|
||||||
App, Bounds, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
|
||||||
Subscription, Task, WeakEntity,
|
|
||||||
};
|
|
||||||
use itertools::Itertools;
|
|
||||||
use project::ProjectItem;
|
|
||||||
use prompt_store::PromptStore;
|
|
||||||
use rope::Point;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use text::ToPoint as _;
|
|
||||||
use ui::{PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
|
|
||||||
use util::ResultExt as _;
|
|
||||||
use workspace::Workspace;
|
|
||||||
use zed_actions::assistant::OpenRulesLibrary;
|
|
||||||
|
|
||||||
pub struct ContextStrip {
|
|
||||||
context_store: Entity<ContextStore>,
|
|
||||||
context_picker: Entity<ContextPicker>,
|
|
||||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
suggest_context_kind: SuggestContextKind,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
prompt_store: Option<WeakEntity<PromptStore>>,
|
|
||||||
_subscriptions: Vec<Subscription>,
|
|
||||||
focused_index: Option<usize>,
|
|
||||||
children_bounds: Option<Vec<Bounds<Pixels>>>,
|
|
||||||
model_usage_context: ModelUsageContext,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContextStrip {
|
|
||||||
pub fn new(
|
|
||||||
context_store: Entity<ContextStore>,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
|
||||||
prompt_store: Option<WeakEntity<PromptStore>>,
|
|
||||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
|
||||||
suggest_context_kind: SuggestContextKind,
|
|
||||||
model_usage_context: ModelUsageContext,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let context_picker = cx.new(|cx| {
|
|
||||||
ContextPicker::new(
|
|
||||||
workspace.clone(),
|
|
||||||
thread_store.clone(),
|
|
||||||
prompt_store.clone(),
|
|
||||||
context_store.downgrade(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let focus_handle = cx.focus_handle();
|
|
||||||
|
|
||||||
let subscriptions = vec![
|
|
||||||
cx.observe(&context_store, |_, _, cx| cx.notify()),
|
|
||||||
cx.subscribe_in(&context_picker, window, Self::handle_context_picker_event),
|
|
||||||
cx.on_focus(&focus_handle, window, Self::handle_focus),
|
|
||||||
cx.on_blur(&focus_handle, window, Self::handle_blur),
|
|
||||||
];
|
|
||||||
|
|
||||||
Self {
|
|
||||||
context_store: context_store.clone(),
|
|
||||||
context_picker,
|
|
||||||
context_picker_menu_handle,
|
|
||||||
focus_handle,
|
|
||||||
suggest_context_kind,
|
|
||||||
workspace,
|
|
||||||
prompt_store,
|
|
||||||
_subscriptions: subscriptions,
|
|
||||||
focused_index: None,
|
|
||||||
children_bounds: None,
|
|
||||||
model_usage_context,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether or not the context strip has items to display
|
|
||||||
pub fn has_context_items(&self, cx: &App) -> bool {
|
|
||||||
self.context_store.read(cx).context().next().is_some()
|
|
||||||
|| self.suggested_context(cx).is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn added_contexts(&self, cx: &App) -> Vec<AddedContext> {
|
|
||||||
if let Some(workspace) = self.workspace.upgrade() {
|
|
||||||
let project = workspace.read(cx).project().read(cx);
|
|
||||||
let prompt_store = self.prompt_store.as_ref().and_then(|p| p.upgrade());
|
|
||||||
|
|
||||||
let current_model = self.model_usage_context.language_model(cx);
|
|
||||||
|
|
||||||
self.context_store
|
|
||||||
.read(cx)
|
|
||||||
.context()
|
|
||||||
.flat_map(|context| {
|
|
||||||
AddedContext::new_pending(
|
|
||||||
context.clone(),
|
|
||||||
prompt_store.as_ref(),
|
|
||||||
project,
|
|
||||||
current_model.as_ref(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn suggested_context(&self, cx: &App) -> Option<SuggestedContext> {
|
|
||||||
match self.suggest_context_kind {
|
|
||||||
SuggestContextKind::Thread => self.suggested_thread(cx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn suggested_thread(&self, cx: &App) -> Option<SuggestedContext> {
|
|
||||||
if !self.context_picker.read(cx).allow_threads() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let workspace = self.workspace.upgrade()?;
|
|
||||||
let panel = workspace.read(cx).panel::<AgentPanel>(cx)?.read(cx);
|
|
||||||
|
|
||||||
if let Some(active_text_thread_editor) = panel.active_text_thread_editor() {
|
|
||||||
let text_thread = active_text_thread_editor.read(cx).text_thread();
|
|
||||||
let weak_text_thread = text_thread.downgrade();
|
|
||||||
let text_thread = text_thread.read(cx);
|
|
||||||
let path = text_thread.path()?;
|
|
||||||
|
|
||||||
if self.context_store.read(cx).includes_text_thread(path) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(SuggestedContext::TextThread {
|
|
||||||
name: text_thread.summary().or_default(),
|
|
||||||
text_thread: weak_text_thread,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_context_picker_event(
|
|
||||||
&mut self,
|
|
||||||
_picker: &Entity<ContextPicker>,
|
|
||||||
_event: &DismissEvent,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
cx.emit(ContextStripEvent::PickerDismissed);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_focus(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.focused_index = self.last_pill_index();
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_blur(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.focused_index = None;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_left(&mut self, _: &FocusLeft, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.focused_index = match self.focused_index {
|
|
||||||
Some(index) if index > 0 => Some(index - 1),
|
|
||||||
_ => self.last_pill_index(),
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_right(&mut self, _: &FocusRight, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let Some(last_index) = self.last_pill_index() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.focused_index = match self.focused_index {
|
|
||||||
Some(index) if index < last_index => Some(index + 1),
|
|
||||||
_ => Some(0),
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_up(&mut self, _: &FocusUp, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let Some(focused_index) = self.focused_index else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if focused_index == 0 {
|
|
||||||
return cx.emit(ContextStripEvent::BlurredUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some((focused, pills)) = self.focused_bounds(focused_index) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let iter = pills[..focused_index].iter().enumerate().rev();
|
|
||||||
self.focused_index = Self::find_best_horizontal_match(focused, iter).or(Some(0));
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_down(&mut self, _: &FocusDown, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let Some(focused_index) = self.focused_index else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let last_index = self.last_pill_index();
|
|
||||||
|
|
||||||
if self.focused_index == last_index {
|
|
||||||
return cx.emit(ContextStripEvent::BlurredDown);
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some((focused, pills)) = self.focused_bounds(focused_index) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let iter = pills.iter().enumerate().skip(focused_index + 1);
|
|
||||||
self.focused_index = Self::find_best_horizontal_match(focused, iter).or(last_index);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focused_bounds(&self, focused: usize) -> Option<(&Bounds<Pixels>, &[Bounds<Pixels>])> {
|
|
||||||
let pill_bounds = self.pill_bounds()?;
|
|
||||||
let focused = pill_bounds.get(focused)?;
|
|
||||||
|
|
||||||
Some((focused, pill_bounds))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pill_bounds(&self) -> Option<&[Bounds<Pixels>]> {
|
|
||||||
let bounds = self.children_bounds.as_ref()?;
|
|
||||||
let eraser = if bounds.len() < 3 { 0 } else { 1 };
|
|
||||||
let pills = &bounds[1..bounds.len() - eraser];
|
|
||||||
|
|
||||||
if pills.is_empty() { None } else { Some(pills) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn last_pill_index(&self) -> Option<usize> {
|
|
||||||
Some(self.pill_bounds()?.len() - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_best_horizontal_match<'a>(
|
|
||||||
focused: &'a Bounds<Pixels>,
|
|
||||||
iter: impl Iterator<Item = (usize, &'a Bounds<Pixels>)>,
|
|
||||||
) -> Option<usize> {
|
|
||||||
let mut best = None;
|
|
||||||
|
|
||||||
let focused_left = focused.left();
|
|
||||||
let focused_right = focused.right();
|
|
||||||
|
|
||||||
for (index, probe) in iter {
|
|
||||||
if probe.origin.y == focused.origin.y {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let overlap = probe.right().min(focused_right) - probe.left().max(focused_left);
|
|
||||||
|
|
||||||
best = match best {
|
|
||||||
Some((_, prev_overlap, y)) if probe.origin.y != y || prev_overlap > overlap => {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Some(_) | None => Some((index, overlap, probe.origin.y)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
best.map(|(index, _, _)| index)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_context(&mut self, context: &AgentContextHandle, window: &mut Window, cx: &mut App) {
|
|
||||||
let Some(workspace) = self.workspace.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
match context {
|
|
||||||
AgentContextHandle::File(file_context) => {
|
|
||||||
if let Some(project_path) = file_context.project_path(cx) {
|
|
||||||
workspace.update(cx, |workspace, cx| {
|
|
||||||
workspace
|
|
||||||
.open_path(project_path, None, true, window, cx)
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AgentContextHandle::Directory(directory_context) => {
|
|
||||||
let entry_id = directory_context.entry_id;
|
|
||||||
workspace.update(cx, |workspace, cx| {
|
|
||||||
workspace.project().update(cx, |_project, cx| {
|
|
||||||
cx.emit(project::Event::RevealInProjectPanel(entry_id));
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
AgentContextHandle::Symbol(symbol_context) => {
|
|
||||||
let buffer = symbol_context.buffer.read(cx);
|
|
||||||
if let Some(project_path) = buffer.project_path(cx) {
|
|
||||||
let snapshot = buffer.snapshot();
|
|
||||||
let target_position = symbol_context.range.start.to_point(&snapshot);
|
|
||||||
open_editor_at_position(project_path, target_position, &workspace, window, cx)
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AgentContextHandle::Selection(selection_context) => {
|
|
||||||
let buffer = selection_context.buffer.read(cx);
|
|
||||||
if let Some(project_path) = buffer.project_path(cx) {
|
|
||||||
let snapshot = buffer.snapshot();
|
|
||||||
let target_position = selection_context.range.start.to_point(&snapshot);
|
|
||||||
|
|
||||||
open_editor_at_position(project_path, target_position, &workspace, window, cx)
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AgentContextHandle::FetchedUrl(fetched_url_context) => {
|
|
||||||
cx.open_url(&fetched_url_context.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
AgentContextHandle::Thread(_thread_context) => {}
|
|
||||||
|
|
||||||
AgentContextHandle::TextThread(text_thread_context) => {
|
|
||||||
workspace.update(cx, |workspace, cx| {
|
|
||||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
|
||||||
let context = text_thread_context.text_thread.clone();
|
|
||||||
window.defer(cx, move |window, cx| {
|
|
||||||
panel.update(cx, |panel, cx| {
|
|
||||||
panel.open_text_thread(context, window, cx)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
AgentContextHandle::Rules(rules_context) => window.dispatch_action(
|
|
||||||
Box::new(OpenRulesLibrary {
|
|
||||||
prompt_to_select: Some(rules_context.prompt_id.0),
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
),
|
|
||||||
|
|
||||||
AgentContextHandle::Image(_) => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_focused_context(
|
|
||||||
&mut self,
|
|
||||||
_: &RemoveFocusedContext,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if let Some(index) = self.focused_index {
|
|
||||||
let added_contexts = self.added_contexts(cx);
|
|
||||||
let Some(context) = added_contexts.get(index) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.context_store.update(cx, |this, cx| {
|
|
||||||
this.remove_context(&context.handle, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
let is_now_empty = added_contexts.len() == 1;
|
|
||||||
if is_now_empty {
|
|
||||||
cx.emit(ContextStripEvent::BlurredEmpty);
|
|
||||||
} else {
|
|
||||||
self.focused_index = Some(index.saturating_sub(1));
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_suggested_focused(&self, added_contexts: &Vec<AddedContext>) -> bool {
|
|
||||||
// We only suggest one item after the actual context
|
|
||||||
self.focused_index == Some(added_contexts.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accept_suggested_context(
|
|
||||||
&mut self,
|
|
||||||
_: &AcceptSuggestedContext,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if let Some(suggested) = self.suggested_context(cx)
|
|
||||||
&& self.is_suggested_focused(&self.added_contexts(cx))
|
|
||||||
{
|
|
||||||
self.add_suggested_context(&suggested, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_suggested_context(&mut self, suggested: &SuggestedContext, cx: &mut Context<Self>) {
|
|
||||||
self.context_store.update(cx, |context_store, cx| {
|
|
||||||
context_store.add_suggested_context(suggested, cx)
|
|
||||||
});
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Focusable for ContextStrip {
|
|
||||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ContextStrip {
|
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let context_picker = self.context_picker.clone();
|
|
||||||
let focus_handle = self.focus_handle.clone();
|
|
||||||
|
|
||||||
let added_contexts = self.added_contexts(cx);
|
|
||||||
let dupe_names = added_contexts
|
|
||||||
.iter()
|
|
||||||
.map(|c| c.name.clone())
|
|
||||||
.sorted()
|
|
||||||
.tuple_windows()
|
|
||||||
.filter(|(a, b)| a == b)
|
|
||||||
.map(|(a, _)| a)
|
|
||||||
.collect::<HashSet<SharedString>>();
|
|
||||||
let no_added_context = added_contexts.is_empty();
|
|
||||||
|
|
||||||
let suggested_context = self.suggested_context(cx).map(|suggested_context| {
|
|
||||||
(
|
|
||||||
suggested_context,
|
|
||||||
self.is_suggested_focused(&added_contexts),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.flex_wrap()
|
|
||||||
.gap_1()
|
|
||||||
.track_focus(&focus_handle)
|
|
||||||
.key_context("ContextStrip")
|
|
||||||
.on_action(cx.listener(Self::focus_up))
|
|
||||||
.on_action(cx.listener(Self::focus_right))
|
|
||||||
.on_action(cx.listener(Self::focus_down))
|
|
||||||
.on_action(cx.listener(Self::focus_left))
|
|
||||||
.on_action(cx.listener(Self::remove_focused_context))
|
|
||||||
.on_action(cx.listener(Self::accept_suggested_context))
|
|
||||||
.on_children_prepainted({
|
|
||||||
let entity = cx.entity().downgrade();
|
|
||||||
move |children_bounds, _window, cx| {
|
|
||||||
entity
|
|
||||||
.update(cx, |this, _| {
|
|
||||||
this.children_bounds = Some(children_bounds);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
PopoverMenu::new("context-picker")
|
|
||||||
.menu({
|
|
||||||
let context_picker = context_picker.clone();
|
|
||||||
move |window, cx| {
|
|
||||||
context_picker.update(cx, |this, cx| {
|
|
||||||
this.init(window, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
Some(context_picker.clone())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_open({
|
|
||||||
let context_picker = context_picker.downgrade();
|
|
||||||
Rc::new(move |window, cx| {
|
|
||||||
context_picker
|
|
||||||
.update(cx, |context_picker, cx| {
|
|
||||||
context_picker.select_first(window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.trigger_with_tooltip(
|
|
||||||
IconButton::new("add-context", IconName::Plus)
|
|
||||||
.icon_size(IconSize::Small)
|
|
||||||
.style(ui::ButtonStyle::Filled),
|
|
||||||
{
|
|
||||||
let focus_handle = focus_handle.clone();
|
|
||||||
move |_window, cx| {
|
|
||||||
Tooltip::for_action_in(
|
|
||||||
"Add Context",
|
|
||||||
&ToggleContextPicker,
|
|
||||||
&focus_handle,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.attach(gpui::Corner::TopLeft)
|
|
||||||
.anchor(gpui::Corner::BottomLeft)
|
|
||||||
.offset(gpui::Point {
|
|
||||||
x: px(0.0),
|
|
||||||
y: px(-2.0),
|
|
||||||
})
|
|
||||||
.with_handle(self.context_picker_menu_handle.clone()),
|
|
||||||
)
|
|
||||||
.children(
|
|
||||||
added_contexts
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, added_context)| {
|
|
||||||
let name = added_context.name.clone();
|
|
||||||
let context = added_context.handle.clone();
|
|
||||||
ContextPill::added(
|
|
||||||
added_context,
|
|
||||||
dupe_names.contains(&name),
|
|
||||||
self.focused_index == Some(i),
|
|
||||||
Some({
|
|
||||||
let context = context.clone();
|
|
||||||
let context_store = self.context_store.clone();
|
|
||||||
Rc::new(cx.listener(move |_this, _event, _window, cx| {
|
|
||||||
context_store.update(cx, |this, cx| {
|
|
||||||
this.remove_context(&context, cx);
|
|
||||||
});
|
|
||||||
cx.notify();
|
|
||||||
}))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.on_click({
|
|
||||||
Rc::new(cx.listener(move |this, event: &ClickEvent, window, cx| {
|
|
||||||
if event.click_count() > 1 {
|
|
||||||
this.open_context(&context, window, cx);
|
|
||||||
} else {
|
|
||||||
this.focused_index = Some(i);
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.when_some(suggested_context, |el, (suggested, focused)| {
|
|
||||||
el.child(
|
|
||||||
ContextPill::suggested(
|
|
||||||
suggested.name().clone(),
|
|
||||||
suggested.icon_path(),
|
|
||||||
suggested.kind(),
|
|
||||||
focused,
|
|
||||||
)
|
|
||||||
.on_click(Rc::new(cx.listener(
|
|
||||||
move |this, _event, _window, cx| {
|
|
||||||
this.add_suggested_context(&suggested, cx);
|
|
||||||
},
|
|
||||||
))),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when(!no_added_context, {
|
|
||||||
move |parent| {
|
|
||||||
parent.child(
|
|
||||||
IconButton::new("remove-all-context", IconName::Eraser)
|
|
||||||
.icon_size(IconSize::Small)
|
|
||||||
.tooltip({
|
|
||||||
let focus_handle = focus_handle.clone();
|
|
||||||
move |_window, cx| {
|
|
||||||
Tooltip::for_action_in(
|
|
||||||
"Remove All Context",
|
|
||||||
&RemoveAllContext,
|
|
||||||
&focus_handle,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_click(cx.listener({
|
|
||||||
let focus_handle = focus_handle.clone();
|
|
||||||
move |_this, _event, window, cx| {
|
|
||||||
focus_handle.dispatch_action(&RemoveAllContext, window, cx);
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into_any()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ContextStripEvent {
|
|
||||||
PickerDismissed,
|
|
||||||
BlurredEmpty,
|
|
||||||
BlurredDown,
|
|
||||||
BlurredUp,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<ContextStripEvent> for ContextStrip {}
|
|
||||||
|
|
||||||
pub enum SuggestContextKind {
|
|
||||||
Thread,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_editor_at_position(
|
|
||||||
project_path: project::ProjectPath,
|
|
||||||
target_position: Point,
|
|
||||||
workspace: &Entity<Workspace>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Task<()> {
|
|
||||||
let open_task = workspace.update(cx, |workspace, cx| {
|
|
||||||
workspace.open_path(project_path, None, true, window, cx)
|
|
||||||
});
|
|
||||||
window.spawn(cx, async move |cx| {
|
|
||||||
if let Some(active_editor) = open_task
|
|
||||||
.await
|
|
||||||
.log_err()
|
|
||||||
.and_then(|item| item.downcast::<Editor>())
|
|
||||||
{
|
|
||||||
active_editor
|
|
||||||
.downgrade()
|
|
||||||
.update_in(cx, |editor, window, cx| {
|
|
||||||
editor.go_to_singleton_buffer_point(target_position, window, cx);
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -4,10 +4,11 @@ use std::ops::Range;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::context::load_context;
|
||||||
|
use crate::mention_set::MentionSet;
|
||||||
use crate::{
|
use crate::{
|
||||||
AgentPanel,
|
AgentPanel,
|
||||||
buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent},
|
buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent},
|
||||||
context_store::ContextStore,
|
|
||||||
inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent},
|
inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent},
|
||||||
terminal_inline_assistant::TerminalInlineAssistant,
|
terminal_inline_assistant::TerminalInlineAssistant,
|
||||||
};
|
};
|
||||||
@@ -31,6 +32,7 @@ use editor::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
|
use futures::FutureExt;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
|
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
|
||||||
WeakEntity, Window, point,
|
WeakEntity, Window, point,
|
||||||
@@ -214,16 +216,10 @@ impl InlineAssistant {
|
|||||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
if is_ai_enabled {
|
if is_ai_enabled {
|
||||||
let panel = workspace.read(cx).panel::<AgentPanel>(cx);
|
|
||||||
let thread_store = panel
|
|
||||||
.as_ref()
|
|
||||||
.map(|agent_panel| agent_panel.read(cx).thread_store().downgrade());
|
|
||||||
|
|
||||||
editor.add_code_action_provider(
|
editor.add_code_action_provider(
|
||||||
Rc::new(AssistantCodeActionProvider {
|
Rc::new(AssistantCodeActionProvider {
|
||||||
editor: cx.entity().downgrade(),
|
editor: cx.entity().downgrade(),
|
||||||
workspace: workspace.downgrade(),
|
workspace: workspace.downgrade(),
|
||||||
thread_store,
|
|
||||||
}),
|
}),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
@@ -235,9 +231,6 @@ impl InlineAssistant {
|
|||||||
editor.cancel(&Default::default(), window, cx);
|
editor.cancel(&Default::default(), window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the Assistant1 code action provider, as it still might be registered.
|
|
||||||
editor.remove_code_action_provider("assistant".into(), window, cx);
|
|
||||||
} else {
|
} else {
|
||||||
editor.remove_code_action_provider(
|
editor.remove_code_action_provider(
|
||||||
ASSISTANT_CODE_ACTION_PROVIDER_ID.into(),
|
ASSISTANT_CODE_ACTION_PROVIDER_ID.into(),
|
||||||
@@ -279,8 +272,7 @@ impl InlineAssistant {
|
|||||||
let agent_panel = agent_panel.read(cx);
|
let agent_panel = agent_panel.read(cx);
|
||||||
|
|
||||||
let prompt_store = agent_panel.prompt_store().as_ref().cloned();
|
let prompt_store = agent_panel.prompt_store().as_ref().cloned();
|
||||||
let thread_store = Some(agent_panel.thread_store().downgrade());
|
let thread_store = agent_panel.thread_store().clone();
|
||||||
let context_store = agent_panel.inline_assist_context_store().clone();
|
|
||||||
|
|
||||||
let handle_assist =
|
let handle_assist =
|
||||||
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
|
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
|
||||||
@@ -289,10 +281,9 @@ impl InlineAssistant {
|
|||||||
assistant.assist(
|
assistant.assist(
|
||||||
&active_editor,
|
&active_editor,
|
||||||
cx.entity().downgrade(),
|
cx.entity().downgrade(),
|
||||||
context_store,
|
|
||||||
workspace.project().downgrade(),
|
workspace.project().downgrade(),
|
||||||
prompt_store,
|
|
||||||
thread_store,
|
thread_store,
|
||||||
|
prompt_store,
|
||||||
action.prompt.clone(),
|
action.prompt.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
@@ -305,8 +296,8 @@ impl InlineAssistant {
|
|||||||
&active_terminal,
|
&active_terminal,
|
||||||
cx.entity().downgrade(),
|
cx.entity().downgrade(),
|
||||||
workspace.project().downgrade(),
|
workspace.project().downgrade(),
|
||||||
prompt_store,
|
|
||||||
thread_store,
|
thread_store,
|
||||||
|
prompt_store,
|
||||||
action.prompt.clone(),
|
action.prompt.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
@@ -477,10 +468,9 @@ impl InlineAssistant {
|
|||||||
&mut self,
|
&mut self,
|
||||||
editor: &Entity<Editor>,
|
editor: &Entity<Editor>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
context_store: Entity<ContextStore>,
|
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
|
thread_store: Entity<HistoryStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
|
||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
codegen_ranges: &[Range<Anchor>],
|
codegen_ranges: &[Range<Anchor>],
|
||||||
@@ -508,9 +498,6 @@ impl InlineAssistant {
|
|||||||
editor.read(cx).buffer().clone(),
|
editor.read(cx).buffer().clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
initial_transaction_id,
|
initial_transaction_id,
|
||||||
context_store.clone(),
|
|
||||||
project.clone(),
|
|
||||||
prompt_store.clone(),
|
|
||||||
self.telemetry.clone(),
|
self.telemetry.clone(),
|
||||||
self.prompt_builder.clone(),
|
self.prompt_builder.clone(),
|
||||||
cx,
|
cx,
|
||||||
@@ -526,10 +513,10 @@ impl InlineAssistant {
|
|||||||
prompt_buffer.clone(),
|
prompt_buffer.clone(),
|
||||||
codegen.clone(),
|
codegen.clone(),
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
context_store.clone(),
|
|
||||||
workspace.clone(),
|
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
prompt_store.as_ref().map(|s| s.downgrade()),
|
prompt_store.clone(),
|
||||||
|
project.clone(),
|
||||||
|
workspace.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -606,10 +593,9 @@ impl InlineAssistant {
|
|||||||
&mut self,
|
&mut self,
|
||||||
editor: &Entity<Editor>,
|
editor: &Entity<Editor>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
context_store: Entity<ContextStore>,
|
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
|
thread_store: Entity<HistoryStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
|
||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
@@ -625,10 +611,9 @@ impl InlineAssistant {
|
|||||||
let assist_to_focus = self.batch_assist(
|
let assist_to_focus = self.batch_assist(
|
||||||
editor,
|
editor,
|
||||||
workspace,
|
workspace,
|
||||||
context_store,
|
|
||||||
project,
|
project,
|
||||||
prompt_store,
|
|
||||||
thread_store,
|
thread_store,
|
||||||
|
prompt_store,
|
||||||
initial_prompt,
|
initial_prompt,
|
||||||
window,
|
window,
|
||||||
&codegen_ranges,
|
&codegen_ranges,
|
||||||
@@ -650,8 +635,8 @@ impl InlineAssistant {
|
|||||||
initial_transaction_id: Option<TransactionId>,
|
initial_transaction_id: Option<TransactionId>,
|
||||||
focus: bool,
|
focus: bool,
|
||||||
workspace: Entity<Workspace>,
|
workspace: Entity<Workspace>,
|
||||||
|
thread_store: Entity<HistoryStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> InlineAssistId {
|
) -> InlineAssistId {
|
||||||
@@ -663,16 +648,14 @@ impl InlineAssistant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let project = workspace.read(cx).project().downgrade();
|
let project = workspace.read(cx).project().downgrade();
|
||||||
let context_store = cx.new(|_cx| ContextStore::new(project.clone()));
|
|
||||||
|
|
||||||
let assist_id = self
|
let assist_id = self
|
||||||
.batch_assist(
|
.batch_assist(
|
||||||
editor,
|
editor,
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
context_store,
|
|
||||||
project,
|
project,
|
||||||
prompt_store,
|
|
||||||
thread_store,
|
thread_store,
|
||||||
|
prompt_store,
|
||||||
Some(initial_prompt),
|
Some(initial_prompt),
|
||||||
window,
|
window,
|
||||||
&[range],
|
&[range],
|
||||||
@@ -1294,7 +1277,8 @@ impl InlineAssistant {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(user_prompt) = assist.user_prompt(cx) else {
|
let Some((user_prompt, mention_set)) = assist.user_prompt(cx).zip(assist.mention_set(cx))
|
||||||
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1310,9 +1294,12 @@ impl InlineAssistant {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let context_task = load_context(&mention_set, cx).shared();
|
||||||
assist
|
assist
|
||||||
.codegen
|
.codegen
|
||||||
.update(cx, |codegen, cx| codegen.start(model, user_prompt, cx))
|
.update(cx, |codegen, cx| {
|
||||||
|
codegen.start(model, user_prompt, context_task, cx)
|
||||||
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1458,6 +1445,7 @@ impl InlineAssistant {
|
|||||||
multi_buffer.update(cx, |multi_buffer, cx| {
|
multi_buffer.update(cx, |multi_buffer, cx| {
|
||||||
multi_buffer.push_excerpts(
|
multi_buffer.push_excerpts(
|
||||||
old_buffer.clone(),
|
old_buffer.clone(),
|
||||||
|
// todo(lw): buffer_start and buffer_end might come from different snapshots!
|
||||||
Some(ExcerptRange::new(buffer_start..buffer_end)),
|
Some(ExcerptRange::new(buffer_start..buffer_end)),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -1778,6 +1766,11 @@ impl InlineAssist {
|
|||||||
let decorations = self.decorations.as_ref()?;
|
let decorations = self.decorations.as_ref()?;
|
||||||
Some(decorations.prompt_editor.read(cx).prompt(cx))
|
Some(decorations.prompt_editor.read(cx).prompt(cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mention_set(&self, cx: &App) -> Option<Entity<MentionSet>> {
|
||||||
|
let decorations = self.decorations.as_ref()?;
|
||||||
|
Some(decorations.prompt_editor.read(cx).mention_set().clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InlineAssistDecorations {
|
struct InlineAssistDecorations {
|
||||||
@@ -1790,10 +1783,9 @@ struct InlineAssistDecorations {
|
|||||||
struct AssistantCodeActionProvider {
|
struct AssistantCodeActionProvider {
|
||||||
editor: WeakEntity<Editor>,
|
editor: WeakEntity<Editor>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant2";
|
const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant";
|
||||||
|
|
||||||
impl CodeActionProvider for AssistantCodeActionProvider {
|
impl CodeActionProvider for AssistantCodeActionProvider {
|
||||||
fn id(&self) -> Arc<str> {
|
fn id(&self) -> Arc<str> {
|
||||||
@@ -1861,10 +1853,20 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
|||||||
) -> Task<Result<ProjectTransaction>> {
|
) -> Task<Result<ProjectTransaction>> {
|
||||||
let editor = self.editor.clone();
|
let editor = self.editor.clone();
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
let thread_store = self.thread_store.clone();
|
|
||||||
let prompt_store = PromptStore::global(cx);
|
let prompt_store = PromptStore::global(cx);
|
||||||
window.spawn(cx, async move |cx| {
|
window.spawn(cx, async move |cx| {
|
||||||
let workspace = workspace.upgrade().context("workspace was released")?;
|
let workspace = workspace.upgrade().context("workspace was released")?;
|
||||||
|
let thread_store = cx.update(|_window, cx| {
|
||||||
|
anyhow::Ok(
|
||||||
|
workspace
|
||||||
|
.read(cx)
|
||||||
|
.panel::<AgentPanel>(cx)
|
||||||
|
.context("missing agent panel")?
|
||||||
|
.read(cx)
|
||||||
|
.thread_store()
|
||||||
|
.clone(),
|
||||||
|
)
|
||||||
|
})??;
|
||||||
let editor = editor.upgrade().context("editor was released")?;
|
let editor = editor.upgrade().context("editor was released")?;
|
||||||
let range = editor
|
let range = editor
|
||||||
.update(cx, |editor, cx| {
|
.update(cx, |editor, cx| {
|
||||||
@@ -1907,8 +1909,8 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
|||||||
None,
|
None,
|
||||||
true,
|
true,
|
||||||
workspace,
|
workspace,
|
||||||
prompt_store,
|
|
||||||
thread_store,
|
thread_store,
|
||||||
|
prompt_store,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
use agent::HistoryStore;
|
use agent::HistoryStore;
|
||||||
use collections::{HashMap, VecDeque};
|
use collections::{HashMap, VecDeque};
|
||||||
use editor::actions::Paste;
|
use editor::actions::Paste;
|
||||||
|
use editor::code_context_menus::CodeContextMenu;
|
||||||
use editor::display_map::{CreaseId, EditorMargins};
|
use editor::display_map::{CreaseId, EditorMargins};
|
||||||
use editor::{Addon, AnchorRangeExt as _, MultiBufferOffset};
|
use editor::{AnchorRangeExt as _, MultiBufferOffset, ToOffset as _};
|
||||||
use editor::{
|
use editor::{
|
||||||
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
||||||
actions::{MoveDown, MoveUp},
|
actions::{MoveDown, MoveUp},
|
||||||
};
|
};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, ClipboardEntry, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
|
AnyElement, App, Context, CursorStyle, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
Focusable, Subscription, TextStyle, WeakEntity, Window,
|
Subscription, TextStyle, WeakEntity, Window,
|
||||||
};
|
};
|
||||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use project::Project;
|
||||||
use prompt_store::PromptStore;
|
use prompt_store::PromptStore;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
@@ -28,22 +30,21 @@ use zed_actions::agent::ToggleModelSelector;
|
|||||||
|
|
||||||
use crate::agent_model_selector::AgentModelSelector;
|
use crate::agent_model_selector::AgentModelSelector;
|
||||||
use crate::buffer_codegen::BufferCodegen;
|
use crate::buffer_codegen::BufferCodegen;
|
||||||
use crate::context::{AgentContextHandle, AgentContextKey};
|
use crate::completion_provider::{
|
||||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextType,
|
||||||
use crate::context_store::{ContextStore, ContextStoreEvent};
|
|
||||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
|
||||||
use crate::terminal_codegen::TerminalCodegen;
|
|
||||||
use crate::{
|
|
||||||
CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext, RemoveAllContext,
|
|
||||||
ToggleContextPicker,
|
|
||||||
};
|
};
|
||||||
|
use crate::mention_set::paste_images_as_context;
|
||||||
|
use crate::mention_set::{MentionSet, crease_for_mention};
|
||||||
|
use crate::terminal_codegen::TerminalCodegen;
|
||||||
|
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
|
||||||
|
|
||||||
pub struct PromptEditor<T> {
|
pub struct PromptEditor<T> {
|
||||||
pub editor: Entity<Editor>,
|
pub editor: Entity<Editor>,
|
||||||
mode: PromptEditorMode,
|
mode: PromptEditorMode,
|
||||||
context_store: Entity<ContextStore>,
|
mention_set: Entity<MentionSet>,
|
||||||
context_strip: Entity<ContextStrip>,
|
history_store: Entity<HistoryStore>,
|
||||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
model_selector: Entity<AgentModelSelector>,
|
model_selector: Entity<AgentModelSelector>,
|
||||||
edited_since_done: bool,
|
edited_since_done: bool,
|
||||||
prompt_history: VecDeque<String>,
|
prompt_history: VecDeque<String>,
|
||||||
@@ -51,7 +52,6 @@ pub struct PromptEditor<T> {
|
|||||||
pending_prompt: String,
|
pending_prompt: String,
|
||||||
_codegen_subscription: Subscription,
|
_codegen_subscription: Subscription,
|
||||||
editor_subscriptions: Vec<Subscription>,
|
editor_subscriptions: Vec<Subscription>,
|
||||||
_context_strip_subscription: Subscription,
|
|
||||||
show_rate_limit_notice: bool,
|
show_rate_limit_notice: bool,
|
||||||
_phantom: std::marker::PhantomData<T>,
|
_phantom: std::marker::PhantomData<T>,
|
||||||
}
|
}
|
||||||
@@ -98,6 +98,19 @@ impl<T: 'static> Render for PromptEditor<T> {
|
|||||||
|
|
||||||
buttons.extend(self.render_buttons(window, cx));
|
buttons.extend(self.render_buttons(window, cx));
|
||||||
|
|
||||||
|
let menu_visible = self.is_completions_menu_visible(cx);
|
||||||
|
let add_context_button = IconButton::new("add-context", IconName::AtSign)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.when(!menu_visible, |this| {
|
||||||
|
this.tooltip(move |_window, cx| {
|
||||||
|
Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.trigger_completion_menu(window, cx);
|
||||||
|
}));
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.key_context("PromptEditor")
|
.key_context("PromptEditor")
|
||||||
.capture_action(cx.listener(Self::paste))
|
.capture_action(cx.listener(Self::paste))
|
||||||
@@ -114,7 +127,6 @@ impl<T: 'static> Render for PromptEditor<T> {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.items_start()
|
.items_start()
|
||||||
.cursor(CursorStyle::Arrow)
|
.cursor(CursorStyle::Arrow)
|
||||||
.on_action(cx.listener(Self::toggle_context_picker))
|
|
||||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||||
this.model_selector
|
this.model_selector
|
||||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||||
@@ -123,7 +135,6 @@ impl<T: 'static> Render for PromptEditor<T> {
|
|||||||
.on_action(cx.listener(Self::cancel))
|
.on_action(cx.listener(Self::cancel))
|
||||||
.on_action(cx.listener(Self::move_up))
|
.on_action(cx.listener(Self::move_up))
|
||||||
.on_action(cx.listener(Self::move_down))
|
.on_action(cx.listener(Self::move_down))
|
||||||
.on_action(cx.listener(Self::remove_all_context))
|
|
||||||
.capture_action(cx.listener(Self::cycle_prev))
|
.capture_action(cx.listener(Self::cycle_prev))
|
||||||
.capture_action(cx.listener(Self::cycle_next))
|
.capture_action(cx.listener(Self::cycle_next))
|
||||||
.child(
|
.child(
|
||||||
@@ -182,7 +193,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
|||||||
.pl_1()
|
.pl_1()
|
||||||
.items_start()
|
.items_start()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(self.context_strip.clone())
|
.child(add_context_button)
|
||||||
.child(self.model_selector.clone()),
|
.child(self.model_selector.clone()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -214,6 +225,19 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn assign_completion_provider(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.set_completion_provider(Some(Rc::new(PromptCompletionProvider::new(
|
||||||
|
PromptEditorCompletionProviderDelegate,
|
||||||
|
cx.weak_entity(),
|
||||||
|
self.mention_set.clone(),
|
||||||
|
self.history_store.clone(),
|
||||||
|
self.prompt_store.clone(),
|
||||||
|
self.workspace.clone(),
|
||||||
|
))));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_show_cursor_when_unfocused(
|
pub fn set_show_cursor_when_unfocused(
|
||||||
&mut self,
|
&mut self,
|
||||||
show_cursor_when_unfocused: bool,
|
show_cursor_when_unfocused: bool,
|
||||||
@@ -226,27 +250,40 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
|
|
||||||
pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let prompt = self.prompt(cx);
|
let prompt = self.prompt(cx);
|
||||||
let existing_creases = self.editor.update(cx, extract_message_creases);
|
let existing_creases = self.editor.update(cx, |editor, cx| {
|
||||||
|
extract_message_creases(editor, &self.mention_set, window, cx)
|
||||||
|
});
|
||||||
let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
|
let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
|
||||||
|
let mut creases = vec![];
|
||||||
self.editor = cx.new(|cx| {
|
self.editor = cx.new(|cx| {
|
||||||
let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
|
let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
|
||||||
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
||||||
editor.set_placeholder_text("Add a prompt…", window, cx);
|
editor.set_placeholder_text("Add a prompt…", window, cx);
|
||||||
editor.set_text(prompt, window, cx);
|
editor.set_text(prompt, window, cx);
|
||||||
insert_message_creases(
|
creases = insert_message_creases(&mut editor, &existing_creases, window, cx);
|
||||||
&mut editor,
|
|
||||||
&existing_creases,
|
|
||||||
&self.context_store,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
if focus {
|
if focus {
|
||||||
window.focus(&editor.focus_handle(cx));
|
window.focus(&editor.focus_handle(cx));
|
||||||
}
|
}
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.mention_set.update(cx, |mention_set, _cx| {
|
||||||
|
debug_assert_eq!(
|
||||||
|
creases.len(),
|
||||||
|
mention_set.creases().len(),
|
||||||
|
"Missing creases"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mentions = mention_set
|
||||||
|
.clear()
|
||||||
|
.zip(creases)
|
||||||
|
.map(|((_, value), id)| (id, value))
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
mention_set.set_mentions(mentions);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.assign_completion_provider(cx);
|
||||||
self.subscribe_to_editor(window, cx);
|
self.subscribe_to_editor(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,43 +311,29 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
self.editor.read(cx).text(cx)
|
self.editor.read(cx).text(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
|
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let images = cx
|
if inline_assistant_model_supports_images(cx)
|
||||||
.read_from_clipboard()
|
&& let Some(task) =
|
||||||
.map(|item| {
|
paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
|
||||||
item.into_entries()
|
{
|
||||||
.filter_map(|entry| {
|
task.detach();
|
||||||
if let ClipboardEntry::Image(image) = entry {
|
|
||||||
Some(image)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if images.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
cx.stop_propagation();
|
|
||||||
|
|
||||||
self.context_store.update(cx, |store, cx| {
|
|
||||||
for image in images {
|
|
||||||
store.add_image_instance(Arc::new(image), cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_prompt_editor_events(
|
fn handle_prompt_editor_events(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &Entity<Editor>,
|
editor: &Entity<Editor>,
|
||||||
event: &EditorEvent,
|
event: &EditorEvent,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
EditorEvent::Edited { .. } => {
|
EditorEvent::Edited { .. } => {
|
||||||
|
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||||
|
|
||||||
|
self.mention_set
|
||||||
|
.update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
|
||||||
|
|
||||||
if let Some(workspace) = window.root::<Workspace>().flatten() {
|
if let Some(workspace) = window.root::<Workspace>().flatten() {
|
||||||
workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
|
let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
|
||||||
@@ -321,7 +344,7 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
.log_edit_event("inline assist", is_via_ssh);
|
.log_edit_event("inline assist", is_via_ssh);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let prompt = self.editor.read(cx).text(cx);
|
let prompt = snapshot.text();
|
||||||
if self
|
if self
|
||||||
.prompt_history_ix
|
.prompt_history_ix
|
||||||
.is_none_or(|ix| self.prompt_history[ix] != prompt)
|
.is_none_or(|ix| self.prompt_history[ix] != prompt)
|
||||||
@@ -343,23 +366,44 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_context_picker(
|
pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
|
||||||
&mut self,
|
self.editor
|
||||||
_: &ToggleContextPicker,
|
.read(cx)
|
||||||
window: &mut Window,
|
.context_menu()
|
||||||
cx: &mut Context<Self>,
|
.borrow()
|
||||||
) {
|
.as_ref()
|
||||||
self.context_picker_menu_handle.toggle(window, cx);
|
.is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_all_context(
|
pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
&mut self,
|
self.editor.update(cx, |editor, cx| {
|
||||||
_: &RemoveAllContext,
|
let menu_is_open = editor.context_menu().borrow().as_ref().is_some_and(|menu| {
|
||||||
_window: &mut Window,
|
matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
|
||||||
cx: &mut Context<Self>,
|
});
|
||||||
) {
|
|
||||||
self.context_store.update(cx, |store, cx| store.clear(cx));
|
let has_at_sign = {
|
||||||
cx.notify();
|
let snapshot = editor.display_snapshot(cx);
|
||||||
|
let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
|
||||||
|
let offset = cursor.to_offset(&snapshot);
|
||||||
|
if offset.0 > 0 {
|
||||||
|
snapshot
|
||||||
|
.buffer_snapshot()
|
||||||
|
.reversed_chars_at(offset)
|
||||||
|
.next()
|
||||||
|
.map(|sign| sign == '@')
|
||||||
|
.unwrap_or(false)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if menu_is_open && has_at_sign {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.insert("@", window, cx);
|
||||||
|
editor.show_completions(&editor::actions::ShowCompletions, window, cx);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cancel(
|
fn cancel(
|
||||||
@@ -434,8 +478,6 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
editor.move_to_end(&Default::default(), window, cx)
|
editor.move_to_end(&Default::default(), window, cx)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if self.context_strip.read(cx).has_context_items(cx) {
|
|
||||||
self.context_strip.focus_handle(cx).focus(window);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,6 +751,7 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
EditorStyle {
|
EditorStyle {
|
||||||
background: colors.editor_background,
|
background: colors.editor_background,
|
||||||
local_player: cx.theme().players().local(),
|
local_player: cx.theme().players().local(),
|
||||||
|
syntax: cx.theme().syntax().clone(),
|
||||||
text: text_style,
|
text: text_style,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -716,21 +759,6 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
})
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_context_strip_event(
|
|
||||||
&mut self,
|
|
||||||
_context_strip: &Entity<ContextStrip>,
|
|
||||||
event: &ContextStripEvent,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
match event {
|
|
||||||
ContextStripEvent::PickerDismissed
|
|
||||||
| ContextStripEvent::BlurredEmpty
|
|
||||||
| ContextStripEvent::BlurredUp => self.editor.focus_handle(cx).focus(window),
|
|
||||||
ContextStripEvent::BlurredDown => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum PromptEditorMode {
|
pub enum PromptEditorMode {
|
||||||
@@ -765,6 +793,36 @@ impl InlineAssistId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PromptEditorCompletionProviderDelegate;
|
||||||
|
|
||||||
|
fn inline_assistant_model_supports_images(cx: &App) -> bool {
|
||||||
|
LanguageModelRegistry::read_global(cx)
|
||||||
|
.inline_assistant_model()
|
||||||
|
.map_or(false, |m| m.model.supports_images())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PromptCompletionProviderDelegate for PromptEditorCompletionProviderDelegate {
|
||||||
|
fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> {
|
||||||
|
vec![
|
||||||
|
PromptContextType::File,
|
||||||
|
PromptContextType::Symbol,
|
||||||
|
PromptContextType::Thread,
|
||||||
|
PromptContextType::Fetch,
|
||||||
|
PromptContextType::Rules,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_images(&self, cx: &App) -> bool {
|
||||||
|
inline_assistant_model_supports_images(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm_command(&self, _cx: &mut App) {}
|
||||||
|
}
|
||||||
|
|
||||||
impl PromptEditor<BufferCodegen> {
|
impl PromptEditor<BufferCodegen> {
|
||||||
pub fn new_buffer(
|
pub fn new_buffer(
|
||||||
id: InlineAssistId,
|
id: InlineAssistId,
|
||||||
@@ -773,15 +831,14 @@ impl PromptEditor<BufferCodegen> {
|
|||||||
prompt_buffer: Entity<MultiBuffer>,
|
prompt_buffer: Entity<MultiBuffer>,
|
||||||
codegen: Entity<BufferCodegen>,
|
codegen: Entity<BufferCodegen>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
context_store: Entity<ContextStore>,
|
history_store: Entity<HistoryStore>,
|
||||||
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
|
project: WeakEntity<Project>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
|
||||||
prompt_store: Option<WeakEntity<PromptStore>>,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<PromptEditor<BufferCodegen>>,
|
cx: &mut Context<PromptEditor<BufferCodegen>>,
|
||||||
) -> PromptEditor<BufferCodegen> {
|
) -> PromptEditor<BufferCodegen> {
|
||||||
let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
|
let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
|
||||||
let codegen_buffer = codegen.read(cx).buffer(cx).read(cx).as_singleton();
|
|
||||||
let mode = PromptEditorMode::Buffer {
|
let mode = PromptEditorMode::Buffer {
|
||||||
id,
|
id,
|
||||||
codegen,
|
codegen,
|
||||||
@@ -805,7 +862,6 @@ impl PromptEditor<BufferCodegen> {
|
|||||||
// typing in one will make what you typed appear in all of them.
|
// typing in one will make what you typed appear in all of them.
|
||||||
editor.set_show_cursor_when_unfocused(true, cx);
|
editor.set_show_cursor_when_unfocused(true, cx);
|
||||||
editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
|
editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
|
||||||
editor.register_addon(ContextCreasesAddon::new());
|
|
||||||
editor.set_context_menu_options(ContextMenuOptions {
|
editor.set_context_menu_options(ContextMenuOptions {
|
||||||
min_entries_visible: 12,
|
min_entries_visible: 12,
|
||||||
max_entries_visible: 12,
|
max_entries_visible: 12,
|
||||||
@@ -815,43 +871,17 @@ impl PromptEditor<BufferCodegen> {
|
|||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
let prompt_editor_entity = prompt_editor.downgrade();
|
let mention_set =
|
||||||
prompt_editor.update(cx, |editor, _| {
|
cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
|
||||||
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
|
||||||
workspace.clone(),
|
|
||||||
context_store.downgrade(),
|
|
||||||
thread_store.clone(),
|
|
||||||
prompt_store.clone(),
|
|
||||||
prompt_editor_entity,
|
|
||||||
codegen_buffer.as_ref().map(Entity::downgrade),
|
|
||||||
))));
|
|
||||||
});
|
|
||||||
|
|
||||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
|
||||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||||
|
|
||||||
let context_strip = cx.new(|cx| {
|
|
||||||
ContextStrip::new(
|
|
||||||
context_store.clone(),
|
|
||||||
workspace.clone(),
|
|
||||||
thread_store.clone(),
|
|
||||||
prompt_store,
|
|
||||||
context_picker_menu_handle.clone(),
|
|
||||||
SuggestContextKind::Thread,
|
|
||||||
ModelUsageContext::InlineAssistant,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let context_strip_subscription =
|
|
||||||
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
|
|
||||||
|
|
||||||
let mut this: PromptEditor<BufferCodegen> = PromptEditor {
|
let mut this: PromptEditor<BufferCodegen> = PromptEditor {
|
||||||
editor: prompt_editor.clone(),
|
editor: prompt_editor.clone(),
|
||||||
context_store,
|
mention_set,
|
||||||
context_strip,
|
history_store,
|
||||||
context_picker_menu_handle,
|
prompt_store,
|
||||||
|
workspace,
|
||||||
model_selector: cx.new(|cx| {
|
model_selector: cx.new(|cx| {
|
||||||
AgentModelSelector::new(
|
AgentModelSelector::new(
|
||||||
fs,
|
fs,
|
||||||
@@ -868,12 +898,12 @@ impl PromptEditor<BufferCodegen> {
|
|||||||
pending_prompt: String::new(),
|
pending_prompt: String::new(),
|
||||||
_codegen_subscription: codegen_subscription,
|
_codegen_subscription: codegen_subscription,
|
||||||
editor_subscriptions: Vec::new(),
|
editor_subscriptions: Vec::new(),
|
||||||
_context_strip_subscription: context_strip_subscription,
|
|
||||||
show_rate_limit_notice: false,
|
show_rate_limit_notice: false,
|
||||||
mode,
|
mode,
|
||||||
_phantom: Default::default(),
|
_phantom: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.assign_completion_provider(cx);
|
||||||
this.subscribe_to_editor(window, cx);
|
this.subscribe_to_editor(window, cx);
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
@@ -919,6 +949,10 @@ impl PromptEditor<BufferCodegen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mention_set(&self) -> &Entity<MentionSet> {
|
||||||
|
&self.mention_set
|
||||||
|
}
|
||||||
|
|
||||||
pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
|
pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
|
||||||
match &self.mode {
|
match &self.mode {
|
||||||
PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
|
PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
|
||||||
@@ -945,10 +979,10 @@ impl PromptEditor<TerminalCodegen> {
|
|||||||
prompt_buffer: Entity<MultiBuffer>,
|
prompt_buffer: Entity<MultiBuffer>,
|
||||||
codegen: Entity<TerminalCodegen>,
|
codegen: Entity<TerminalCodegen>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
context_store: Entity<ContextStore>,
|
history_store: Entity<HistoryStore>,
|
||||||
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
|
project: WeakEntity<Project>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
|
||||||
prompt_store: Option<WeakEntity<PromptStore>>,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -980,43 +1014,17 @@ impl PromptEditor<TerminalCodegen> {
|
|||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
let prompt_editor_entity = prompt_editor.downgrade();
|
let mention_set =
|
||||||
prompt_editor.update(cx, |editor, _| {
|
cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
|
||||||
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
|
||||||
workspace.clone(),
|
|
||||||
context_store.downgrade(),
|
|
||||||
thread_store.clone(),
|
|
||||||
prompt_store.clone(),
|
|
||||||
prompt_editor_entity,
|
|
||||||
None,
|
|
||||||
))));
|
|
||||||
});
|
|
||||||
|
|
||||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
|
||||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||||
|
|
||||||
let context_strip = cx.new(|cx| {
|
|
||||||
ContextStrip::new(
|
|
||||||
context_store.clone(),
|
|
||||||
workspace.clone(),
|
|
||||||
thread_store.clone(),
|
|
||||||
prompt_store.clone(),
|
|
||||||
context_picker_menu_handle.clone(),
|
|
||||||
SuggestContextKind::Thread,
|
|
||||||
ModelUsageContext::InlineAssistant,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let context_strip_subscription =
|
|
||||||
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
|
|
||||||
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
editor: prompt_editor.clone(),
|
editor: prompt_editor.clone(),
|
||||||
context_store,
|
mention_set,
|
||||||
context_strip,
|
history_store,
|
||||||
context_picker_menu_handle,
|
prompt_store,
|
||||||
|
workspace,
|
||||||
model_selector: cx.new(|cx| {
|
model_selector: cx.new(|cx| {
|
||||||
AgentModelSelector::new(
|
AgentModelSelector::new(
|
||||||
fs,
|
fs,
|
||||||
@@ -1033,12 +1041,12 @@ impl PromptEditor<TerminalCodegen> {
|
|||||||
pending_prompt: String::new(),
|
pending_prompt: String::new(),
|
||||||
_codegen_subscription: codegen_subscription,
|
_codegen_subscription: codegen_subscription,
|
||||||
editor_subscriptions: Vec::new(),
|
editor_subscriptions: Vec::new(),
|
||||||
_context_strip_subscription: context_strip_subscription,
|
|
||||||
mode,
|
mode,
|
||||||
show_rate_limit_notice: false,
|
show_rate_limit_notice: false,
|
||||||
_phantom: Default::default(),
|
_phantom: Default::default(),
|
||||||
};
|
};
|
||||||
this.count_lines(cx);
|
this.count_lines(cx);
|
||||||
|
this.assign_completion_provider(cx);
|
||||||
this.subscribe_to_editor(window, cx);
|
this.subscribe_to_editor(window, cx);
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
@@ -1085,6 +1093,10 @@ impl PromptEditor<TerminalCodegen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mention_set(&self) -> &Entity<MentionSet> {
|
||||||
|
&self.mention_set
|
||||||
|
}
|
||||||
|
|
||||||
pub fn codegen(&self) -> &Entity<TerminalCodegen> {
|
pub fn codegen(&self) -> &Entity<TerminalCodegen> {
|
||||||
match &self.mode {
|
match &self.mode {
|
||||||
PromptEditorMode::Buffer { .. } => unreachable!(),
|
PromptEditorMode::Buffer { .. } => unreachable!(),
|
||||||
@@ -1164,131 +1176,41 @@ impl GenerationMode {
|
|||||||
|
|
||||||
/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
|
/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct MessageCrease {
|
struct MessageCrease {
|
||||||
pub range: Range<MultiBufferOffset>,
|
range: Range<MultiBufferOffset>,
|
||||||
pub icon_path: SharedString,
|
icon_path: SharedString,
|
||||||
pub label: SharedString,
|
label: SharedString,
|
||||||
/// None for a deserialized message, Some otherwise.
|
|
||||||
pub context: Option<AgentContextHandle>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
fn extract_message_creases(
|
||||||
pub struct ContextCreasesAddon {
|
|
||||||
creases: HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>>,
|
|
||||||
_subscription: Option<Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Addon for ContextCreasesAddon {
|
|
||||||
fn to_any(&self) -> &dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
|
|
||||||
Some(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContextCreasesAddon {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
creases: HashMap::default(),
|
|
||||||
_subscription: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_creases(
|
|
||||||
&mut self,
|
|
||||||
context_store: &Entity<ContextStore>,
|
|
||||||
key: AgentContextKey,
|
|
||||||
creases: impl IntoIterator<Item = (CreaseId, SharedString)>,
|
|
||||||
cx: &mut Context<Editor>,
|
|
||||||
) {
|
|
||||||
self.creases.entry(key).or_default().extend(creases);
|
|
||||||
self._subscription = Some(
|
|
||||||
cx.subscribe(context_store, |editor, _, event, cx| match event {
|
|
||||||
ContextStoreEvent::ContextRemoved(key) => {
|
|
||||||
let Some(this) = editor.addon_mut::<Self>() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let (crease_ids, replacement_texts): (Vec<_>, Vec<_>) = this
|
|
||||||
.creases
|
|
||||||
.remove(key)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.unzip();
|
|
||||||
let ranges = editor
|
|
||||||
.remove_creases(crease_ids, cx)
|
|
||||||
.into_iter()
|
|
||||||
.map(|(_, range)| range)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
editor.unfold_ranges(&ranges, false, false, cx);
|
|
||||||
editor.edit(ranges.into_iter().zip(replacement_texts), cx);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn into_inner(self) -> HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>> {
|
|
||||||
self.creases
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extract_message_creases(
|
|
||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
cx: &mut Context<'_, Editor>,
|
mention_set: &Entity<MentionSet>,
|
||||||
) -> Vec<MessageCrease> {
|
|
||||||
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
||||||
let mut contexts_by_crease_id = editor
|
|
||||||
.addon_mut::<ContextCreasesAddon>()
|
|
||||||
.map(std::mem::take)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_inner()
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|(key, creases)| {
|
|
||||||
let context = key.0;
|
|
||||||
creases
|
|
||||||
.into_iter()
|
|
||||||
.map(move |(id, _)| (id, context.clone()))
|
|
||||||
})
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
// Filter the addon's list of creases based on what the editor reports,
|
|
||||||
// since the addon might have removed creases in it.
|
|
||||||
|
|
||||||
editor.display_map.update(cx, |display_map, cx| {
|
|
||||||
display_map
|
|
||||||
.snapshot(cx)
|
|
||||||
.crease_snapshot
|
|
||||||
.creases()
|
|
||||||
.filter_map(|(id, crease)| {
|
|
||||||
Some((
|
|
||||||
id,
|
|
||||||
(
|
|
||||||
crease.range().to_offset(&buffer_snapshot),
|
|
||||||
crease.metadata()?.clone(),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.map(|(id, (range, metadata))| {
|
|
||||||
let context = contexts_by_crease_id.remove(&id);
|
|
||||||
MessageCrease {
|
|
||||||
range,
|
|
||||||
context,
|
|
||||||
label: metadata.label,
|
|
||||||
icon_path: metadata.icon_path,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_message_creases(
|
|
||||||
editor: &mut Editor,
|
|
||||||
message_creases: &[MessageCrease],
|
|
||||||
context_store: &Entity<ContextStore>,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<'_, Editor>,
|
cx: &mut Context<'_, Editor>,
|
||||||
) {
|
) -> Vec<MessageCrease> {
|
||||||
|
let creases = mention_set.read(cx).creases();
|
||||||
|
let snapshot = editor.snapshot(window, cx);
|
||||||
|
snapshot
|
||||||
|
.crease_snapshot
|
||||||
|
.creases()
|
||||||
|
.filter(|(id, _)| creases.contains(id))
|
||||||
|
.filter_map(|(_, crease)| {
|
||||||
|
let metadata = crease.metadata()?.clone();
|
||||||
|
Some(MessageCrease {
|
||||||
|
range: crease.range().to_offset(snapshot.buffer()),
|
||||||
|
label: metadata.label,
|
||||||
|
icon_path: metadata.icon_path,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_message_creases(
|
||||||
|
editor: &mut Editor,
|
||||||
|
message_creases: &[MessageCrease],
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<'_, Editor>,
|
||||||
|
) -> Vec<CreaseId> {
|
||||||
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
|
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
let creases = message_creases
|
let creases = message_creases
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1305,12 +1227,5 @@ pub fn insert_message_creases(
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let ids = editor.insert_creases(creases.clone(), cx);
|
let ids = editor.insert_creases(creases.clone(), cx);
|
||||||
editor.fold_creases(creases, false, window, cx);
|
editor.fold_creases(creases, false, window, cx);
|
||||||
if let Some(addon) = editor.addon_mut::<ContextCreasesAddon>() {
|
ids
|
||||||
for (crease, id) in message_creases.iter().zip(ids) {
|
|
||||||
if let Some(context) = crease.context.as_ref() {
|
|
||||||
let key = AgentContextKey(context.clone());
|
|
||||||
addon.add_creases(context_store, key, vec![(id, crease.label.clone())], cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ use std::{cmp::Reverse, sync::Arc};
|
|||||||
|
|
||||||
use collections::IndexMap;
|
use collections::IndexMap;
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||||
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
|
use gpui::{
|
||||||
|
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
|
||||||
|
};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
||||||
LanguageModelRegistry,
|
LanguageModelRegistry,
|
||||||
};
|
};
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use ui::{ListItem, ListItemSpacing, prelude::*};
|
use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*};
|
||||||
|
use zed_actions::agent::OpenSettings;
|
||||||
|
|
||||||
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
|
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
|
||||||
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
|
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
|
||||||
@@ -20,6 +23,7 @@ pub fn language_model_selector(
|
|||||||
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
||||||
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
||||||
popover_styles: bool,
|
popover_styles: bool,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<LanguageModelSelector>,
|
cx: &mut Context<LanguageModelSelector>,
|
||||||
) -> LanguageModelSelector {
|
) -> LanguageModelSelector {
|
||||||
@@ -27,6 +31,7 @@ pub fn language_model_selector(
|
|||||||
get_active_model,
|
get_active_model,
|
||||||
on_model_changed,
|
on_model_changed,
|
||||||
popover_styles,
|
popover_styles,
|
||||||
|
focus_handle,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -88,6 +93,7 @@ pub struct LanguageModelPickerDelegate {
|
|||||||
_authenticate_all_providers_task: Task<()>,
|
_authenticate_all_providers_task: Task<()>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
popover_styles: bool,
|
popover_styles: bool,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LanguageModelPickerDelegate {
|
impl LanguageModelPickerDelegate {
|
||||||
@@ -95,6 +101,7 @@ impl LanguageModelPickerDelegate {
|
|||||||
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
||||||
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
||||||
popover_styles: bool,
|
popover_styles: bool,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Picker<Self>>,
|
cx: &mut Context<Picker<Self>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -128,6 +135,7 @@ impl LanguageModelPickerDelegate {
|
|||||||
},
|
},
|
||||||
)],
|
)],
|
||||||
popover_styles,
|
popover_styles,
|
||||||
|
focus_handle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,6 +529,8 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
|||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Picker<Self>>,
|
cx: &mut Context<Picker<Self>>,
|
||||||
) -> Option<gpui::AnyElement> {
|
) -> Option<gpui::AnyElement> {
|
||||||
|
let focus_handle = self.focus_handle.clone();
|
||||||
|
|
||||||
if !self.popover_styles {
|
if !self.popover_styles {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -528,22 +538,19 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
|||||||
Some(
|
Some(
|
||||||
h_flex()
|
h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
.p_1p5()
|
||||||
.border_t_1()
|
.border_t_1()
|
||||||
.border_color(cx.theme().colors().border_variant)
|
.border_color(cx.theme().colors().border_variant)
|
||||||
.p_1()
|
|
||||||
.gap_4()
|
|
||||||
.justify_between()
|
|
||||||
.child(
|
.child(
|
||||||
Button::new("configure", "Configure")
|
Button::new("configure", "Configure")
|
||||||
.icon(IconName::Settings)
|
.full_width()
|
||||||
.icon_size(IconSize::Small)
|
.style(ButtonStyle::Outlined)
|
||||||
.icon_color(Color::Muted)
|
.key_binding(
|
||||||
.icon_position(IconPosition::Start)
|
KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx)
|
||||||
|
.map(|kb| kb.size(rems_from_px(12.))),
|
||||||
|
)
|
||||||
.on_click(|_, window, cx| {
|
.on_click(|_, window, cx| {
|
||||||
window.dispatch_action(
|
window.dispatch_action(OpenSettings.boxed_clone(), cx);
|
||||||
zed_actions::agent::OpenSettings.boxed_clone(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.into_any(),
|
.into_any(),
|
||||||
|
|||||||
1098
crates/agent_ui/src/mention_set.rs
Normal file
1098
crates/agent_ui/src/mention_set.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
context::load_context,
|
context::load_context,
|
||||||
context_store::ContextStore,
|
|
||||||
inline_prompt_editor::{
|
inline_prompt_editor::{
|
||||||
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
|
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
|
||||||
},
|
},
|
||||||
@@ -73,8 +72,8 @@ impl TerminalInlineAssistant {
|
|||||||
terminal_view: &Entity<TerminalView>,
|
terminal_view: &Entity<TerminalView>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
|
thread_store: Entity<HistoryStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
|
||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
@@ -87,7 +86,6 @@ impl TerminalInlineAssistant {
|
|||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let context_store = cx.new(|_cx| ContextStore::new(project));
|
|
||||||
let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
|
let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
|
||||||
|
|
||||||
let prompt_editor = cx.new(|cx| {
|
let prompt_editor = cx.new(|cx| {
|
||||||
@@ -97,10 +95,10 @@ impl TerminalInlineAssistant {
|
|||||||
prompt_buffer.clone(),
|
prompt_buffer.clone(),
|
||||||
codegen,
|
codegen,
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
context_store.clone(),
|
|
||||||
workspace.clone(),
|
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
prompt_store.as_ref().map(|s| s.downgrade()),
|
prompt_store.clone(),
|
||||||
|
project.clone(),
|
||||||
|
workspace.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -119,8 +117,6 @@ impl TerminalInlineAssistant {
|
|||||||
terminal_view,
|
terminal_view,
|
||||||
prompt_editor,
|
prompt_editor,
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
context_store,
|
|
||||||
prompt_store,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -227,6 +223,10 @@ impl TerminalInlineAssistant {
|
|||||||
assist_id: TerminalInlineAssistId,
|
assist_id: TerminalInlineAssistId,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Result<Task<LanguageModelRequest>> {
|
) -> Result<Task<LanguageModelRequest>> {
|
||||||
|
let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
|
||||||
|
.inline_assistant_model()
|
||||||
|
.context("No inline assistant model")?;
|
||||||
|
|
||||||
let assist = self.assists.get(&assist_id).context("invalid assist")?;
|
let assist = self.assists.get(&assist_id).context("invalid assist")?;
|
||||||
|
|
||||||
let shell = std::env::var("SHELL").ok();
|
let shell = std::env::var("SHELL").ok();
|
||||||
@@ -243,45 +243,31 @@ impl TerminalInlineAssistant {
|
|||||||
.ok()
|
.ok()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let prompt_editor = assist.prompt_editor.clone().context("invalid assist")?;
|
||||||
|
|
||||||
let prompt = self.prompt_builder.generate_terminal_assistant_prompt(
|
let prompt = self.prompt_builder.generate_terminal_assistant_prompt(
|
||||||
&assist
|
&prompt_editor.read(cx).prompt(cx),
|
||||||
.prompt_editor
|
|
||||||
.clone()
|
|
||||||
.context("invalid assist")?
|
|
||||||
.read(cx)
|
|
||||||
.prompt(cx),
|
|
||||||
shell.as_deref(),
|
shell.as_deref(),
|
||||||
working_directory.as_deref(),
|
working_directory.as_deref(),
|
||||||
&latest_output,
|
&latest_output,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let contexts = assist
|
|
||||||
.context_store
|
|
||||||
.read(cx)
|
|
||||||
.context()
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let context_load_task = assist.workspace.update(cx, |workspace, cx| {
|
|
||||||
let project = workspace.project();
|
|
||||||
load_context(contexts, project, &assist.prompt_store, cx)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
|
|
||||||
.inline_assistant_model()
|
|
||||||
.context("No inline assistant model")?;
|
|
||||||
|
|
||||||
let temperature = AgentSettings::temperature_for_model(&model, cx);
|
let temperature = AgentSettings::temperature_for_model(&model, cx);
|
||||||
|
|
||||||
|
let mention_set = prompt_editor.read(cx).mention_set().clone();
|
||||||
|
let load_context_task = load_context(&mention_set, cx);
|
||||||
|
|
||||||
Ok(cx.background_spawn(async move {
|
Ok(cx.background_spawn(async move {
|
||||||
let mut request_message = LanguageModelRequestMessage {
|
let mut request_message = LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![],
|
content: vec![],
|
||||||
cache: false,
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
context_load_task
|
if let Some(context) = load_context_task.await {
|
||||||
.await
|
context.add_to_request_message(&mut request_message);
|
||||||
.add_to_request_message(&mut request_message);
|
}
|
||||||
|
|
||||||
request_message.content.push(prompt.into());
|
request_message.content.push(prompt.into());
|
||||||
|
|
||||||
@@ -409,8 +395,6 @@ struct TerminalInlineAssist {
|
|||||||
prompt_editor: Option<Entity<PromptEditor<TerminalCodegen>>>,
|
prompt_editor: Option<Entity<PromptEditor<TerminalCodegen>>>,
|
||||||
codegen: Entity<TerminalCodegen>,
|
codegen: Entity<TerminalCodegen>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
context_store: Entity<ContextStore>,
|
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,8 +404,6 @@ impl TerminalInlineAssist {
|
|||||||
terminal: &Entity<TerminalView>,
|
terminal: &Entity<TerminalView>,
|
||||||
prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
|
prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
context_store: Entity<ContextStore>,
|
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -431,8 +413,6 @@ impl TerminalInlineAssist {
|
|||||||
prompt_editor: Some(prompt_editor.clone()),
|
prompt_editor: Some(prompt_editor.clone()),
|
||||||
codegen: codegen.clone(),
|
codegen: codegen.clone(),
|
||||||
workspace,
|
workspace,
|
||||||
context_store,
|
|
||||||
prompt_store,
|
|
||||||
_subscriptions: vec![
|
_subscriptions: vec![
|
||||||
window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
|
window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
|
||||||
TerminalInlineAssistant::update_global(cx, |this, cx| {
|
TerminalInlineAssistant::update_global(cx, |this, cx| {
|
||||||
|
|||||||
@@ -280,6 +280,8 @@ impl TextThreadEditor {
|
|||||||
.thought_process_output_sections()
|
.thought_process_output_sections()
|
||||||
.to_vec();
|
.to_vec();
|
||||||
let slash_commands = text_thread.read(cx).slash_commands().clone();
|
let slash_commands = text_thread.read(cx).slash_commands().clone();
|
||||||
|
let focus_handle = editor.read(cx).focus_handle(cx);
|
||||||
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
text_thread,
|
text_thread,
|
||||||
slash_commands,
|
slash_commands,
|
||||||
@@ -315,6 +317,7 @@ impl TextThreadEditor {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
true, // Use popover styles for picker
|
true, // Use popover styles for picker
|
||||||
|
focus_handle,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ mod acp_onboarding_modal;
|
|||||||
mod agent_notification;
|
mod agent_notification;
|
||||||
mod burn_mode_tooltip;
|
mod burn_mode_tooltip;
|
||||||
mod claude_code_onboarding_modal;
|
mod claude_code_onboarding_modal;
|
||||||
mod context_pill;
|
|
||||||
mod end_trial_upsell;
|
mod end_trial_upsell;
|
||||||
mod hold_for_default;
|
mod hold_for_default;
|
||||||
mod onboarding_modal;
|
mod onboarding_modal;
|
||||||
@@ -13,7 +12,6 @@ pub use acp_onboarding_modal::*;
|
|||||||
pub use agent_notification::*;
|
pub use agent_notification::*;
|
||||||
pub use burn_mode_tooltip::*;
|
pub use burn_mode_tooltip::*;
|
||||||
pub use claude_code_onboarding_modal::*;
|
pub use claude_code_onboarding_modal::*;
|
||||||
pub use context_pill::*;
|
|
||||||
pub use end_trial_upsell::*;
|
pub use end_trial_upsell::*;
|
||||||
pub use hold_for_default::*;
|
pub use hold_for_default::*;
|
||||||
pub use onboarding_modal::*;
|
pub use onboarding_modal::*;
|
||||||
|
|||||||
@@ -1,858 +0,0 @@
|
|||||||
use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
|
|
||||||
|
|
||||||
use file_icons::FileIcons;
|
|
||||||
use futures::FutureExt as _;
|
|
||||||
use gpui::{
|
|
||||||
Animation, AnimationExt as _, AnyView, ClickEvent, Entity, Image, MouseButton, Task,
|
|
||||||
pulsating_between,
|
|
||||||
};
|
|
||||||
use language_model::LanguageModelImage;
|
|
||||||
use project::Project;
|
|
||||||
use prompt_store::PromptStore;
|
|
||||||
use rope::Point;
|
|
||||||
use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
|
|
||||||
use util::paths::PathStyle;
|
|
||||||
|
|
||||||
use crate::context::{
|
|
||||||
AgentContextHandle, ContextId, ContextKind, DirectoryContextHandle, FetchedUrlContext,
|
|
||||||
FileContextHandle, ImageContext, ImageStatus, RulesContextHandle, SelectionContextHandle,
|
|
||||||
SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub enum ContextPill {
|
|
||||||
Added {
|
|
||||||
context: AddedContext,
|
|
||||||
dupe_name: bool,
|
|
||||||
focused: bool,
|
|
||||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
|
||||||
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
|
||||||
},
|
|
||||||
Suggested {
|
|
||||||
name: SharedString,
|
|
||||||
icon_path: Option<SharedString>,
|
|
||||||
kind: ContextKind,
|
|
||||||
focused: bool,
|
|
||||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContextPill {
|
|
||||||
pub fn added(
|
|
||||||
context: AddedContext,
|
|
||||||
dupe_name: bool,
|
|
||||||
focused: bool,
|
|
||||||
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
|
||||||
) -> Self {
|
|
||||||
Self::Added {
|
|
||||||
context,
|
|
||||||
dupe_name,
|
|
||||||
on_remove,
|
|
||||||
focused,
|
|
||||||
on_click: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn suggested(
|
|
||||||
name: SharedString,
|
|
||||||
icon_path: Option<SharedString>,
|
|
||||||
kind: ContextKind,
|
|
||||||
focused: bool,
|
|
||||||
) -> Self {
|
|
||||||
Self::Suggested {
|
|
||||||
name,
|
|
||||||
icon_path,
|
|
||||||
kind,
|
|
||||||
focused,
|
|
||||||
on_click: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_click(mut self, listener: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>) -> Self {
|
|
||||||
match &mut self {
|
|
||||||
ContextPill::Added { on_click, .. } => {
|
|
||||||
*on_click = Some(listener);
|
|
||||||
}
|
|
||||||
ContextPill::Suggested { on_click, .. } => {
|
|
||||||
*on_click = Some(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self) -> ElementId {
|
|
||||||
match self {
|
|
||||||
Self::Added { context, .. } => context.handle.element_id("context-pill".into()),
|
|
||||||
Self::Suggested { .. } => "suggested-context-pill".into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn icon(&self) -> Icon {
|
|
||||||
match self {
|
|
||||||
Self::Suggested {
|
|
||||||
icon_path: Some(icon_path),
|
|
||||||
..
|
|
||||||
} => Icon::from_path(icon_path),
|
|
||||||
Self::Suggested { kind, .. } => Icon::new(kind.icon()),
|
|
||||||
Self::Added { context, .. } => context.icon(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for ContextPill {
|
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
let color = cx.theme().colors();
|
|
||||||
|
|
||||||
let base_pill = h_flex()
|
|
||||||
.id(self.id())
|
|
||||||
.pl_1()
|
|
||||||
.pb(px(1.))
|
|
||||||
.border_1()
|
|
||||||
.rounded_sm()
|
|
||||||
.gap_1()
|
|
||||||
.child(self.icon().size(IconSize::XSmall).color(Color::Muted));
|
|
||||||
|
|
||||||
match &self {
|
|
||||||
ContextPill::Added {
|
|
||||||
context,
|
|
||||||
dupe_name,
|
|
||||||
on_remove,
|
|
||||||
focused,
|
|
||||||
on_click,
|
|
||||||
} => {
|
|
||||||
let status_is_error = matches!(context.status, ContextStatus::Error { .. });
|
|
||||||
let status_is_warning = matches!(context.status, ContextStatus::Warning { .. });
|
|
||||||
|
|
||||||
base_pill
|
|
||||||
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
|
|
||||||
.map(|pill| {
|
|
||||||
if status_is_error {
|
|
||||||
pill.bg(cx.theme().status().error_background)
|
|
||||||
.border_color(cx.theme().status().error_border)
|
|
||||||
} else if status_is_warning {
|
|
||||||
pill.bg(cx.theme().status().warning_background)
|
|
||||||
.border_color(cx.theme().status().warning_border)
|
|
||||||
} else if *focused {
|
|
||||||
pill.bg(color.element_background)
|
|
||||||
.border_color(color.border_focused)
|
|
||||||
} else {
|
|
||||||
pill.bg(color.element_background)
|
|
||||||
.border_color(color.border.opacity(0.5))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.id("context-data")
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
div().max_w_64().child(
|
|
||||||
Label::new(context.name.clone())
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.truncate(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.when_some(context.parent.as_ref(), |element, parent_name| {
|
|
||||||
if *dupe_name {
|
|
||||||
element.child(
|
|
||||||
Label::new(parent_name.clone())
|
|
||||||
.size(LabelSize::XSmall)
|
|
||||||
.color(Color::Muted),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
element
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.when_some(context.tooltip.as_ref(), |element, tooltip| {
|
|
||||||
element.tooltip(Tooltip::text(tooltip.clone()))
|
|
||||||
})
|
|
||||||
.map(|element| match &context.status {
|
|
||||||
ContextStatus::Ready => element
|
|
||||||
.when_some(
|
|
||||||
context.render_hover.as_ref(),
|
|
||||||
|element, render_hover| {
|
|
||||||
let render_hover = render_hover.clone();
|
|
||||||
element.hoverable_tooltip(move |window, cx| {
|
|
||||||
render_hover(window, cx)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.into_any(),
|
|
||||||
ContextStatus::Loading { message } => element
|
|
||||||
.tooltip(ui::Tooltip::text(message.clone()))
|
|
||||||
.with_animation(
|
|
||||||
"pulsating-ctx-pill",
|
|
||||||
Animation::new(Duration::from_secs(2))
|
|
||||||
.repeat()
|
|
||||||
.with_easing(pulsating_between(0.4, 0.8)),
|
|
||||||
|label, delta| label.opacity(delta),
|
|
||||||
)
|
|
||||||
.into_any_element(),
|
|
||||||
ContextStatus::Warning { message }
|
|
||||||
| ContextStatus::Error { message } => element
|
|
||||||
.tooltip(ui::Tooltip::text(message.clone()))
|
|
||||||
.into_any_element(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.when_some(on_remove.as_ref(), |element, on_remove| {
|
|
||||||
element.child(
|
|
||||||
IconButton::new(
|
|
||||||
context.handle.element_id("remove".into()),
|
|
||||||
IconName::Close,
|
|
||||||
)
|
|
||||||
.shape(IconButtonShape::Square)
|
|
||||||
.icon_size(IconSize::XSmall)
|
|
||||||
.tooltip(Tooltip::text("Remove Context"))
|
|
||||||
.on_click({
|
|
||||||
let on_remove = on_remove.clone();
|
|
||||||
move |event, window, cx| on_remove(event, window, cx)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when_some(on_click.as_ref(), |element, on_click| {
|
|
||||||
let on_click = on_click.clone();
|
|
||||||
element.cursor_pointer().on_click(move |event, window, cx| {
|
|
||||||
on_click(event, window, cx);
|
|
||||||
cx.stop_propagation();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
ContextPill::Suggested {
|
|
||||||
name,
|
|
||||||
icon_path: _,
|
|
||||||
kind: _,
|
|
||||||
focused,
|
|
||||||
on_click,
|
|
||||||
} => base_pill
|
|
||||||
.cursor_pointer()
|
|
||||||
.pr_1()
|
|
||||||
.border_dashed()
|
|
||||||
.map(|pill| {
|
|
||||||
if *focused {
|
|
||||||
pill.border_color(color.border_focused)
|
|
||||||
.bg(color.element_background.opacity(0.5))
|
|
||||||
} else {
|
|
||||||
pill.border_color(color.border)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
|
|
||||||
.child(
|
|
||||||
div().max_w_64().child(
|
|
||||||
Label::new(name.clone())
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.color(Color::Muted)
|
|
||||||
.truncate(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.tooltip(|_window, cx| {
|
|
||||||
Tooltip::with_meta("Suggested Context", None, "Click to add it", cx)
|
|
||||||
})
|
|
||||||
.when_some(on_click.as_ref(), |element, on_click| {
|
|
||||||
let on_click = on_click.clone();
|
|
||||||
element.on_click(move |event, window, cx| {
|
|
||||||
on_click(event, window, cx);
|
|
||||||
cx.stop_propagation();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.into_any(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ContextStatus {
|
|
||||||
Ready,
|
|
||||||
Loading { message: SharedString },
|
|
||||||
Error { message: SharedString },
|
|
||||||
Warning { message: SharedString },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(RegisterComponent)]
|
|
||||||
pub struct AddedContext {
|
|
||||||
pub handle: AgentContextHandle,
|
|
||||||
pub kind: ContextKind,
|
|
||||||
pub name: SharedString,
|
|
||||||
pub parent: Option<SharedString>,
|
|
||||||
pub tooltip: Option<SharedString>,
|
|
||||||
pub icon_path: Option<SharedString>,
|
|
||||||
pub status: ContextStatus,
|
|
||||||
pub render_hover: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AddedContext {
|
|
||||||
pub fn icon(&self) -> Icon {
|
|
||||||
match &self.status {
|
|
||||||
ContextStatus::Warning { .. } => Icon::new(IconName::Warning).color(Color::Warning),
|
|
||||||
ContextStatus::Error { .. } => Icon::new(IconName::XCircle).color(Color::Error),
|
|
||||||
_ => {
|
|
||||||
if let Some(icon_path) = &self.icon_path {
|
|
||||||
Icon::from_path(icon_path)
|
|
||||||
} else {
|
|
||||||
Icon::new(self.kind.icon())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Creates an `AddedContext` by retrieving relevant details of `AgentContext`. This returns a
|
|
||||||
/// `None` if `DirectoryContext` or `RulesContext` no longer exist.
|
|
||||||
///
|
|
||||||
/// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak.
|
|
||||||
pub fn new_pending(
|
|
||||||
handle: AgentContextHandle,
|
|
||||||
prompt_store: Option<&Entity<PromptStore>>,
|
|
||||||
project: &Project,
|
|
||||||
model: Option<&Arc<dyn language_model::LanguageModel>>,
|
|
||||||
cx: &App,
|
|
||||||
) -> Option<AddedContext> {
|
|
||||||
match handle {
|
|
||||||
AgentContextHandle::File(handle) => {
|
|
||||||
Self::pending_file(handle, project.path_style(cx), cx)
|
|
||||||
}
|
|
||||||
AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
|
|
||||||
AgentContextHandle::Symbol(handle) => {
|
|
||||||
Self::pending_symbol(handle, project.path_style(cx), cx)
|
|
||||||
}
|
|
||||||
AgentContextHandle::Selection(handle) => {
|
|
||||||
Self::pending_selection(handle, project.path_style(cx), cx)
|
|
||||||
}
|
|
||||||
AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
|
|
||||||
AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
|
|
||||||
AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)),
|
|
||||||
AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
|
|
||||||
AgentContextHandle::Image(handle) => {
|
|
||||||
Some(Self::image(handle, model, project.path_style(cx), cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pending_file(
|
|
||||||
handle: FileContextHandle,
|
|
||||||
path_style: PathStyle,
|
|
||||||
cx: &App,
|
|
||||||
) -> Option<AddedContext> {
|
|
||||||
let full_path = handle
|
|
||||||
.buffer
|
|
||||||
.read(cx)
|
|
||||||
.file()?
|
|
||||||
.full_path(cx)
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string();
|
|
||||||
Some(Self::file(handle, &full_path, path_style, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn file(
|
|
||||||
handle: FileContextHandle,
|
|
||||||
full_path: &str,
|
|
||||||
path_style: PathStyle,
|
|
||||||
cx: &App,
|
|
||||||
) -> AddedContext {
|
|
||||||
let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
|
|
||||||
AddedContext {
|
|
||||||
kind: ContextKind::File,
|
|
||||||
name,
|
|
||||||
parent,
|
|
||||||
tooltip: Some(SharedString::new(full_path)),
|
|
||||||
icon_path: FileIcons::get_icon(Path::new(full_path), cx),
|
|
||||||
status: ContextStatus::Ready,
|
|
||||||
render_hover: None,
|
|
||||||
handle: AgentContextHandle::File(handle),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pending_directory(
|
|
||||||
handle: DirectoryContextHandle,
|
|
||||||
project: &Project,
|
|
||||||
cx: &App,
|
|
||||||
) -> Option<AddedContext> {
|
|
||||||
let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
|
|
||||||
let entry = worktree.entry_for_id(handle.entry_id)?;
|
|
||||||
let full_path = worktree
|
|
||||||
.full_path(&entry.path)
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string();
|
|
||||||
Some(Self::directory(handle, &full_path, project.path_style(cx)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn directory(
|
|
||||||
handle: DirectoryContextHandle,
|
|
||||||
full_path: &str,
|
|
||||||
path_style: PathStyle,
|
|
||||||
) -> AddedContext {
|
|
||||||
let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
|
|
||||||
AddedContext {
|
|
||||||
kind: ContextKind::Directory,
|
|
||||||
name,
|
|
||||||
parent,
|
|
||||||
tooltip: Some(SharedString::new(full_path)),
|
|
||||||
icon_path: None,
|
|
||||||
status: ContextStatus::Ready,
|
|
||||||
render_hover: None,
|
|
||||||
handle: AgentContextHandle::Directory(handle),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pending_symbol(
|
|
||||||
handle: SymbolContextHandle,
|
|
||||||
path_style: PathStyle,
|
|
||||||
cx: &App,
|
|
||||||
) -> Option<AddedContext> {
|
|
||||||
let excerpt = ContextFileExcerpt::new(
|
|
||||||
&handle.full_path(cx)?.to_string_lossy(),
|
|
||||||
handle.enclosing_line_range(cx),
|
|
||||||
path_style,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
Some(AddedContext {
|
|
||||||
kind: ContextKind::Symbol,
|
|
||||||
name: handle.symbol.clone(),
|
|
||||||
parent: Some(excerpt.file_name_and_range.clone()),
|
|
||||||
tooltip: None,
|
|
||||||
icon_path: None,
|
|
||||||
status: ContextStatus::Ready,
|
|
||||||
render_hover: {
|
|
||||||
let handle = handle.clone();
|
|
||||||
Some(Rc::new(move |_, cx| {
|
|
||||||
excerpt.hover_view(handle.text(cx), cx).into()
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
handle: AgentContextHandle::Symbol(handle),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pending_selection(
|
|
||||||
handle: SelectionContextHandle,
|
|
||||||
path_style: PathStyle,
|
|
||||||
cx: &App,
|
|
||||||
) -> Option<AddedContext> {
|
|
||||||
let excerpt = ContextFileExcerpt::new(
|
|
||||||
&handle.full_path(cx)?.to_string_lossy(),
|
|
||||||
handle.line_range(cx),
|
|
||||||
path_style,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
Some(AddedContext {
|
|
||||||
kind: ContextKind::Selection,
|
|
||||||
name: excerpt.file_name_and_range.clone(),
|
|
||||||
parent: excerpt.parent_name.clone(),
|
|
||||||
tooltip: None,
|
|
||||||
icon_path: excerpt.icon_path.clone(),
|
|
||||||
status: ContextStatus::Ready,
|
|
||||||
render_hover: {
|
|
||||||
let handle = handle.clone();
|
|
||||||
Some(Rc::new(move |_, cx| {
|
|
||||||
excerpt.hover_view(handle.text(cx), cx).into()
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
handle: AgentContextHandle::Selection(handle),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetched_url(context: FetchedUrlContext) -> AddedContext {
|
|
||||||
AddedContext {
|
|
||||||
kind: ContextKind::FetchedUrl,
|
|
||||||
name: context.url.clone(),
|
|
||||||
parent: None,
|
|
||||||
tooltip: None,
|
|
||||||
icon_path: None,
|
|
||||||
status: ContextStatus::Ready,
|
|
||||||
render_hover: None,
|
|
||||||
handle: AgentContextHandle::FetchedUrl(context),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pending_thread(handle: ThreadContextHandle, cx: &App) -> AddedContext {
|
|
||||||
AddedContext {
|
|
||||||
kind: ContextKind::Thread,
|
|
||||||
name: handle.title(cx),
|
|
||||||
parent: None,
|
|
||||||
tooltip: None,
|
|
||||||
icon_path: None,
|
|
||||||
status: if handle.thread.read(cx).is_generating_summary() {
|
|
||||||
ContextStatus::Loading {
|
|
||||||
message: "Summarizing…".into(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ContextStatus::Ready
|
|
||||||
},
|
|
||||||
render_hover: {
|
|
||||||
let thread = handle.thread.clone();
|
|
||||||
Some(Rc::new(move |_, cx| {
|
|
||||||
let text = thread
|
|
||||||
.update(cx, |thread, cx| thread.summary(cx))
|
|
||||||
.now_or_never()
|
|
||||||
.flatten()
|
|
||||||
.unwrap_or_else(|| SharedString::from(thread.read(cx).to_markdown()));
|
|
||||||
ContextPillHover::new_text(text, cx).into()
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
handle: AgentContextHandle::Thread(handle),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pending_text_thread(handle: TextThreadContextHandle, cx: &App) -> AddedContext {
|
|
||||||
AddedContext {
|
|
||||||
kind: ContextKind::TextThread,
|
|
||||||
name: handle.title(cx),
|
|
||||||
parent: None,
|
|
||||||
tooltip: None,
|
|
||||||
icon_path: None,
|
|
||||||
status: ContextStatus::Ready,
|
|
||||||
render_hover: {
|
|
||||||
let text_thread = handle.text_thread.clone();
|
|
||||||
Some(Rc::new(move |_, cx| {
|
|
||||||
let text = text_thread.read(cx).to_xml(cx);
|
|
||||||
ContextPillHover::new_text(text.into(), cx).into()
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
handle: AgentContextHandle::TextThread(handle),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pending_rules(
|
|
||||||
handle: RulesContextHandle,
|
|
||||||
prompt_store: Option<&Entity<PromptStore>>,
|
|
||||||
cx: &App,
|
|
||||||
) -> Option<AddedContext> {
|
|
||||||
let title = prompt_store
|
|
||||||
.as_ref()?
|
|
||||||
.read(cx)
|
|
||||||
.metadata(handle.prompt_id.into())?
|
|
||||||
.title
|
|
||||||
.unwrap_or_else(|| "Unnamed Rule".into());
|
|
||||||
Some(AddedContext {
|
|
||||||
kind: ContextKind::Rules,
|
|
||||||
name: title,
|
|
||||||
parent: None,
|
|
||||||
tooltip: None,
|
|
||||||
icon_path: None,
|
|
||||||
status: ContextStatus::Ready,
|
|
||||||
render_hover: None,
|
|
||||||
handle: AgentContextHandle::Rules(handle),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn image(
|
|
||||||
context: ImageContext,
|
|
||||||
model: Option<&Arc<dyn language_model::LanguageModel>>,
|
|
||||||
path_style: PathStyle,
|
|
||||||
cx: &App,
|
|
||||||
) -> AddedContext {
|
|
||||||
let (name, parent, icon_path) = if let Some(full_path) = context.full_path.as_ref() {
|
|
||||||
let (name, parent) =
|
|
||||||
extract_file_name_and_directory_from_full_path(full_path, path_style);
|
|
||||||
let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
|
|
||||||
(name, parent, icon_path)
|
|
||||||
} else {
|
|
||||||
("Image".into(), None, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
let status = match context.status(model) {
|
|
||||||
ImageStatus::Loading => ContextStatus::Loading {
|
|
||||||
message: "Loading…".into(),
|
|
||||||
},
|
|
||||||
ImageStatus::Error => ContextStatus::Error {
|
|
||||||
message: "Failed to load Image".into(),
|
|
||||||
},
|
|
||||||
ImageStatus::Warning => ContextStatus::Warning {
|
|
||||||
message: format!(
|
|
||||||
"{} doesn't support attaching Images as Context",
|
|
||||||
model.map(|m| m.name().0).unwrap_or_else(|| "Model".into())
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
},
|
|
||||||
ImageStatus::Ready => ContextStatus::Ready,
|
|
||||||
};
|
|
||||||
|
|
||||||
AddedContext {
|
|
||||||
kind: ContextKind::Image,
|
|
||||||
name,
|
|
||||||
parent,
|
|
||||||
tooltip: None,
|
|
||||||
icon_path,
|
|
||||||
status,
|
|
||||||
render_hover: Some(Rc::new({
|
|
||||||
let image = context.original_image.clone();
|
|
||||||
move |_, cx| {
|
|
||||||
let image = image.clone();
|
|
||||||
ContextPillHover::new(cx, move |_, _| {
|
|
||||||
gpui::img(image.clone())
|
|
||||||
.max_w_96()
|
|
||||||
.max_h_96()
|
|
||||||
.into_any_element()
|
|
||||||
})
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
handle: AgentContextHandle::Image(context),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_file_name_and_directory_from_full_path(
|
|
||||||
path: &str,
|
|
||||||
path_style: PathStyle,
|
|
||||||
) -> (SharedString, Option<SharedString>) {
|
|
||||||
let (parent, file_name) = path_style.split(path);
|
|
||||||
let parent = parent.and_then(|parent| {
|
|
||||||
let parent = parent.trim_end_matches(path_style.separator());
|
|
||||||
let (_, parent) = path_style.split(parent);
|
|
||||||
if parent.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(SharedString::new(parent))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
(SharedString::new(file_name), parent)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct ContextFileExcerpt {
|
|
||||||
pub file_name_and_range: SharedString,
|
|
||||||
pub full_path_and_range: SharedString,
|
|
||||||
pub parent_name: Option<SharedString>,
|
|
||||||
pub icon_path: Option<SharedString>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContextFileExcerpt {
|
|
||||||
pub fn new(full_path: &str, line_range: Range<Point>, path_style: PathStyle, cx: &App) -> Self {
|
|
||||||
let (parent, file_name) = path_style.split(full_path);
|
|
||||||
let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
|
|
||||||
let mut full_path_and_range = full_path.to_owned();
|
|
||||||
full_path_and_range.push_str(&line_range_text);
|
|
||||||
let mut file_name_and_range = file_name.to_owned();
|
|
||||||
file_name_and_range.push_str(&line_range_text);
|
|
||||||
|
|
||||||
let parent_name = parent.and_then(|parent| {
|
|
||||||
let parent = parent.trim_end_matches(path_style.separator());
|
|
||||||
let (_, parent) = path_style.split(parent);
|
|
||||||
if parent.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(SharedString::new(parent))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
|
|
||||||
|
|
||||||
ContextFileExcerpt {
|
|
||||||
file_name_and_range: file_name_and_range.into(),
|
|
||||||
full_path_and_range: full_path_and_range.into(),
|
|
||||||
parent_name,
|
|
||||||
icon_path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hover_view(&self, text: SharedString, cx: &mut App) -> Entity<ContextPillHover> {
|
|
||||||
let icon_path = self.icon_path.clone();
|
|
||||||
let full_path_and_range = self.full_path_and_range.clone();
|
|
||||||
ContextPillHover::new(cx, move |_, cx| {
|
|
||||||
v_flex()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_0p5()
|
|
||||||
.w_full()
|
|
||||||
.max_w_full()
|
|
||||||
.border_b_1()
|
|
||||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
|
||||||
.children(
|
|
||||||
icon_path
|
|
||||||
.clone()
|
|
||||||
.map(Icon::from_path)
|
|
||||||
.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
// TODO: make this truncate on the left.
|
|
||||||
Label::new(full_path_and_range.clone())
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.ml_1(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.id("context-pill-hover-contents")
|
|
||||||
.overflow_scroll()
|
|
||||||
.max_w_128()
|
|
||||||
.max_h_96()
|
|
||||||
.child(Label::new(text.clone()).buffer_font(cx)),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ContextPillHover {
|
|
||||||
render_hover: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContextPillHover {
|
|
||||||
fn new(
|
|
||||||
cx: &mut App,
|
|
||||||
render_hover: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
|
|
||||||
) -> Entity<Self> {
|
|
||||||
cx.new(|_| Self {
|
|
||||||
render_hover: Box::new(render_hover),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_text(content: SharedString, cx: &mut App) -> Entity<Self> {
|
|
||||||
Self::new(cx, move |_, _| {
|
|
||||||
div()
|
|
||||||
.id("context-pill-hover-contents")
|
|
||||||
.overflow_scroll()
|
|
||||||
.max_w_128()
|
|
||||||
.max_h_96()
|
|
||||||
.child(content.clone())
|
|
||||||
.into_any_element()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ContextPillHover {
|
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
tooltip_container(cx, move |this, cx| {
|
|
||||||
this.occlude()
|
|
||||||
.on_mouse_move(|_, _, cx| cx.stop_propagation())
|
|
||||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
|
||||||
.child((self.render_hover)(window, cx))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for AddedContext {
|
|
||||||
fn scope() -> ComponentScope {
|
|
||||||
ComponentScope::Agent
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sort_name() -> &'static str {
|
|
||||||
"AddedContext"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
|
||||||
let mut next_context_id = ContextId::zero();
|
|
||||||
let image_ready = (
|
|
||||||
"Ready",
|
|
||||||
AddedContext::image(
|
|
||||||
ImageContext {
|
|
||||||
context_id: next_context_id.post_inc(),
|
|
||||||
project_path: None,
|
|
||||||
full_path: None,
|
|
||||||
original_image: Arc::new(Image::empty()),
|
|
||||||
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
PathStyle::local(),
|
|
||||||
cx,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
let image_loading = (
|
|
||||||
"Loading",
|
|
||||||
AddedContext::image(
|
|
||||||
ImageContext {
|
|
||||||
context_id: next_context_id.post_inc(),
|
|
||||||
project_path: None,
|
|
||||||
full_path: None,
|
|
||||||
original_image: Arc::new(Image::empty()),
|
|
||||||
image_task: cx
|
|
||||||
.background_spawn(async move {
|
|
||||||
smol::Timer::after(Duration::from_secs(60 * 5)).await;
|
|
||||||
Some(LanguageModelImage::empty())
|
|
||||||
})
|
|
||||||
.shared(),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
PathStyle::local(),
|
|
||||||
cx,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
let image_error = (
|
|
||||||
"Error",
|
|
||||||
AddedContext::image(
|
|
||||||
ImageContext {
|
|
||||||
context_id: next_context_id.post_inc(),
|
|
||||||
project_path: None,
|
|
||||||
full_path: None,
|
|
||||||
original_image: Arc::new(Image::empty()),
|
|
||||||
image_task: Task::ready(None).shared(),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
PathStyle::local(),
|
|
||||||
cx,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Some(
|
|
||||||
v_flex()
|
|
||||||
.gap_6()
|
|
||||||
.children(
|
|
||||||
vec![image_ready, image_loading, image_error]
|
|
||||||
.into_iter()
|
|
||||||
.map(|(text, context)| {
|
|
||||||
single_example(
|
|
||||||
text,
|
|
||||||
ContextPill::added(context, false, false, None).into_any_element(),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.into_any(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use gpui::App;
|
|
||||||
use language_model::{LanguageModel, fake_provider::FakeLanguageModel};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn test_image_context_warning_for_unsupported_model(cx: &mut App) {
|
|
||||||
let model: Arc<dyn LanguageModel> = Arc::new(FakeLanguageModel::default());
|
|
||||||
assert!(!model.supports_images());
|
|
||||||
|
|
||||||
let image_context = ImageContext {
|
|
||||||
context_id: ContextId::zero(),
|
|
||||||
project_path: None,
|
|
||||||
original_image: Arc::new(Image::empty()),
|
|
||||||
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
|
|
||||||
full_path: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let added_context =
|
|
||||||
AddedContext::image(image_context, Some(&model), PathStyle::local(), cx);
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
added_context.status,
|
|
||||||
ContextStatus::Warning { .. }
|
|
||||||
));
|
|
||||||
|
|
||||||
assert!(matches!(added_context.kind, ContextKind::Image));
|
|
||||||
assert_eq!(added_context.name.as_ref(), "Image");
|
|
||||||
assert!(added_context.parent.is_none());
|
|
||||||
assert!(added_context.icon_path.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn test_image_context_ready_for_no_model(cx: &mut App) {
|
|
||||||
let image_context = ImageContext {
|
|
||||||
context_id: ContextId::zero(),
|
|
||||||
project_path: None,
|
|
||||||
original_image: Arc::new(Image::empty()),
|
|
||||||
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
|
|
||||||
full_path: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let added_context = AddedContext::image(image_context, None, PathStyle::local(), cx);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
matches!(added_context.status, ContextStatus::Ready),
|
|
||||||
"Expected ready status when no model provided"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(matches!(added_context.kind, ContextKind::Image));
|
|
||||||
assert_eq!(added_context.name.as_ref(), "Image");
|
|
||||||
assert!(added_context.parent.is_none());
|
|
||||||
assert!(added_context.icon_path.is_none());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -67,6 +67,13 @@ pub enum Model {
|
|||||||
alias = "claude-opus-4-1-thinking-latest"
|
alias = "claude-opus-4-1-thinking-latest"
|
||||||
)]
|
)]
|
||||||
ClaudeOpus4_1Thinking,
|
ClaudeOpus4_1Thinking,
|
||||||
|
#[serde(rename = "claude-opus-4-5", alias = "claude-opus-4-5-latest")]
|
||||||
|
ClaudeOpus4_5,
|
||||||
|
#[serde(
|
||||||
|
rename = "claude-opus-4-5-thinking",
|
||||||
|
alias = "claude-opus-4-5-thinking-latest"
|
||||||
|
)]
|
||||||
|
ClaudeOpus4_5Thinking,
|
||||||
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
|
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
|
||||||
ClaudeSonnet4,
|
ClaudeSonnet4,
|
||||||
#[serde(
|
#[serde(
|
||||||
@@ -131,6 +138,14 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_id(id: &str) -> Result<Self> {
|
pub fn from_id(id: &str) -> Result<Self> {
|
||||||
|
if id.starts_with("claude-opus-4-5-thinking") {
|
||||||
|
return Ok(Self::ClaudeOpus4_5Thinking);
|
||||||
|
}
|
||||||
|
|
||||||
|
if id.starts_with("claude-opus-4-5") {
|
||||||
|
return Ok(Self::ClaudeOpus4_5);
|
||||||
|
}
|
||||||
|
|
||||||
if id.starts_with("claude-opus-4-1-thinking") {
|
if id.starts_with("claude-opus-4-1-thinking") {
|
||||||
return Ok(Self::ClaudeOpus4_1Thinking);
|
return Ok(Self::ClaudeOpus4_1Thinking);
|
||||||
}
|
}
|
||||||
@@ -208,6 +223,8 @@ impl Model {
|
|||||||
Self::ClaudeOpus4_1 => "claude-opus-4-1-latest",
|
Self::ClaudeOpus4_1 => "claude-opus-4-1-latest",
|
||||||
Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
|
Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
|
||||||
Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest",
|
Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest",
|
||||||
|
Self::ClaudeOpus4_5 => "claude-opus-4-5-latest",
|
||||||
|
Self::ClaudeOpus4_5Thinking => "claude-opus-4-5-thinking-latest",
|
||||||
Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
|
Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
|
||||||
Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
|
Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
|
||||||
Self::ClaudeSonnet4_5 => "claude-sonnet-4-5-latest",
|
Self::ClaudeSonnet4_5 => "claude-sonnet-4-5-latest",
|
||||||
@@ -230,6 +247,7 @@ impl Model {
|
|||||||
match self {
|
match self {
|
||||||
Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
|
Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
|
||||||
Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805",
|
Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805",
|
||||||
|
Self::ClaudeOpus4_5 | Self::ClaudeOpus4_5Thinking => "claude-opus-4-5-20251101",
|
||||||
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
|
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
|
||||||
Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking => "claude-sonnet-4-5-20250929",
|
Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking => "claude-sonnet-4-5-20250929",
|
||||||
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||||
@@ -249,6 +267,8 @@ impl Model {
|
|||||||
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
|
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
|
||||||
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
|
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
|
||||||
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
|
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
|
||||||
|
Self::ClaudeOpus4_5 => "Claude Opus 4.5",
|
||||||
|
Self::ClaudeOpus4_5Thinking => "Claude Opus 4.5 Thinking",
|
||||||
Self::ClaudeSonnet4 => "Claude Sonnet 4",
|
Self::ClaudeSonnet4 => "Claude Sonnet 4",
|
||||||
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
|
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
|
||||||
Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5",
|
Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5",
|
||||||
@@ -274,6 +294,8 @@ impl Model {
|
|||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking
|
||||||
|
| Self::ClaudeOpus4_5
|
||||||
|
| Self::ClaudeOpus4_5Thinking
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
@@ -303,6 +325,8 @@ impl Model {
|
|||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking
|
||||||
|
| Self::ClaudeOpus4_5
|
||||||
|
| Self::ClaudeOpus4_5Thinking
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
@@ -326,6 +350,8 @@ impl Model {
|
|||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking
|
||||||
|
| Self::ClaudeOpus4_5
|
||||||
|
| Self::ClaudeOpus4_5Thinking
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
@@ -348,6 +374,8 @@ impl Model {
|
|||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking
|
||||||
|
| Self::ClaudeOpus4_5
|
||||||
|
| Self::ClaudeOpus4_5Thinking
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
@@ -372,6 +400,7 @@ impl Model {
|
|||||||
match self {
|
match self {
|
||||||
Self::ClaudeOpus4
|
Self::ClaudeOpus4
|
||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
|
| Self::ClaudeOpus4_5
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
| Self::Claude3_5Sonnet
|
| Self::Claude3_5Sonnet
|
||||||
@@ -383,6 +412,7 @@ impl Model {
|
|||||||
| Self::Claude3Haiku => AnthropicModelMode::Default,
|
| Self::Claude3Haiku => AnthropicModelMode::Default,
|
||||||
Self::ClaudeOpus4Thinking
|
Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking
|
||||||
|
| Self::ClaudeOpus4_5Thinking
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeSonnet4_5Thinking
|
| Self::ClaudeSonnet4_5Thinking
|
||||||
| Self::ClaudeHaiku4_5Thinking
|
| Self::ClaudeHaiku4_5Thinking
|
||||||
@@ -393,7 +423,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn beta_headers(&self) -> String {
|
pub fn beta_headers(&self) -> Option<String> {
|
||||||
let mut headers = vec![];
|
let mut headers = vec![];
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
@@ -415,7 +445,11 @@ impl Model {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
headers.join(",")
|
if headers.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(headers.join(","))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tool_model_id(&self) -> &str {
|
pub fn tool_model_id(&self) -> &str {
|
||||||
@@ -431,56 +465,12 @@ impl Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn complete(
|
|
||||||
client: &dyn HttpClient,
|
|
||||||
api_url: &str,
|
|
||||||
api_key: &str,
|
|
||||||
request: Request,
|
|
||||||
beta_headers: String,
|
|
||||||
) -> Result<Response, AnthropicError> {
|
|
||||||
let uri = format!("{api_url}/v1/messages");
|
|
||||||
let request_builder = HttpRequest::builder()
|
|
||||||
.method(Method::POST)
|
|
||||||
.uri(uri)
|
|
||||||
.header("Anthropic-Version", "2023-06-01")
|
|
||||||
.header("Anthropic-Beta", beta_headers)
|
|
||||||
.header("X-Api-Key", api_key.trim())
|
|
||||||
.header("Content-Type", "application/json");
|
|
||||||
|
|
||||||
let serialized_request =
|
|
||||||
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
|
|
||||||
let request = request_builder
|
|
||||||
.body(AsyncBody::from(serialized_request))
|
|
||||||
.map_err(AnthropicError::BuildRequestBody)?;
|
|
||||||
|
|
||||||
let mut response = client
|
|
||||||
.send(request)
|
|
||||||
.await
|
|
||||||
.map_err(AnthropicError::HttpSend)?;
|
|
||||||
let status_code = response.status();
|
|
||||||
let mut body = String::new();
|
|
||||||
response
|
|
||||||
.body_mut()
|
|
||||||
.read_to_string(&mut body)
|
|
||||||
.await
|
|
||||||
.map_err(AnthropicError::ReadResponse)?;
|
|
||||||
|
|
||||||
if status_code.is_success() {
|
|
||||||
Ok(serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)?)
|
|
||||||
} else {
|
|
||||||
Err(AnthropicError::HttpResponseError {
|
|
||||||
status_code,
|
|
||||||
message: body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn stream_completion(
|
pub async fn stream_completion(
|
||||||
client: &dyn HttpClient,
|
client: &dyn HttpClient,
|
||||||
api_url: &str,
|
api_url: &str,
|
||||||
api_key: &str,
|
api_key: &str,
|
||||||
request: Request,
|
request: Request,
|
||||||
beta_headers: String,
|
beta_headers: Option<String>,
|
||||||
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
|
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
|
||||||
stream_completion_with_rate_limit_info(client, api_url, api_key, request, beta_headers)
|
stream_completion_with_rate_limit_info(client, api_url, api_key, request, beta_headers)
|
||||||
.await
|
.await
|
||||||
@@ -578,7 +568,7 @@ pub async fn stream_completion_with_rate_limit_info(
|
|||||||
api_url: &str,
|
api_url: &str,
|
||||||
api_key: &str,
|
api_key: &str,
|
||||||
request: Request,
|
request: Request,
|
||||||
beta_headers: String,
|
beta_headers: Option<String>,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
(
|
(
|
||||||
BoxStream<'static, Result<Event, AnthropicError>>,
|
BoxStream<'static, Result<Event, AnthropicError>>,
|
||||||
@@ -592,13 +582,17 @@ pub async fn stream_completion_with_rate_limit_info(
|
|||||||
};
|
};
|
||||||
let uri = format!("{api_url}/v1/messages");
|
let uri = format!("{api_url}/v1/messages");
|
||||||
|
|
||||||
let request_builder = HttpRequest::builder()
|
let mut request_builder = HttpRequest::builder()
|
||||||
.method(Method::POST)
|
.method(Method::POST)
|
||||||
.uri(uri)
|
.uri(uri)
|
||||||
.header("Anthropic-Version", "2023-06-01")
|
.header("Anthropic-Version", "2023-06-01")
|
||||||
.header("Anthropic-Beta", beta_headers)
|
|
||||||
.header("X-Api-Key", api_key.trim())
|
.header("X-Api-Key", api_key.trim())
|
||||||
.header("Content-Type", "application/json");
|
.header("Content-Type", "application/json");
|
||||||
|
|
||||||
|
if let Some(beta_headers) = beta_headers {
|
||||||
|
request_builder = request_builder.header("Anthropic-Beta", beta_headers);
|
||||||
|
}
|
||||||
|
|
||||||
let serialized_request =
|
let serialized_request =
|
||||||
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
|
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
|
||||||
let request = request_builder
|
let request = request_builder
|
||||||
|
|||||||
@@ -250,7 +250,6 @@ impl PasswordProxy {
|
|||||||
.await
|
.await
|
||||||
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
|
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
|
||||||
make_file_executable(&askpass_script_path).await?;
|
make_file_executable(&askpass_script_path).await?;
|
||||||
// todo(shell): There might be no powershell on the system
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let askpass_helper = format!(
|
let askpass_helper = format!(
|
||||||
"powershell.exe -ExecutionPolicy Bypass -File {}",
|
"powershell.exe -ExecutionPolicy Bypass -File {}",
|
||||||
@@ -375,7 +374,7 @@ fn generate_askpass_script(
|
|||||||
Ok(format!(
|
Ok(format!(
|
||||||
r#"
|
r#"
|
||||||
$ErrorActionPreference = 'Stop';
|
$ErrorActionPreference = 'Stop';
|
||||||
($args -join [char]0) | & {askpass_program} --askpass={askpass_socket} 2> $null
|
($args -join [char]0) | {askpass_program} --askpass={askpass_socket} 2> $null
|
||||||
"#,
|
"#,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ fs.workspace = true
|
|||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
language_model.workspace = true
|
language_model.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|||||||
@@ -880,10 +880,9 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
|||||||
let num_sections = rng.random_range(0..=3);
|
let num_sections = rng.random_range(0..=3);
|
||||||
let mut section_start = 0;
|
let mut section_start = 0;
|
||||||
for _ in 0..num_sections {
|
for _ in 0..num_sections {
|
||||||
let mut section_end = rng.random_range(section_start..=output_text.len());
|
let section_end = output_text.floor_char_boundary(
|
||||||
while !output_text.is_char_boundary(section_end) {
|
rng.random_range(section_start..=output_text.len()),
|
||||||
section_end += 1;
|
);
|
||||||
}
|
|
||||||
events.push(Ok(SlashCommandEvent::StartSection {
|
events.push(Ok(SlashCommandEvent::StartSection {
|
||||||
icon: IconName::Ai,
|
icon: IconName::Ai,
|
||||||
label: "section".into(),
|
label: "section".into(),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use gpui::{
|
|||||||
App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
|
App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
|
||||||
Task,
|
Task,
|
||||||
};
|
};
|
||||||
|
use itertools::Itertools as _;
|
||||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
||||||
@@ -1417,6 +1418,7 @@ impl TextThread {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Respond only with OK, nothing else.".into()],
|
content: vec!["Respond only with OK, nothing else.".into()],
|
||||||
cache: false,
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
});
|
});
|
||||||
req
|
req
|
||||||
};
|
};
|
||||||
@@ -1852,14 +1854,17 @@ impl TextThread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ensure_trailing_newline
|
if ensure_trailing_newline
|
||||||
&& buffer.contains_str_at(command_range_end, "\n")
|
&& buffer
|
||||||
|
.chars_at(command_range_end)
|
||||||
|
.next()
|
||||||
|
.is_some_and(|c| c == '\n')
|
||||||
{
|
{
|
||||||
let newline_offset = insert_position.saturating_sub(1);
|
if let Some((prev_char, '\n')) =
|
||||||
if buffer.contains_str_at(newline_offset, "\n")
|
buffer.reversed_chars_at(insert_position).next_tuple()
|
||||||
&& last_section_range.is_none_or(|last_section_range| {
|
&& last_section_range.is_none_or(|last_section_range| {
|
||||||
!last_section_range
|
!last_section_range
|
||||||
.to_offset(buffer)
|
.to_offset(buffer)
|
||||||
.contains(&newline_offset)
|
.contains(&(insert_position - prev_char.len_utf8()))
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
deletions.push((command_range_end..command_range_end + 1, ""));
|
deletions.push((command_range_end..command_range_end + 1, ""));
|
||||||
@@ -2085,6 +2090,11 @@ impl TextThread {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
LanguageModelCompletionEvent::StartMessage { .. } => {}
|
LanguageModelCompletionEvent::StartMessage { .. } => {}
|
||||||
|
LanguageModelCompletionEvent::ReasoningDetails(_) => {
|
||||||
|
// ReasoningDetails are metadata (signatures, encrypted data, format info)
|
||||||
|
// used for request/response validation, not UI content.
|
||||||
|
// The displayable thinking text is already handled by the Thinking event.
|
||||||
|
}
|
||||||
LanguageModelCompletionEvent::Stop(reason) => {
|
LanguageModelCompletionEvent::Stop(reason) => {
|
||||||
stop_reason = reason;
|
stop_reason = reason;
|
||||||
}
|
}
|
||||||
@@ -2308,6 +2318,7 @@ impl TextThread {
|
|||||||
role: message.role,
|
role: message.role,
|
||||||
content: Vec::new(),
|
content: Vec::new(),
|
||||||
cache: message.cache.as_ref().is_some_and(|cache| cache.is_anchor),
|
cache: message.cache.as_ref().is_some_and(|cache| cache.is_anchor),
|
||||||
|
reasoning_details: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
while let Some(content) = contents.peek() {
|
while let Some(content) = contents.peek() {
|
||||||
@@ -2679,6 +2690,7 @@ impl TextThread {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![SUMMARIZE_THREAD_PROMPT.into()],
|
content: vec![SUMMARIZE_THREAD_PROMPT.into()],
|
||||||
cache: false,
|
cache: false,
|
||||||
|
reasoning_details: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If there is no summary, it is set with `done: false` so that "Loading Summary…" can
|
// If there is no summary, it is set with `done: false` so that "Loading Summary…" can
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ http_client.workspace = true
|
|||||||
log.workspace = true
|
log.workspace = true
|
||||||
paths.workspace = true
|
paths.workspace = true
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
|
semver.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ use anyhow::{Context as _, Result};
|
|||||||
use client::Client;
|
use client::Client;
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, Global, SemanticVersion,
|
App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, Global, Task, Window,
|
||||||
Task, Window, actions,
|
actions,
|
||||||
};
|
};
|
||||||
use http_client::{HttpClient, HttpClientWithUrl};
|
use http_client::{HttpClient, HttpClientWithUrl};
|
||||||
use paths::remote_servers_dir;
|
use paths::remote_servers_dir;
|
||||||
use release_channel::{AppCommitSha, ReleaseChannel};
|
use release_channel::{AppCommitSha, ReleaseChannel};
|
||||||
|
use semver::Version;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{RegisterSetting, Settings, SettingsStore};
|
use settings::{RegisterSetting, Settings, SettingsStore};
|
||||||
use smol::fs::File;
|
use smol::fs::File;
|
||||||
@@ -44,7 +45,7 @@ actions!(
|
|||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum VersionCheckType {
|
pub enum VersionCheckType {
|
||||||
Sha(AppCommitSha),
|
Sha(AppCommitSha),
|
||||||
Semantic(SemanticVersion),
|
Semantic(Version),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
@@ -100,7 +101,7 @@ impl AutoUpdateStatus {
|
|||||||
|
|
||||||
pub struct AutoUpdater {
|
pub struct AutoUpdater {
|
||||||
status: AutoUpdateStatus,
|
status: AutoUpdateStatus,
|
||||||
current_version: SemanticVersion,
|
current_version: Version,
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
pending_poll: Option<Task<Option<()>>>,
|
pending_poll: Option<Task<Option<()>>>,
|
||||||
quit_subscription: Option<gpui::Subscription>,
|
quit_subscription: Option<gpui::Subscription>,
|
||||||
@@ -256,7 +257,7 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut App) -> Option<()> {
|
|||||||
match release_channel {
|
match release_channel {
|
||||||
ReleaseChannel::Stable | ReleaseChannel::Preview => {
|
ReleaseChannel::Stable | ReleaseChannel::Preview => {
|
||||||
let auto_updater = auto_updater.read(cx);
|
let auto_updater = auto_updater.read(cx);
|
||||||
let current_version = auto_updater.current_version;
|
let current_version = auto_updater.current_version.clone();
|
||||||
let release_channel = release_channel.dev_name();
|
let release_channel = release_channel.dev_name();
|
||||||
let path = format!("/releases/{release_channel}/{current_version}");
|
let path = format!("/releases/{release_channel}/{current_version}");
|
||||||
let url = &auto_updater.client.http_client().build_url(&path);
|
let url = &auto_updater.client.http_client().build_url(&path);
|
||||||
@@ -322,7 +323,7 @@ impl AutoUpdater {
|
|||||||
cx.default_global::<GlobalAutoUpdate>().0.clone()
|
cx.default_global::<GlobalAutoUpdate>().0.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(current_version: SemanticVersion, client: Arc<Client>, cx: &mut Context<Self>) -> Self {
|
fn new(current_version: Version, client: Arc<Client>, cx: &mut Context<Self>) -> Self {
|
||||||
// On windows, executable files cannot be overwritten while they are
|
// On windows, executable files cannot be overwritten while they are
|
||||||
// running, so we must wait to overwrite the application until quitting
|
// running, so we must wait to overwrite the application until quitting
|
||||||
// or restarting. When quitting the app, we spawn the auto update helper
|
// or restarting. When quitting the app, we spawn the auto update helper
|
||||||
@@ -400,8 +401,8 @@ impl AutoUpdater {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_version(&self) -> SemanticVersion {
|
pub fn current_version(&self) -> Version {
|
||||||
self.current_version
|
self.current_version.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status(&self) -> AutoUpdateStatus {
|
pub fn status(&self) -> AutoUpdateStatus {
|
||||||
@@ -422,7 +423,7 @@ impl AutoUpdater {
|
|||||||
// Ok(None).
|
// Ok(None).
|
||||||
pub async fn download_remote_server_release(
|
pub async fn download_remote_server_release(
|
||||||
release_channel: ReleaseChannel,
|
release_channel: ReleaseChannel,
|
||||||
version: Option<SemanticVersion>,
|
version: Option<Version>,
|
||||||
os: &str,
|
os: &str,
|
||||||
arch: &str,
|
arch: &str,
|
||||||
set_status: impl Fn(&str, &mut AsyncApp) + Send + 'static,
|
set_status: impl Fn(&str, &mut AsyncApp) + Send + 'static,
|
||||||
@@ -469,7 +470,7 @@ impl AutoUpdater {
|
|||||||
|
|
||||||
pub async fn get_remote_server_release_url(
|
pub async fn get_remote_server_release_url(
|
||||||
channel: ReleaseChannel,
|
channel: ReleaseChannel,
|
||||||
version: Option<SemanticVersion>,
|
version: Option<Version>,
|
||||||
os: &str,
|
os: &str,
|
||||||
arch: &str,
|
arch: &str,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
@@ -491,7 +492,7 @@ impl AutoUpdater {
|
|||||||
async fn get_release_asset(
|
async fn get_release_asset(
|
||||||
this: &Entity<Self>,
|
this: &Entity<Self>,
|
||||||
release_channel: ReleaseChannel,
|
release_channel: ReleaseChannel,
|
||||||
version: Option<SemanticVersion>,
|
version: Option<Version>,
|
||||||
asset: &str,
|
asset: &str,
|
||||||
os: &str,
|
os: &str,
|
||||||
arch: &str,
|
arch: &str,
|
||||||
@@ -554,7 +555,7 @@ impl AutoUpdater {
|
|||||||
this.read_with(cx, |this, cx| {
|
this.read_with(cx, |this, cx| {
|
||||||
(
|
(
|
||||||
this.client.http_client(),
|
this.client.http_client(),
|
||||||
this.current_version,
|
this.current_version.clone(),
|
||||||
this.status.clone(),
|
this.status.clone(),
|
||||||
ReleaseChannel::try_global(cx).unwrap_or(ReleaseChannel::Stable),
|
ReleaseChannel::try_global(cx).unwrap_or(ReleaseChannel::Stable),
|
||||||
)
|
)
|
||||||
@@ -627,16 +628,19 @@ impl AutoUpdater {
|
|||||||
fn check_if_fetched_version_is_newer(
|
fn check_if_fetched_version_is_newer(
|
||||||
release_channel: ReleaseChannel,
|
release_channel: ReleaseChannel,
|
||||||
app_commit_sha: Result<Option<String>>,
|
app_commit_sha: Result<Option<String>>,
|
||||||
installed_version: SemanticVersion,
|
installed_version: Version,
|
||||||
fetched_version: String,
|
fetched_version: String,
|
||||||
status: AutoUpdateStatus,
|
status: AutoUpdateStatus,
|
||||||
) -> Result<Option<VersionCheckType>> {
|
) -> Result<Option<VersionCheckType>> {
|
||||||
let parsed_fetched_version = fetched_version.parse::<SemanticVersion>();
|
let parsed_fetched_version = fetched_version.parse::<Version>();
|
||||||
|
|
||||||
if let AutoUpdateStatus::Updated { version, .. } = status {
|
if let AutoUpdateStatus::Updated { version, .. } = status {
|
||||||
match version {
|
match version {
|
||||||
VersionCheckType::Sha(cached_version) => {
|
VersionCheckType::Sha(cached_version) => {
|
||||||
let should_download = fetched_version != cached_version.full();
|
let should_download = parsed_fetched_version
|
||||||
|
.as_ref()
|
||||||
|
.ok()
|
||||||
|
.is_none_or(|version| version.build.as_str() != cached_version.full());
|
||||||
let newer_version = should_download
|
let newer_version = should_download
|
||||||
.then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
|
.then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
|
||||||
return Ok(newer_version);
|
return Ok(newer_version);
|
||||||
@@ -655,7 +659,12 @@ impl AutoUpdater {
|
|||||||
let should_download = app_commit_sha
|
let should_download = app_commit_sha
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|sha| fetched_version != sha)
|
.map(|sha| {
|
||||||
|
parsed_fetched_version
|
||||||
|
.as_ref()
|
||||||
|
.ok()
|
||||||
|
.is_none_or(|version| version.build.as_str() != sha)
|
||||||
|
})
|
||||||
.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
let newer_version = should_download
|
let newer_version = should_download
|
||||||
.then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
|
.then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
|
||||||
@@ -708,8 +717,8 @@ impl AutoUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn check_if_fetched_version_is_newer_non_nightly(
|
fn check_if_fetched_version_is_newer_non_nightly(
|
||||||
installed_version: SemanticVersion,
|
installed_version: Version,
|
||||||
fetched_version: SemanticVersion,
|
fetched_version: Version,
|
||||||
) -> Result<Option<VersionCheckType>> {
|
) -> Result<Option<VersionCheckType>> {
|
||||||
let should_download = fetched_version > installed_version;
|
let should_download = fetched_version > installed_version;
|
||||||
let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
|
let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
|
||||||
@@ -1020,7 +1029,7 @@ mod tests {
|
|||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
settings::init(cx);
|
settings::init(cx);
|
||||||
|
|
||||||
let current_version = SemanticVersion::new(0, 100, 0);
|
let current_version = semver::Version::new(0, 100, 0);
|
||||||
release_channel::init_test(current_version, ReleaseChannel::Stable, cx);
|
release_channel::init_test(current_version, ReleaseChannel::Stable, cx);
|
||||||
|
|
||||||
let clock = Arc::new(FakeSystemClock::new());
|
let clock = Arc::new(FakeSystemClock::new());
|
||||||
@@ -1059,7 +1068,7 @@ mod tests {
|
|||||||
|
|
||||||
auto_updater.read_with(cx, |updater, _| {
|
auto_updater.read_with(cx, |updater, _| {
|
||||||
assert_eq!(updater.status(), AutoUpdateStatus::Idle);
|
assert_eq!(updater.status(), AutoUpdateStatus::Idle);
|
||||||
assert_eq!(updater.current_version(), SemanticVersion::new(0, 100, 0));
|
assert_eq!(updater.current_version(), semver::Version::new(0, 100, 0));
|
||||||
});
|
});
|
||||||
|
|
||||||
release_available.store(true, atomic::Ordering::SeqCst);
|
release_available.store(true, atomic::Ordering::SeqCst);
|
||||||
@@ -1078,7 +1087,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
status,
|
status,
|
||||||
AutoUpdateStatus::Downloading {
|
AutoUpdateStatus::Downloading {
|
||||||
version: VersionCheckType::Semantic(SemanticVersion::new(0, 100, 1))
|
version: VersionCheckType::Semantic(semver::Version::new(0, 100, 1))
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1108,7 +1117,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
status,
|
status,
|
||||||
AutoUpdateStatus::Updated {
|
AutoUpdateStatus::Updated {
|
||||||
version: VersionCheckType::Semantic(SemanticVersion::new(0, 100, 1))
|
version: VersionCheckType::Semantic(semver::Version::new(0, 100, 1))
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
let will_restart = cx.expect_restart();
|
let will_restart = cx.expect_restart();
|
||||||
@@ -1122,9 +1131,9 @@ mod tests {
|
|||||||
fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
|
fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
|
||||||
let release_channel = ReleaseChannel::Stable;
|
let release_channel = ReleaseChannel::Stable;
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let installed_version = semver::Version::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Idle;
|
let status = AutoUpdateStatus::Idle;
|
||||||
let fetched_version = SemanticVersion::new(1, 0, 0);
|
let fetched_version = semver::Version::new(1, 0, 0);
|
||||||
|
|
||||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||||
release_channel,
|
release_channel,
|
||||||
@@ -1141,9 +1150,9 @@ mod tests {
|
|||||||
fn test_stable_does_update_when_fetched_version_is_higher() {
|
fn test_stable_does_update_when_fetched_version_is_higher() {
|
||||||
let release_channel = ReleaseChannel::Stable;
|
let release_channel = ReleaseChannel::Stable;
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let installed_version = semver::Version::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Idle;
|
let status = AutoUpdateStatus::Idle;
|
||||||
let fetched_version = SemanticVersion::new(1, 0, 1);
|
let fetched_version = semver::Version::new(1, 0, 1);
|
||||||
|
|
||||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||||
release_channel,
|
release_channel,
|
||||||
@@ -1163,11 +1172,11 @@ mod tests {
|
|||||||
fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
|
fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
|
||||||
let release_channel = ReleaseChannel::Stable;
|
let release_channel = ReleaseChannel::Stable;
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let installed_version = semver::Version::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
|
version: VersionCheckType::Semantic(semver::Version::new(1, 0, 1)),
|
||||||
};
|
};
|
||||||
let fetched_version = SemanticVersion::new(1, 0, 1);
|
let fetched_version = semver::Version::new(1, 0, 1);
|
||||||
|
|
||||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||||
release_channel,
|
release_channel,
|
||||||
@@ -1184,11 +1193,11 @@ mod tests {
|
|||||||
fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
|
fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
|
||||||
let release_channel = ReleaseChannel::Stable;
|
let release_channel = ReleaseChannel::Stable;
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let installed_version = semver::Version::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
|
version: VersionCheckType::Semantic(semver::Version::new(1, 0, 1)),
|
||||||
};
|
};
|
||||||
let fetched_version = SemanticVersion::new(1, 0, 2);
|
let fetched_version = semver::Version::new(1, 0, 2);
|
||||||
|
|
||||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||||
release_channel,
|
release_channel,
|
||||||
@@ -1208,9 +1217,10 @@ mod tests {
|
|||||||
fn test_nightly_does_not_update_when_fetched_sha_is_same() {
|
fn test_nightly_does_not_update_when_fetched_sha_is_same() {
|
||||||
let release_channel = ReleaseChannel::Nightly;
|
let release_channel = ReleaseChannel::Nightly;
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let mut installed_version = semver::Version::new(1, 0, 0);
|
||||||
|
installed_version.build = semver::BuildMetadata::new("a").unwrap();
|
||||||
let status = AutoUpdateStatus::Idle;
|
let status = AutoUpdateStatus::Idle;
|
||||||
let fetched_sha = "a".to_string();
|
let fetched_sha = "1.0.0+a".to_string();
|
||||||
|
|
||||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||||
release_channel,
|
release_channel,
|
||||||
@@ -1227,7 +1237,7 @@ mod tests {
|
|||||||
fn test_nightly_does_update_when_fetched_sha_is_not_same() {
|
fn test_nightly_does_update_when_fetched_sha_is_not_same() {
|
||||||
let release_channel = ReleaseChannel::Nightly;
|
let release_channel = ReleaseChannel::Nightly;
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let installed_version = semver::Version::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Idle;
|
let status = AutoUpdateStatus::Idle;
|
||||||
let fetched_sha = "b".to_string();
|
let fetched_sha = "b".to_string();
|
||||||
|
|
||||||
@@ -1246,14 +1256,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_nightly_does_not_update_when_fetched_sha_is_same_as_cached() {
|
fn test_nightly_does_not_update_when_fetched_version_is_same_as_cached() {
|
||||||
let release_channel = ReleaseChannel::Nightly;
|
let release_channel = ReleaseChannel::Nightly;
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let mut installed_version = semver::Version::new(1, 0, 0);
|
||||||
|
installed_version.build = semver::BuildMetadata::new("a").unwrap();
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||||
};
|
};
|
||||||
let fetched_sha = "b".to_string();
|
let fetched_sha = "1.0.0+b".to_string();
|
||||||
|
|
||||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||||
release_channel,
|
release_channel,
|
||||||
@@ -1270,11 +1281,12 @@ mod tests {
|
|||||||
fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
|
fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
|
||||||
let release_channel = ReleaseChannel::Nightly;
|
let release_channel = ReleaseChannel::Nightly;
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let mut installed_version = semver::Version::new(1, 0, 0);
|
||||||
|
installed_version.build = semver::BuildMetadata::new("a").unwrap();
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||||
};
|
};
|
||||||
let fetched_sha = "c".to_string();
|
let fetched_sha = "1.0.0+c".to_string();
|
||||||
|
|
||||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||||
release_channel,
|
release_channel,
|
||||||
@@ -1294,7 +1306,7 @@ mod tests {
|
|||||||
fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
|
fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
|
||||||
let release_channel = ReleaseChannel::Nightly;
|
let release_channel = ReleaseChannel::Nightly;
|
||||||
let app_commit_sha = Ok(None);
|
let app_commit_sha = Ok(None);
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let installed_version = semver::Version::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Idle;
|
let status = AutoUpdateStatus::Idle;
|
||||||
let fetched_sha = "a".to_string();
|
let fetched_sha = "a".to_string();
|
||||||
|
|
||||||
@@ -1317,11 +1329,11 @@ mod tests {
|
|||||||
{
|
{
|
||||||
let release_channel = ReleaseChannel::Nightly;
|
let release_channel = ReleaseChannel::Nightly;
|
||||||
let app_commit_sha = Ok(None);
|
let app_commit_sha = Ok(None);
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let installed_version = semver::Version::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||||
};
|
};
|
||||||
let fetched_sha = "b".to_string();
|
let fetched_sha = "1.0.0+b".to_string();
|
||||||
|
|
||||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||||
release_channel,
|
release_channel,
|
||||||
@@ -1339,7 +1351,7 @@ mod tests {
|
|||||||
{
|
{
|
||||||
let release_channel = ReleaseChannel::Nightly;
|
let release_channel = ReleaseChannel::Nightly;
|
||||||
let app_commit_sha = Ok(None);
|
let app_commit_sha = Ok(None);
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let installed_version = semver::Version::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ pub enum Model {
|
|||||||
alias = "claude-opus-4-1-thinking-latest"
|
alias = "claude-opus-4-1-thinking-latest"
|
||||||
)]
|
)]
|
||||||
ClaudeOpus4_1Thinking,
|
ClaudeOpus4_1Thinking,
|
||||||
|
#[serde(rename = "claude-opus-4-5", alias = "claude-opus-4-5-latest")]
|
||||||
|
ClaudeOpus4_5,
|
||||||
|
#[serde(
|
||||||
|
rename = "claude-opus-4-5-thinking",
|
||||||
|
alias = "claude-opus-4-5-thinking-latest"
|
||||||
|
)]
|
||||||
|
ClaudeOpus4_5Thinking,
|
||||||
#[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")]
|
#[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")]
|
||||||
Claude3_5SonnetV2,
|
Claude3_5SonnetV2,
|
||||||
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
|
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
|
||||||
@@ -141,7 +148,19 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_id(id: &str) -> anyhow::Result<Self> {
|
pub fn from_id(id: &str) -> anyhow::Result<Self> {
|
||||||
if id.starts_with("claude-3-5-sonnet-v2") {
|
if id.starts_with("claude-opus-4-5-thinking") {
|
||||||
|
Ok(Self::ClaudeOpus4_5Thinking)
|
||||||
|
} else if id.starts_with("claude-opus-4-5") {
|
||||||
|
Ok(Self::ClaudeOpus4_5)
|
||||||
|
} else if id.starts_with("claude-opus-4-1-thinking") {
|
||||||
|
Ok(Self::ClaudeOpus4_1Thinking)
|
||||||
|
} else if id.starts_with("claude-opus-4-1") {
|
||||||
|
Ok(Self::ClaudeOpus4_1)
|
||||||
|
} else if id.starts_with("claude-opus-4-thinking") {
|
||||||
|
Ok(Self::ClaudeOpus4Thinking)
|
||||||
|
} else if id.starts_with("claude-opus-4") {
|
||||||
|
Ok(Self::ClaudeOpus4)
|
||||||
|
} else if id.starts_with("claude-3-5-sonnet-v2") {
|
||||||
Ok(Self::Claude3_5SonnetV2)
|
Ok(Self::Claude3_5SonnetV2)
|
||||||
} else if id.starts_with("claude-3-opus") {
|
} else if id.starts_with("claude-3-opus") {
|
||||||
Ok(Self::Claude3Opus)
|
Ok(Self::Claude3Opus)
|
||||||
@@ -178,6 +197,8 @@ impl Model {
|
|||||||
Model::ClaudeOpus4_1 => "claude-opus-4-1",
|
Model::ClaudeOpus4_1 => "claude-opus-4-1",
|
||||||
Model::ClaudeOpus4Thinking => "claude-opus-4-thinking",
|
Model::ClaudeOpus4Thinking => "claude-opus-4-thinking",
|
||||||
Model::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking",
|
Model::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking",
|
||||||
|
Model::ClaudeOpus4_5 => "claude-opus-4-5",
|
||||||
|
Model::ClaudeOpus4_5Thinking => "claude-opus-4-5-thinking",
|
||||||
Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2",
|
Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2",
|
||||||
Model::Claude3_5Sonnet => "claude-3-5-sonnet",
|
Model::Claude3_5Sonnet => "claude-3-5-sonnet",
|
||||||
Model::Claude3Opus => "claude-3-opus",
|
Model::Claude3Opus => "claude-3-opus",
|
||||||
@@ -245,6 +266,9 @@ impl Model {
|
|||||||
Model::ClaudeOpus4_1 | Model::ClaudeOpus4_1Thinking => {
|
Model::ClaudeOpus4_1 | Model::ClaudeOpus4_1Thinking => {
|
||||||
"anthropic.claude-opus-4-1-20250805-v1:0"
|
"anthropic.claude-opus-4-1-20250805-v1:0"
|
||||||
}
|
}
|
||||||
|
Model::ClaudeOpus4_5 | Model::ClaudeOpus4_5Thinking => {
|
||||||
|
"anthropic.claude-opus-4-5-20251101-v1:0"
|
||||||
|
}
|
||||||
Model::Claude3_5SonnetV2 => "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
Model::Claude3_5SonnetV2 => "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||||
Model::Claude3_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
Model::Claude3_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||||
Model::Claude3Opus => "anthropic.claude-3-opus-20240229-v1:0",
|
Model::Claude3Opus => "anthropic.claude-3-opus-20240229-v1:0",
|
||||||
@@ -309,6 +333,8 @@ impl Model {
|
|||||||
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
|
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
|
||||||
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
|
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
|
||||||
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
|
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
|
||||||
|
Self::ClaudeOpus4_5 => "Claude Opus 4.5",
|
||||||
|
Self::ClaudeOpus4_5Thinking => "Claude Opus 4.5 Thinking",
|
||||||
Self::Claude3_5SonnetV2 => "Claude 3.5 Sonnet v2",
|
Self::Claude3_5SonnetV2 => "Claude 3.5 Sonnet v2",
|
||||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||||
Self::Claude3Opus => "Claude 3 Opus",
|
Self::Claude3Opus => "Claude 3 Opus",
|
||||||
@@ -379,7 +405,9 @@ impl Model {
|
|||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
| Self::ClaudeSonnet4_5Thinking
|
| Self::ClaudeSonnet4_5Thinking
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1Thinking => 200_000,
|
| Self::ClaudeOpus4_1Thinking
|
||||||
|
| Self::ClaudeOpus4_5
|
||||||
|
| Self::ClaudeOpus4_5Thinking => 200_000,
|
||||||
Self::AmazonNovaPremier => 1_000_000,
|
Self::AmazonNovaPremier => 1_000_000,
|
||||||
Self::PalmyraWriterX5 => 1_000_000,
|
Self::PalmyraWriterX5 => 1_000_000,
|
||||||
Self::PalmyraWriterX4 => 128_000,
|
Self::PalmyraWriterX4 => 128_000,
|
||||||
@@ -393,7 +421,11 @@ impl Model {
|
|||||||
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
|
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
|
||||||
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
|
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
|
||||||
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => 64_000,
|
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => 64_000,
|
||||||
Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking | Self::ClaudeHaiku4_5 => 64_000,
|
Self::ClaudeSonnet4_5
|
||||||
|
| Self::ClaudeSonnet4_5Thinking
|
||||||
|
| Self::ClaudeHaiku4_5
|
||||||
|
| Self::ClaudeOpus4_5
|
||||||
|
| Self::ClaudeOpus4_5Thinking => 64_000,
|
||||||
Self::ClaudeOpus4
|
Self::ClaudeOpus4
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
@@ -418,6 +450,8 @@ impl Model {
|
|||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking
|
||||||
|
| Self::ClaudeOpus4_5
|
||||||
|
| Self::ClaudeOpus4_5Thinking
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
@@ -443,6 +477,8 @@ impl Model {
|
|||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking
|
||||||
|
| Self::ClaudeOpus4_5
|
||||||
|
| Self::ClaudeOpus4_5Thinking
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
@@ -484,7 +520,9 @@ impl Model {
|
|||||||
| Self::ClaudeOpus4
|
| Self::ClaudeOpus4
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4_1Thinking => true,
|
| Self::ClaudeOpus4_1Thinking
|
||||||
|
| Self::ClaudeOpus4_5
|
||||||
|
| Self::ClaudeOpus4_5Thinking => true,
|
||||||
|
|
||||||
// Custom models - check if they have cache configuration
|
// Custom models - check if they have cache configuration
|
||||||
Self::Custom {
|
Self::Custom {
|
||||||
@@ -506,7 +544,9 @@ impl Model {
|
|||||||
| Self::ClaudeOpus4
|
| Self::ClaudeOpus4
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4_1Thinking => Some(BedrockModelCacheConfiguration {
|
| Self::ClaudeOpus4_1Thinking
|
||||||
|
| Self::ClaudeOpus4_5
|
||||||
|
| Self::ClaudeOpus4_5Thinking => Some(BedrockModelCacheConfiguration {
|
||||||
max_cache_anchors: 4,
|
max_cache_anchors: 4,
|
||||||
min_total_token: 1024,
|
min_total_token: 1024,
|
||||||
}),
|
}),
|
||||||
@@ -535,11 +575,11 @@ impl Model {
|
|||||||
budget_tokens: Some(4096),
|
budget_tokens: Some(4096),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Model::ClaudeOpus4Thinking | Model::ClaudeOpus4_1Thinking => {
|
Model::ClaudeOpus4Thinking
|
||||||
BedrockModelMode::Thinking {
|
| Model::ClaudeOpus4_1Thinking
|
||||||
budget_tokens: Some(4096),
|
| Model::ClaudeOpus4_5Thinking => BedrockModelMode::Thinking {
|
||||||
}
|
budget_tokens: Some(4096),
|
||||||
}
|
},
|
||||||
_ => BedrockModelMode::Default,
|
_ => BedrockModelMode::Default,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -593,6 +633,8 @@ impl Model {
|
|||||||
| Model::ClaudeOpus4Thinking
|
| Model::ClaudeOpus4Thinking
|
||||||
| Model::ClaudeOpus4_1
|
| Model::ClaudeOpus4_1
|
||||||
| Model::ClaudeOpus4_1Thinking
|
| Model::ClaudeOpus4_1Thinking
|
||||||
|
| Model::ClaudeOpus4_5
|
||||||
|
| Model::ClaudeOpus4_5Thinking
|
||||||
| Model::Claude3Haiku
|
| Model::Claude3Haiku
|
||||||
| Model::Claude3Opus
|
| Model::Claude3Opus
|
||||||
| Model::Claude3Sonnet
|
| Model::Claude3Sonnet
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ impl std::fmt::Debug for BufferDiffInner {
|
|||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("BufferDiffSnapshot")
|
f.debug_struct("BufferDiffSnapshot")
|
||||||
.field("hunks", &self.hunks)
|
.field("hunks", &self.hunks)
|
||||||
|
.field("remote_id", &self.base_text.remote_id())
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ collections = { workspace = true, features = ["test-support"] }
|
|||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
rpc = { workspace = true, features = ["test-support"] }
|
rpc = { workspace = true, features = ["test-support"] }
|
||||||
client = { workspace = true, features = ["test-support"] }
|
client = { workspace = true, features = ["test-support"] }
|
||||||
|
semver.workspace = true
|
||||||
settings = { workspace = true, features = ["test-support"] }
|
settings = { workspace = true, features = ["test-support"] }
|
||||||
util = { workspace = true, features = ["test-support"] }
|
util = { workspace = true, features = ["test-support"] }
|
||||||
http_client = { workspace = true, features = ["test-support"] }
|
http_client = { workspace = true, features = ["test-support"] }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use client::{Client, UserStore};
|
use client::{Client, UserStore};
|
||||||
use clock::FakeSystemClock;
|
use clock::FakeSystemClock;
|
||||||
use gpui::{App, AppContext as _, Entity, SemanticVersion};
|
use gpui::{App, AppContext as _, Entity};
|
||||||
use http_client::FakeHttpClient;
|
use http_client::FakeHttpClient;
|
||||||
use rpc::proto::{self};
|
use rpc::proto::{self};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
@@ -236,7 +236,7 @@ fn test_dangling_channel_paths(cx: &mut App) {
|
|||||||
fn init_test(cx: &mut App) -> Entity<ChannelStore> {
|
fn init_test(cx: &mut App) -> Entity<ChannelStore> {
|
||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
release_channel::init(SemanticVersion::default(), cx);
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||||
|
|
||||||
let clock = Arc::new(FakeSystemClock::new());
|
let clock = Arc::new(FakeSystemClock::new());
|
||||||
let http = FakeHttpClient::with_404_response();
|
let http = FakeHttpClient::with_404_response();
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ util.workspace = true
|
|||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
rayon.workspace = true
|
rayon.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
serde_json.workspace = true
|
||||||
|
util = { workspace = true, features = ["test-support"] }
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
||||||
exec.workspace = true
|
exec.workspace = true
|
||||||
fork.workspace = true
|
fork.workspace = true
|
||||||
|
|||||||
@@ -23,4 +23,7 @@ fn main() {
|
|||||||
|
|
||||||
println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}");
|
println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}");
|
||||||
}
|
}
|
||||||
|
if let Some(build_identifier) = option_env!("GITHUB_RUN_NUMBER") {
|
||||||
|
println!("cargo:rustc-env=ZED_BUILD_ID={build_identifier}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ use clap::Parser;
|
|||||||
use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};
|
use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{
|
use std::{
|
||||||
env, fs, io,
|
env,
|
||||||
|
ffi::OsStr,
|
||||||
|
fs, io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::ExitStatus,
|
process::ExitStatus,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
@@ -129,37 +131,177 @@ struct Args {
|
|||||||
askpass: Option<String>,
|
askpass: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses a path containing a position (e.g. `path:line:column`)
|
||||||
|
/// and returns its canonicalized string representation.
|
||||||
|
///
|
||||||
|
/// If a part of path doesn't exist, it will canonicalize the
|
||||||
|
/// existing part and append the non-existing part.
|
||||||
|
///
|
||||||
|
/// This method must return an absolute path, as many zed
|
||||||
|
/// crates assume absolute paths.
|
||||||
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
|
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
|
||||||
let canonicalized = match Path::new(argument_str).canonicalize() {
|
match Path::new(argument_str).canonicalize() {
|
||||||
Ok(existing_path) => PathWithPosition::from_path(existing_path),
|
Ok(existing_path) => Ok(PathWithPosition::from_path(existing_path)),
|
||||||
Err(_) => {
|
Err(_) => PathWithPosition::parse_str(argument_str).map_path(|mut path| {
|
||||||
let path = PathWithPosition::parse_str(argument_str);
|
|
||||||
let curdir = env::current_dir().context("retrieving current directory")?;
|
let curdir = env::current_dir().context("retrieving current directory")?;
|
||||||
path.map_path(|path| match fs::canonicalize(&path) {
|
let mut children = Vec::new();
|
||||||
Ok(path) => Ok(path),
|
let root;
|
||||||
Err(e) => {
|
loop {
|
||||||
if let Some(mut parent) = path.parent() {
|
// canonicalize handles './', and '/'.
|
||||||
if parent == Path::new("") {
|
if let Ok(canonicalized) = fs::canonicalize(&path) {
|
||||||
parent = &curdir
|
root = canonicalized;
|
||||||
}
|
break;
|
||||||
match fs::canonicalize(parent) {
|
|
||||||
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
|
|
||||||
Err(_) => Err(e),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
// The comparison to `curdir` is just a shortcut
|
||||||
}
|
// since we know it is canonical. The other one
|
||||||
.with_context(|| format!("parsing as path with position {argument_str}"))?,
|
// is if `argument_str` is a string that starts
|
||||||
};
|
// with a name (e.g. "foo/bar").
|
||||||
Ok(canonicalized.to_string(|path| path.to_string_lossy().into_owned()))
|
if path == curdir || path == Path::new("") {
|
||||||
|
root = curdir;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
children.push(
|
||||||
|
path.file_name()
|
||||||
|
.with_context(|| format!("parsing as path with position {argument_str}"))?
|
||||||
|
.to_owned(),
|
||||||
|
);
|
||||||
|
if !path.pop() {
|
||||||
|
unreachable!("parsing as path with position {argument_str}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(children.iter().rev().fold(root, |mut path, child| {
|
||||||
|
path.push(child);
|
||||||
|
path
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
.map(|path_with_pos| path_with_pos.to_string(|path| path.to_string_lossy().into_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
use util::path;
|
||||||
|
use util::paths::SanitizedPath;
|
||||||
|
use util::test::TempTree;
|
||||||
|
|
||||||
|
macro_rules! assert_path_eq {
|
||||||
|
($left:expr, $right:expr) => {
|
||||||
|
assert_eq!(
|
||||||
|
SanitizedPath::new(Path::new(&$left)),
|
||||||
|
SanitizedPath::new(Path::new(&$right))
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cwd() -> PathBuf {
|
||||||
|
env::current_dir().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
static CWD_LOCK: Mutex<()> = Mutex::new(());
|
||||||
|
|
||||||
|
fn with_cwd<T>(path: &Path, f: impl FnOnce() -> anyhow::Result<T>) -> anyhow::Result<T> {
|
||||||
|
let _lock = CWD_LOCK.lock();
|
||||||
|
let old_cwd = cwd();
|
||||||
|
env::set_current_dir(path)?;
|
||||||
|
let result = f();
|
||||||
|
env::set_current_dir(old_cwd)?;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_non_existing_path() {
|
||||||
|
// Absolute path
|
||||||
|
let result = parse_path_with_position(path!("/non/existing/path.txt")).unwrap();
|
||||||
|
assert_path_eq!(result, path!("/non/existing/path.txt"));
|
||||||
|
|
||||||
|
// Absolute path in cwd
|
||||||
|
let path = cwd().join(path!("non/existing/path.txt"));
|
||||||
|
let expected = path.to_string_lossy().to_string();
|
||||||
|
let result = parse_path_with_position(&expected).unwrap();
|
||||||
|
assert_path_eq!(result, expected);
|
||||||
|
|
||||||
|
// Relative path
|
||||||
|
let result = parse_path_with_position(path!("non/existing/path.txt")).unwrap();
|
||||||
|
assert_path_eq!(result, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_existing_path() {
|
||||||
|
let temp_tree = TempTree::new(json!({
|
||||||
|
"file.txt": "",
|
||||||
|
}));
|
||||||
|
let file_path = temp_tree.path().join("file.txt");
|
||||||
|
let expected = file_path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
// Absolute path
|
||||||
|
let result = parse_path_with_position(file_path.to_str().unwrap()).unwrap();
|
||||||
|
assert_path_eq!(result, expected);
|
||||||
|
|
||||||
|
// Relative path
|
||||||
|
let result = with_cwd(temp_tree.path(), || parse_path_with_position("file.txt")).unwrap();
|
||||||
|
assert_path_eq!(result, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE:
|
||||||
|
// While POSIX symbolic links are somewhat supported on Windows, they are an opt in by the user, and thus
|
||||||
|
// we assume that they are not supported out of the box.
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
#[test]
|
||||||
|
fn test_parse_symlink_file() {
|
||||||
|
let temp_tree = TempTree::new(json!({
|
||||||
|
"target.txt": "",
|
||||||
|
}));
|
||||||
|
let target_path = temp_tree.path().join("target.txt");
|
||||||
|
let symlink_path = temp_tree.path().join("symlink.txt");
|
||||||
|
std::os::unix::fs::symlink(&target_path, &symlink_path).unwrap();
|
||||||
|
|
||||||
|
// Absolute path
|
||||||
|
let result = parse_path_with_position(symlink_path.to_str().unwrap()).unwrap();
|
||||||
|
assert_eq!(result, target_path.to_string_lossy());
|
||||||
|
|
||||||
|
// Relative path
|
||||||
|
let result =
|
||||||
|
with_cwd(temp_tree.path(), || parse_path_with_position("symlink.txt")).unwrap();
|
||||||
|
assert_eq!(result, target_path.to_string_lossy());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
#[test]
|
||||||
|
fn test_parse_symlink_dir() {
|
||||||
|
let temp_tree = TempTree::new(json!({
|
||||||
|
"some": {
|
||||||
|
"dir": { // symlink target
|
||||||
|
"ec": {
|
||||||
|
"tory": {
|
||||||
|
"file.txt": "",
|
||||||
|
}}}}}));
|
||||||
|
|
||||||
|
let target_file_path = temp_tree.path().join("some/dir/ec/tory/file.txt");
|
||||||
|
let expected = target_file_path.to_string_lossy();
|
||||||
|
|
||||||
|
let dir_path = temp_tree.path().join("some/dir");
|
||||||
|
let symlink_path = temp_tree.path().join("symlink");
|
||||||
|
std::os::unix::fs::symlink(&dir_path, &symlink_path).unwrap();
|
||||||
|
|
||||||
|
// Absolute path
|
||||||
|
let result =
|
||||||
|
parse_path_with_position(symlink_path.join("ec/tory/file.txt").to_str().unwrap())
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
|
||||||
|
// Relative path
|
||||||
|
let result = with_cwd(temp_tree.path(), || {
|
||||||
|
parse_path_with_position("symlink/ec/tory/file.txt")
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
|
fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
|
||||||
let mut source = PathWithPosition::parse_str(source);
|
let mut source = PathWithPosition::parse_str(source);
|
||||||
let mut command = util::command::new_std_command("wsl.exe");
|
|
||||||
|
|
||||||
let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
|
let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
|
||||||
if user.is_empty() {
|
if user.is_empty() {
|
||||||
@@ -170,22 +312,35 @@ fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
|
|||||||
(None, wsl)
|
(None, wsl)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut args = vec!["--distribution", distro_name];
|
||||||
if let Some(user) = user {
|
if let Some(user) = user {
|
||||||
command.arg("--user").arg(user);
|
args.push("--user");
|
||||||
|
args.push(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = command
|
let command = [
|
||||||
.arg("--distribution")
|
OsStr::new("realpath"),
|
||||||
.arg(distro_name)
|
OsStr::new("-s"),
|
||||||
.arg("--exec")
|
source.path.as_ref(),
|
||||||
.arg("wslpath")
|
];
|
||||||
.arg("-m")
|
|
||||||
.arg(&source.path)
|
|
||||||
.output()?;
|
|
||||||
|
|
||||||
let result = String::from_utf8_lossy(&output.stdout);
|
let output = util::command::new_std_command("wsl.exe")
|
||||||
let prefix = format!("//wsl.localhost/{}", distro_name);
|
.args(&args)
|
||||||
source.path = Path::new(result.trim().strip_prefix(&prefix).unwrap_or(&result)).to_owned();
|
.arg("--exec")
|
||||||
|
.args(&command)
|
||||||
|
.output()?;
|
||||||
|
let result = if output.status.success() {
|
||||||
|
String::from_utf8_lossy(&output.stdout).to_string()
|
||||||
|
} else {
|
||||||
|
let fallback = util::command::new_std_command("wsl.exe")
|
||||||
|
.args(&args)
|
||||||
|
.arg("--")
|
||||||
|
.args(&command)
|
||||||
|
.output()?;
|
||||||
|
String::from_utf8_lossy(&fallback.stdout).to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
source.path = Path::new(result.trim()).to_owned();
|
||||||
|
|
||||||
Ok(source.to_string(|path| path.to_string_lossy().into_owned()))
|
Ok(source.to_string(|path| path.to_string_lossy().into_owned()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ settings = { workspace = true, features = ["test-support"] }
|
|||||||
util = { workspace = true, features = ["test-support"] }
|
util = { workspace = true, features = ["test-support"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
semver.workspace = true
|
||||||
windows.workspace = true
|
windows.workspace = true
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ pub fn os_version() -> String {
|
|||||||
let mut info = unsafe { std::mem::zeroed() };
|
let mut info = unsafe { std::mem::zeroed() };
|
||||||
let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut info) };
|
let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut info) };
|
||||||
if status.is_ok() {
|
if status.is_ok() {
|
||||||
gpui::SemanticVersion::new(
|
semver::Version::new(
|
||||||
info.dwMajorVersion as _,
|
info.dwMajorVersion as _,
|
||||||
info.dwMinorVersion as _,
|
info.dwMinorVersion as _,
|
||||||
info.dwBuildNumber as _,
|
info.dwBuildNumber as _,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::{
|
use std::{
|
||||||
fmt::{Display, Write as _},
|
fmt::{Display, Write as _},
|
||||||
ops::{Add, Range, Sub},
|
ops::{Add, Range, Sub},
|
||||||
path::{Path, PathBuf},
|
path::Path,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use strum::EnumIter;
|
use strum::EnumIter;
|
||||||
@@ -17,7 +17,7 @@ pub struct PlanContextRetrievalRequest {
|
|||||||
pub excerpt_path: Arc<Path>,
|
pub excerpt_path: Arc<Path>,
|
||||||
pub excerpt_line_range: Range<Line>,
|
pub excerpt_line_range: Range<Line>,
|
||||||
pub cursor_file_max_row: Line,
|
pub cursor_file_max_row: Line,
|
||||||
pub events: Vec<Event>,
|
pub events: Vec<Arc<Event>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -36,7 +36,7 @@ pub struct PredictEditsRequest {
|
|||||||
pub signatures: Vec<Signature>,
|
pub signatures: Vec<Signature>,
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||||
pub referenced_declarations: Vec<ReferencedDeclaration>,
|
pub referenced_declarations: Vec<ReferencedDeclaration>,
|
||||||
pub events: Vec<Event>,
|
pub events: Vec<Arc<Event>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub can_collect_data: bool,
|
pub can_collect_data: bool,
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||||
@@ -80,6 +80,8 @@ pub enum PromptFormat {
|
|||||||
Minimal,
|
Minimal,
|
||||||
/// One-sentence instructions + FIM-like template
|
/// One-sentence instructions + FIM-like template
|
||||||
MinimalQwen,
|
MinimalQwen,
|
||||||
|
/// No instructions, Qwen chat + Seed-Coder 1120 FIM-like template
|
||||||
|
SeedCoder1120,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PromptFormat {
|
impl PromptFormat {
|
||||||
@@ -108,6 +110,7 @@ impl std::fmt::Display for PromptFormat {
|
|||||||
PromptFormat::OldTextNewText => write!(f, "Old Text / New Text"),
|
PromptFormat::OldTextNewText => write!(f, "Old Text / New Text"),
|
||||||
PromptFormat::Minimal => write!(f, "Minimal"),
|
PromptFormat::Minimal => write!(f, "Minimal"),
|
||||||
PromptFormat::MinimalQwen => write!(f, "Minimal + Qwen FIM"),
|
PromptFormat::MinimalQwen => write!(f, "Minimal + Qwen FIM"),
|
||||||
|
PromptFormat::SeedCoder1120 => write!(f, "Seed-Coder 1120"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,10 +120,11 @@ impl std::fmt::Display for PromptFormat {
|
|||||||
#[serde(tag = "event")]
|
#[serde(tag = "event")]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
BufferChange {
|
BufferChange {
|
||||||
path: Option<PathBuf>,
|
path: Arc<Path>,
|
||||||
old_path: Option<PathBuf>,
|
old_path: Arc<Path>,
|
||||||
diff: String,
|
diff: String,
|
||||||
predicted: bool,
|
predicted: bool,
|
||||||
|
in_open_source_repo: bool,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,23 +136,21 @@ impl Display for Event {
|
|||||||
old_path,
|
old_path,
|
||||||
diff,
|
diff,
|
||||||
predicted,
|
predicted,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
let new_path = path.as_deref().unwrap_or(Path::new("untitled"));
|
|
||||||
let old_path = old_path.as_deref().unwrap_or(new_path);
|
|
||||||
|
|
||||||
if *predicted {
|
if *predicted {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"// User accepted prediction:\n--- a/{}\n+++ b/{}\n{diff}",
|
"// User accepted prediction:\n--- a/{}\n+++ b/{}\n{diff}",
|
||||||
DiffPathFmt(old_path),
|
DiffPathFmt(old_path),
|
||||||
DiffPathFmt(new_path)
|
DiffPathFmt(path)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"--- a/{}\n+++ b/{}\n{diff}",
|
"--- a/{}\n+++ b/{}\n{diff}",
|
||||||
DiffPathFmt(old_path),
|
DiffPathFmt(old_path),
|
||||||
DiffPathFmt(new_path)
|
DiffPathFmt(path)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,10 +299,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_event_display() {
|
fn test_event_display() {
|
||||||
let ev = Event::BufferChange {
|
let ev = Event::BufferChange {
|
||||||
path: None,
|
path: Path::new("untitled").into(),
|
||||||
old_path: None,
|
old_path: Path::new("untitled").into(),
|
||||||
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
||||||
predicted: false,
|
predicted: false,
|
||||||
|
in_open_source_repo: true,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ev.to_string(),
|
ev.to_string(),
|
||||||
@@ -314,10 +317,11 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let ev = Event::BufferChange {
|
let ev = Event::BufferChange {
|
||||||
path: Some(PathBuf::from("foo/bar.txt")),
|
path: Path::new("foo/bar.txt").into(),
|
||||||
old_path: Some(PathBuf::from("foo/bar.txt")),
|
old_path: Path::new("foo/bar.txt").into(),
|
||||||
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
||||||
predicted: false,
|
predicted: false,
|
||||||
|
in_open_source_repo: true,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ev.to_string(),
|
ev.to_string(),
|
||||||
@@ -331,10 +335,11 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let ev = Event::BufferChange {
|
let ev = Event::BufferChange {
|
||||||
path: Some(PathBuf::from("abc.txt")),
|
path: Path::new("abc.txt").into(),
|
||||||
old_path: Some(PathBuf::from("123.txt")),
|
old_path: Path::new("123.txt").into(),
|
||||||
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
||||||
predicted: false,
|
predicted: false,
|
||||||
|
in_open_source_repo: true,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ev.to_string(),
|
ev.to_string(),
|
||||||
@@ -348,10 +353,11 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let ev = Event::BufferChange {
|
let ev = Event::BufferChange {
|
||||||
path: Some(PathBuf::from("abc.txt")),
|
path: Path::new("abc.txt").into(),
|
||||||
old_path: Some(PathBuf::from("123.txt")),
|
old_path: Path::new("123.txt").into(),
|
||||||
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
||||||
predicted: true,
|
predicted: true,
|
||||||
|
in_open_source_repo: true,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ev.to_string(),
|
ev.to_string(),
|
||||||
|
|||||||
@@ -169,15 +169,18 @@ pub fn build_prompt(
|
|||||||
) -> Result<(String, SectionLabels)> {
|
) -> Result<(String, SectionLabels)> {
|
||||||
let mut section_labels = Default::default();
|
let mut section_labels = Default::default();
|
||||||
|
|
||||||
|
let prompt_data = PromptData {
|
||||||
|
events: request.events.clone(),
|
||||||
|
cursor_point: request.cursor_point,
|
||||||
|
cursor_path: request.excerpt_path.clone(),
|
||||||
|
included_files: request.included_files.clone(),
|
||||||
|
};
|
||||||
match request.prompt_format {
|
match request.prompt_format {
|
||||||
PromptFormat::MinimalQwen => {
|
PromptFormat::MinimalQwen => {
|
||||||
let prompt = MinimalQwenPrompt {
|
return Ok((MinimalQwenPrompt.render(&prompt_data), section_labels));
|
||||||
events: request.events.clone(),
|
}
|
||||||
cursor_point: request.cursor_point,
|
PromptFormat::SeedCoder1120 => {
|
||||||
cursor_path: request.excerpt_path.clone(),
|
return Ok((SeedCoder1120Prompt.render(&prompt_data), section_labels));
|
||||||
included_files: request.included_files.clone(),
|
|
||||||
};
|
|
||||||
return Ok((prompt.render(), section_labels));
|
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
};
|
};
|
||||||
@@ -208,6 +211,7 @@ pub fn build_prompt(
|
|||||||
}
|
}
|
||||||
PromptFormat::OnlySnippets => vec![],
|
PromptFormat::OnlySnippets => vec![],
|
||||||
PromptFormat::MinimalQwen => unreachable!(),
|
PromptFormat::MinimalQwen => unreachable!(),
|
||||||
|
PromptFormat::SeedCoder1120 => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut prompt = match request.prompt_format {
|
let mut prompt = match request.prompt_format {
|
||||||
@@ -218,6 +222,7 @@ pub fn build_prompt(
|
|||||||
PromptFormat::OnlySnippets => String::new(),
|
PromptFormat::OnlySnippets => String::new(),
|
||||||
PromptFormat::Minimal => STUDENT_MODEL_INSTRUCTIONS.to_string(),
|
PromptFormat::Minimal => STUDENT_MODEL_INSTRUCTIONS.to_string(),
|
||||||
PromptFormat::MinimalQwen => unreachable!(),
|
PromptFormat::MinimalQwen => unreachable!(),
|
||||||
|
PromptFormat::SeedCoder1120 => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if request.events.is_empty() {
|
if request.events.is_empty() {
|
||||||
@@ -328,6 +333,13 @@ pub fn build_prompt(
|
|||||||
Ok((prompt, section_labels))
|
Ok((prompt, section_labels))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generation_params(prompt_format: PromptFormat) -> GenerationParams {
|
||||||
|
match prompt_format {
|
||||||
|
PromptFormat::SeedCoder1120 => SeedCoder1120Prompt::generation_params(),
|
||||||
|
_ => GenerationParams::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn write_codeblock<'a>(
|
pub fn write_codeblock<'a>(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
excerpts: impl IntoIterator<Item = &'a Excerpt>,
|
excerpts: impl IntoIterator<Item = &'a Excerpt>,
|
||||||
@@ -420,7 +432,7 @@ pub fn write_excerpts<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_events(output: &mut String, events: &[predict_edits_v3::Event]) {
|
pub fn push_events(output: &mut String, events: &[Arc<predict_edits_v3::Event>]) {
|
||||||
if events.is_empty() {
|
if events.is_empty() {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -786,6 +798,7 @@ impl<'a> SyntaxBasedPrompt<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
PromptFormat::MinimalQwen => unreachable!(),
|
PromptFormat::MinimalQwen => unreachable!(),
|
||||||
|
PromptFormat::SeedCoder1120 => unreachable!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
let push_full_snippet = |output: &mut String| {
|
let push_full_snippet = |output: &mut String| {
|
||||||
@@ -896,19 +909,34 @@ fn declaration_size(declaration: &ReferencedDeclaration, style: DeclarationStyle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MinimalQwenPrompt {
|
struct PromptData {
|
||||||
events: Vec<Event>,
|
events: Vec<Arc<Event>>,
|
||||||
cursor_point: Point,
|
cursor_point: Point,
|
||||||
cursor_path: Arc<Path>, // TODO: make a common struct with cursor_point
|
cursor_path: Arc<Path>, // TODO: make a common struct with cursor_point
|
||||||
included_files: Vec<IncludedFile>,
|
included_files: Vec<IncludedFile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MinimalQwenPrompt {
|
#[derive(Default)]
|
||||||
const INSTRUCTIONS: &str = "You are a code completion assistant that analyzes edit history to identify and systematically complete incomplete refactorings or patterns across the entire codebase.\n";
|
pub struct GenerationParams {
|
||||||
|
pub temperature: Option<f32>,
|
||||||
|
pub top_p: Option<f32>,
|
||||||
|
pub stop: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
fn render(&self) -> String {
|
trait PromptFormatter {
|
||||||
let edit_history = self.fmt_edit_history();
|
fn render(&self, data: &PromptData) -> String;
|
||||||
let context = self.fmt_context();
|
|
||||||
|
fn generation_params() -> GenerationParams {
|
||||||
|
return GenerationParams::default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MinimalQwenPrompt;
|
||||||
|
|
||||||
|
impl PromptFormatter for MinimalQwenPrompt {
|
||||||
|
fn render(&self, data: &PromptData) -> String {
|
||||||
|
let edit_history = self.fmt_edit_history(data);
|
||||||
|
let context = self.fmt_context(data);
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"{instructions}\n\n{edit_history}\n\n{context}",
|
"{instructions}\n\n{edit_history}\n\n{context}",
|
||||||
@@ -917,13 +945,17 @@ impl MinimalQwenPrompt {
|
|||||||
context = context
|
context = context
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn fmt_edit_history(&self) -> String {
|
impl MinimalQwenPrompt {
|
||||||
if self.events.is_empty() {
|
const INSTRUCTIONS: &str = "You are a code completion assistant that analyzes edit history to identify and systematically complete incomplete refactorings or patterns across the entire codebase.\n";
|
||||||
|
|
||||||
|
fn fmt_edit_history(&self, data: &PromptData) -> String {
|
||||||
|
if data.events.is_empty() {
|
||||||
"(No edit history)\n\n".to_string()
|
"(No edit history)\n\n".to_string()
|
||||||
} else {
|
} else {
|
||||||
let mut events_str = String::new();
|
let mut events_str = String::new();
|
||||||
push_events(&mut events_str, &self.events);
|
push_events(&mut events_str, &data.events);
|
||||||
format!(
|
format!(
|
||||||
"The following are the latest edits made by the user, from earlier to later.\n\n{}",
|
"The following are the latest edits made by the user, from earlier to later.\n\n{}",
|
||||||
events_str
|
events_str
|
||||||
@@ -931,18 +963,18 @@ impl MinimalQwenPrompt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fmt_context(&self) -> String {
|
fn fmt_context(&self, data: &PromptData) -> String {
|
||||||
let mut context = String::new();
|
let mut context = String::new();
|
||||||
let include_line_numbers = true;
|
let include_line_numbers = true;
|
||||||
|
|
||||||
for related_file in &self.included_files {
|
for related_file in &data.included_files {
|
||||||
writeln!(context, "<|file_sep|>{}", DiffPathFmt(&related_file.path)).unwrap();
|
writeln!(context, "<|file_sep|>{}", DiffPathFmt(&related_file.path)).unwrap();
|
||||||
|
|
||||||
if related_file.path == self.cursor_path {
|
if related_file.path == data.cursor_path {
|
||||||
write!(context, "<|fim_prefix|>").unwrap();
|
write!(context, "<|fim_prefix|>").unwrap();
|
||||||
write_excerpts(
|
write_excerpts(
|
||||||
&related_file.excerpts,
|
&related_file.excerpts,
|
||||||
&[(self.cursor_point, "<|fim_suffix|>")],
|
&[(data.cursor_point, "<|fim_suffix|>")],
|
||||||
related_file.max_row,
|
related_file.max_row,
|
||||||
include_line_numbers,
|
include_line_numbers,
|
||||||
&mut context,
|
&mut context,
|
||||||
@@ -961,3 +993,83 @@ impl MinimalQwenPrompt {
|
|||||||
context
|
context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SeedCoder1120Prompt;
|
||||||
|
|
||||||
|
impl PromptFormatter for SeedCoder1120Prompt {
|
||||||
|
fn render(&self, data: &PromptData) -> String {
|
||||||
|
let edit_history = self.fmt_edit_history(data);
|
||||||
|
let context = self.fmt_context(data);
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"# Edit History:\n{edit_history}\n\n{context}",
|
||||||
|
edit_history = edit_history,
|
||||||
|
context = context
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generation_params() -> GenerationParams {
|
||||||
|
GenerationParams {
|
||||||
|
temperature: Some(0.2),
|
||||||
|
top_p: Some(0.9),
|
||||||
|
stop: Some(vec!["<[end_of_sentence]>".into()]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SeedCoder1120Prompt {
|
||||||
|
fn fmt_edit_history(&self, data: &PromptData) -> String {
|
||||||
|
if data.events.is_empty() {
|
||||||
|
"(No edit history)\n\n".to_string()
|
||||||
|
} else {
|
||||||
|
let mut events_str = String::new();
|
||||||
|
push_events(&mut events_str, &data.events);
|
||||||
|
events_str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_context(&self, data: &PromptData) -> String {
|
||||||
|
let mut context = String::new();
|
||||||
|
let include_line_numbers = true;
|
||||||
|
|
||||||
|
for related_file in &data.included_files {
|
||||||
|
writeln!(context, "# Path: {}\n", DiffPathFmt(&related_file.path)).unwrap();
|
||||||
|
|
||||||
|
if related_file.path == data.cursor_path {
|
||||||
|
let fim_prompt = self.fmt_fim(&related_file, data.cursor_point);
|
||||||
|
context.push_str(&fim_prompt);
|
||||||
|
} else {
|
||||||
|
write_excerpts(
|
||||||
|
&related_file.excerpts,
|
||||||
|
&[],
|
||||||
|
related_file.max_row,
|
||||||
|
include_line_numbers,
|
||||||
|
&mut context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_fim(&self, file: &IncludedFile, cursor_point: Point) -> String {
|
||||||
|
let mut buf = String::new();
|
||||||
|
const FIM_SUFFIX: &str = "<[fim-suffix]>";
|
||||||
|
const FIM_PREFIX: &str = "<[fim-prefix]>";
|
||||||
|
const FIM_MIDDLE: &str = "<[fim-middle]>";
|
||||||
|
write!(buf, "{}", FIM_PREFIX).unwrap();
|
||||||
|
write_excerpts(
|
||||||
|
&file.excerpts,
|
||||||
|
&[(cursor_point, FIM_SUFFIX)],
|
||||||
|
file.max_row,
|
||||||
|
true,
|
||||||
|
&mut buf,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Swap prefix and suffix parts
|
||||||
|
let index = buf.find(FIM_SUFFIX).unwrap();
|
||||||
|
let prefix = &buf[..index];
|
||||||
|
let suffix = &buf[index..];
|
||||||
|
|
||||||
|
format!("{}{}{}", suffix, prefix, FIM_MIDDLE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ impl EditPredictionProvider for CodestralCompletionProvider {
|
|||||||
Self::api_key(cx).is_some()
|
Self::api_key(cx).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_refreshing(&self) -> bool {
|
fn is_refreshing(&self, _cx: &App) -> bool {
|
||||||
self.pending_request.is_some()
|
self.pending_request.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ scrypt = "0.11"
|
|||||||
# sea-orm and sea-orm-macros versions must match exactly.
|
# sea-orm and sea-orm-macros versions must match exactly.
|
||||||
sea-orm = { version = "=1.1.10", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
|
sea-orm = { version = "=1.1.10", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
|
||||||
sea-orm-macros = "=1.1.10"
|
sea-orm-macros = "=1.1.10"
|
||||||
semantic_version.workspace = true
|
|
||||||
semver.workspace = true
|
semver.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use collections::{BTreeSet, HashMap};
|
use collections::{BTreeSet, HashMap};
|
||||||
use rpc::{ExtensionApiManifest, ExtensionProvides, GetExtensionsResponse};
|
use rpc::{ExtensionApiManifest, ExtensionProvides, GetExtensionsResponse};
|
||||||
use semantic_version::SemanticVersion;
|
use semver::Version as SemanticVersion;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
@@ -108,8 +108,8 @@ struct GetExtensionUpdatesParams {
|
|||||||
ids: String,
|
ids: String,
|
||||||
min_schema_version: i32,
|
min_schema_version: i32,
|
||||||
max_schema_version: i32,
|
max_schema_version: i32,
|
||||||
min_wasm_api_version: SemanticVersion,
|
min_wasm_api_version: semver::Version,
|
||||||
max_wasm_api_version: SemanticVersion,
|
max_wasm_api_version: semver::Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_extension_updates(
|
async fn get_extension_updates(
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use sea_orm::{
|
|||||||
entity::prelude::*,
|
entity::prelude::*,
|
||||||
sea_query::{Alias, Expr, OnConflict},
|
sea_query::{Alias, Expr, OnConflict},
|
||||||
};
|
};
|
||||||
use semantic_version::SemanticVersion;
|
use semver::Version;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
use std::{
|
use std::{
|
||||||
@@ -671,7 +671,7 @@ pub struct NewExtensionVersion {
|
|||||||
|
|
||||||
pub struct ExtensionVersionConstraints {
|
pub struct ExtensionVersionConstraints {
|
||||||
pub schema_versions: RangeInclusive<i32>,
|
pub schema_versions: RangeInclusive<i32>,
|
||||||
pub wasm_api_versions: RangeInclusive<SemanticVersion>,
|
pub wasm_api_versions: RangeInclusive<semver::Version>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalSettingsKind {
|
impl LocalSettingsKind {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ impl Database {
|
|||||||
extensions: &[extension::Model],
|
extensions: &[extension::Model],
|
||||||
constraints: Option<&ExtensionVersionConstraints>,
|
constraints: Option<&ExtensionVersionConstraints>,
|
||||||
tx: &DatabaseTransaction,
|
tx: &DatabaseTransaction,
|
||||||
) -> Result<HashMap<ExtensionId, (extension_version::Model, SemanticVersion)>> {
|
) -> Result<HashMap<ExtensionId, (extension_version::Model, Version)>> {
|
||||||
let mut versions = extension_version::Entity::find()
|
let mut versions = extension_version::Entity::find()
|
||||||
.filter(
|
.filter(
|
||||||
extension_version::Column::ExtensionId
|
extension_version::Column::ExtensionId
|
||||||
@@ -79,11 +79,10 @@ impl Database {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut max_versions =
|
let mut max_versions =
|
||||||
HashMap::<ExtensionId, (extension_version::Model, SemanticVersion)>::default();
|
HashMap::<ExtensionId, (extension_version::Model, Version)>::default();
|
||||||
while let Some(version) = versions.next().await {
|
while let Some(version) = versions.next().await {
|
||||||
let version = version?;
|
let version = version?;
|
||||||
let Some(extension_version) = SemanticVersion::from_str(&version.version).log_err()
|
let Some(extension_version) = Version::from_str(&version.version).log_err() else {
|
||||||
else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,7 +101,7 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(wasm_api_version) = version.wasm_api_version.as_ref() {
|
if let Some(wasm_api_version) = version.wasm_api_version.as_ref() {
|
||||||
if let Some(version) = SemanticVersion::from_str(wasm_api_version).log_err() {
|
if let Some(version) = Version::from_str(wasm_api_version).log_err() {
|
||||||
if !constraints.wasm_api_versions.contains(&version) {
|
if !constraints.wasm_api_versions.contains(&version) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ use rpc::{
|
|||||||
RequestMessage, ShareProject, UpdateChannelBufferCollaborators,
|
RequestMessage, ShareProject, UpdateChannelBufferCollaborators,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use semantic_version::SemanticVersion;
|
use semver::Version;
|
||||||
use serde::{Serialize, Serializer};
|
use serde::{Serialize, Serializer};
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
@@ -453,6 +453,7 @@ impl Server {
|
|||||||
.add_request_handler(forward_mutating_project_request::<proto::StashPop>)
|
.add_request_handler(forward_mutating_project_request::<proto::StashPop>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::StashDrop>)
|
.add_request_handler(forward_mutating_project_request::<proto::StashDrop>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::Commit>)
|
.add_request_handler(forward_mutating_project_request::<proto::Commit>)
|
||||||
|
.add_request_handler(forward_mutating_project_request::<proto::RunGitHook>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::GitInit>)
|
.add_request_handler(forward_mutating_project_request::<proto::GitInit>)
|
||||||
.add_request_handler(forward_read_only_project_request::<proto::GetRemotes>)
|
.add_request_handler(forward_read_only_project_request::<proto::GetRemotes>)
|
||||||
.add_request_handler(forward_read_only_project_request::<proto::GitShow>)
|
.add_request_handler(forward_read_only_project_request::<proto::GitShow>)
|
||||||
@@ -984,14 +985,14 @@ impl Server {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let mut pool = self.connection_pool.lock();
|
let mut pool = self.connection_pool.lock();
|
||||||
pool.add_connection(connection_id, user.id, user.admin, zed_version);
|
pool.add_connection(connection_id, user.id, user.admin, zed_version.clone());
|
||||||
self.peer.send(
|
self.peer.send(
|
||||||
connection_id,
|
connection_id,
|
||||||
build_initial_contacts_update(contacts, &pool),
|
build_initial_contacts_update(contacts, &pool),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if should_auto_subscribe_to_channels(zed_version) {
|
if should_auto_subscribe_to_channels(&zed_version) {
|
||||||
subscribe_user_to_channels(user.id, session).await?;
|
subscribe_user_to_channels(user.id, session).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1135,7 +1136,7 @@ impl Header for ProtocolVersion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppVersionHeader(SemanticVersion);
|
pub struct AppVersionHeader(Version);
|
||||||
impl Header for AppVersionHeader {
|
impl Header for AppVersionHeader {
|
||||||
fn name() -> &'static HeaderName {
|
fn name() -> &'static HeaderName {
|
||||||
static ZED_APP_VERSION: OnceLock<HeaderName> = OnceLock::new();
|
static ZED_APP_VERSION: OnceLock<HeaderName> = OnceLock::new();
|
||||||
@@ -2833,8 +2834,8 @@ async fn remove_contact(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_auto_subscribe_to_channels(version: ZedVersion) -> bool {
|
fn should_auto_subscribe_to_channels(version: &ZedVersion) -> bool {
|
||||||
version.0.minor() < 139
|
version.0.minor < 139
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn subscribe_to_channels(
|
async fn subscribe_to_channels(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::db::{ChannelId, ChannelRole, UserId};
|
|||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use collections::{BTreeMap, HashMap, HashSet};
|
use collections::{BTreeMap, HashMap, HashSet};
|
||||||
use rpc::ConnectionId;
|
use rpc::ConnectionId;
|
||||||
use semantic_version::SemanticVersion;
|
use semver::Version;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
@@ -19,8 +19,8 @@ struct ConnectedPrincipal {
|
|||||||
connection_ids: HashSet<ConnectionId>,
|
connection_ids: HashSet<ConnectionId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, PartialOrd, PartialEq, Eq, Ord)]
|
#[derive(Clone, Debug, Serialize, PartialOrd, PartialEq, Eq, Ord)]
|
||||||
pub struct ZedVersion(pub SemanticVersion);
|
pub struct ZedVersion(pub Version);
|
||||||
|
|
||||||
impl fmt::Display for ZedVersion {
|
impl fmt::Display for ZedVersion {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
@@ -32,13 +32,13 @@ impl ZedVersion {
|
|||||||
pub fn can_collaborate(&self) -> bool {
|
pub fn can_collaborate(&self) -> bool {
|
||||||
// v0.204.1 was the first version after the auto-update bug.
|
// v0.204.1 was the first version after the auto-update bug.
|
||||||
// We reject any clients older than that to hope we can persuade them to upgrade.
|
// We reject any clients older than that to hope we can persuade them to upgrade.
|
||||||
if self.0 < SemanticVersion::new(0, 204, 1) {
|
if self.0 < Version::new(0, 204, 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since we hotfixed the changes to no longer connect to Collab automatically to Preview, we also need to reject
|
// Since we hotfixed the changes to no longer connect to Collab automatically to Preview, we also need to reject
|
||||||
// versions in the range [v0.199.0, v0.199.1].
|
// versions in the range [v0.199.0, v0.199.1].
|
||||||
if self.0 >= SemanticVersion::new(0, 199, 0) && self.0 < SemanticVersion::new(0, 199, 2) {
|
if self.0 >= Version::new(0, 199, 0) && self.0 < Version::new(0, 199, 2) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ use editor::{
|
|||||||
DocumentColorsRenderMode, Editor, FETCH_COLORS_DEBOUNCE_TIMEOUT, MultiBufferOffset, RowInfo,
|
DocumentColorsRenderMode, Editor, FETCH_COLORS_DEBOUNCE_TIMEOUT, MultiBufferOffset, RowInfo,
|
||||||
SelectionEffects,
|
SelectionEffects,
|
||||||
actions::{
|
actions::{
|
||||||
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
|
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, CopyFileLocation,
|
||||||
ExpandMacroRecursively, MoveToEnd, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
|
CopyFileName, CopyFileNameWithoutExtension, ExpandMacroRecursively, MoveToEnd, Redo,
|
||||||
|
Rename, SelectAll, ToggleCodeActions, Undo,
|
||||||
},
|
},
|
||||||
test::{
|
test::{
|
||||||
editor_test_context::{AssertionContextManager, EditorTestContext},
|
editor_test_context::{AssertionContextManager, EditorTestContext},
|
||||||
@@ -4269,6 +4270,288 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_copy_file_name_without_extension(
|
||||||
|
cx_a: &mut TestAppContext,
|
||||||
|
cx_b: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
let mut server = TestServer::start(cx_a.executor()).await;
|
||||||
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
server
|
||||||
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx_b.update(editor::init);
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.fs()
|
||||||
|
.insert_tree(
|
||||||
|
path!("/root"),
|
||||||
|
json!({
|
||||||
|
"src": {
|
||||||
|
"main.rs": indoc! {"
|
||||||
|
fn main() {
|
||||||
|
println!(\"Hello, world!\");
|
||||||
|
}
|
||||||
|
"},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
|
||||||
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
let project_id = active_call_a
|
||||||
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||||
|
|
||||||
|
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||||
|
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||||
|
|
||||||
|
let editor_a = workspace_a
|
||||||
|
.update_in(cx_a, |workspace, window, cx| {
|
||||||
|
workspace.open_path(
|
||||||
|
(worktree_id, rel_path("src/main.rs")),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let editor_b = workspace_b
|
||||||
|
.update_in(cx_b, |workspace, window, cx| {
|
||||||
|
workspace.open_path(
|
||||||
|
(worktree_id, rel_path("src/main.rs")),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx_a.run_until_parked();
|
||||||
|
cx_b.run_until_parked();
|
||||||
|
|
||||||
|
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||||
|
editor.copy_file_name_without_extension(&CopyFileNameWithoutExtension, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
cx_a.read_from_clipboard().and_then(|item| item.text()),
|
||||||
|
Some("main".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||||
|
editor.copy_file_name_without_extension(&CopyFileNameWithoutExtension, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
cx_b.read_from_clipboard().and_then(|item| item.text()),
|
||||||
|
Some("main".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_copy_file_name(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||||
|
let mut server = TestServer::start(cx_a.executor()).await;
|
||||||
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
server
|
||||||
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx_b.update(editor::init);
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.fs()
|
||||||
|
.insert_tree(
|
||||||
|
path!("/root"),
|
||||||
|
json!({
|
||||||
|
"src": {
|
||||||
|
"main.rs": indoc! {"
|
||||||
|
fn main() {
|
||||||
|
println!(\"Hello, world!\");
|
||||||
|
}
|
||||||
|
"},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
|
||||||
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
let project_id = active_call_a
|
||||||
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||||
|
|
||||||
|
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||||
|
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||||
|
|
||||||
|
let editor_a = workspace_a
|
||||||
|
.update_in(cx_a, |workspace, window, cx| {
|
||||||
|
workspace.open_path(
|
||||||
|
(worktree_id, rel_path("src/main.rs")),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let editor_b = workspace_b
|
||||||
|
.update_in(cx_b, |workspace, window, cx| {
|
||||||
|
workspace.open_path(
|
||||||
|
(worktree_id, rel_path("src/main.rs")),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx_a.run_until_parked();
|
||||||
|
cx_b.run_until_parked();
|
||||||
|
|
||||||
|
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||||
|
editor.copy_file_name(&CopyFileName, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
cx_a.read_from_clipboard().and_then(|item| item.text()),
|
||||||
|
Some("main.rs".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||||
|
editor.copy_file_name(&CopyFileName, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
cx_b.read_from_clipboard().and_then(|item| item.text()),
|
||||||
|
Some("main.rs".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_copy_file_location(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||||
|
let mut server = TestServer::start(cx_a.executor()).await;
|
||||||
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
server
|
||||||
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx_b.update(editor::init);
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.fs()
|
||||||
|
.insert_tree(
|
||||||
|
path!("/root"),
|
||||||
|
json!({
|
||||||
|
"src": {
|
||||||
|
"main.rs": indoc! {"
|
||||||
|
fn main() {
|
||||||
|
println!(\"Hello, world!\");
|
||||||
|
}
|
||||||
|
"},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
|
||||||
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
let project_id = active_call_a
|
||||||
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||||
|
|
||||||
|
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||||
|
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||||
|
|
||||||
|
let editor_a = workspace_a
|
||||||
|
.update_in(cx_a, |workspace, window, cx| {
|
||||||
|
workspace.open_path(
|
||||||
|
(worktree_id, rel_path("src/main.rs")),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let editor_b = workspace_b
|
||||||
|
.update_in(cx_b, |workspace, window, cx| {
|
||||||
|
workspace.open_path(
|
||||||
|
(worktree_id, rel_path("src/main.rs")),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx_a.run_until_parked();
|
||||||
|
cx_b.run_until_parked();
|
||||||
|
|
||||||
|
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||||
|
editor.change_selections(Default::default(), window, cx, |s| {
|
||||||
|
s.select_ranges([MultiBufferOffset(16)..MultiBufferOffset(16)]);
|
||||||
|
});
|
||||||
|
editor.copy_file_location(&CopyFileLocation, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
cx_a.read_from_clipboard().and_then(|item| item.text()),
|
||||||
|
Some(format!("{}:2", path!("src/main.rs")))
|
||||||
|
);
|
||||||
|
|
||||||
|
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||||
|
editor.change_selections(Default::default(), window, cx, |s| {
|
||||||
|
s.select_ranges([MultiBufferOffset(16)..MultiBufferOffset(16)]);
|
||||||
|
});
|
||||||
|
editor.copy_file_location(&CopyFileLocation, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
cx_b.read_from_clipboard().and_then(|item| item.text()),
|
||||||
|
Some(format!("{}:2", path!("src/main.rs")))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn tab_undo_assert(
|
fn tab_undo_assert(
|
||||||
cx_a: &mut EditorTestContext,
|
cx_a: &mut EditorTestContext,
|
||||||
|
|||||||
@@ -529,7 +529,7 @@ async fn test_basic_following(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Client B activates a panel, and the previously-opened screen-sharing item gets activated.
|
// Client B activates a panel, and the previously-opened screen-sharing item gets activated.
|
||||||
let panel = cx_b.new(|cx| TestPanel::new(DockPosition::Left, cx));
|
let panel = cx_b.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
|
||||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||||
workspace.add_panel(panel, window, cx);
|
workspace.add_panel(panel, window, cx);
|
||||||
workspace.toggle_panel_focus::<TestPanel>(window, cx);
|
workspace.toggle_panel_focus::<TestPanel>(window, cx);
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ use debugger_ui::debugger_panel::DebugPanel;
|
|||||||
use extension::ExtensionHostProxy;
|
use extension::ExtensionHostProxy;
|
||||||
use fs::{FakeFs, Fs as _, RemoveOptions};
|
use fs::{FakeFs, Fs as _, RemoveOptions};
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
use gpui::{
|
use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as _, VisualContext};
|
||||||
AppContext as _, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal as _,
|
|
||||||
VisualContext,
|
|
||||||
};
|
|
||||||
use http_client::BlockedHttpClient;
|
use http_client::BlockedHttpClient;
|
||||||
use language::{
|
use language::{
|
||||||
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
|
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
|
||||||
@@ -43,10 +40,10 @@ async fn test_sharing_an_ssh_remote_project(
|
|||||||
) {
|
) {
|
||||||
let executor = cx_a.executor();
|
let executor = cx_a.executor();
|
||||||
cx_a.update(|cx| {
|
cx_a.update(|cx| {
|
||||||
release_channel::init(SemanticVersion::default(), cx);
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||||
});
|
});
|
||||||
server_cx.update(|cx| {
|
server_cx.update(|cx| {
|
||||||
release_channel::init(SemanticVersion::default(), cx);
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||||
});
|
});
|
||||||
let mut server = TestServer::start(executor.clone()).await;
|
let mut server = TestServer::start(executor.clone()).await;
|
||||||
let client_a = server.create_client(cx_a, "user_a").await;
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
@@ -211,10 +208,10 @@ async fn test_ssh_collaboration_git_branches(
|
|||||||
server_cx.set_name("server");
|
server_cx.set_name("server");
|
||||||
|
|
||||||
cx_a.update(|cx| {
|
cx_a.update(|cx| {
|
||||||
release_channel::init(SemanticVersion::default(), cx);
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||||
});
|
});
|
||||||
server_cx.update(|cx| {
|
server_cx.update(|cx| {
|
||||||
release_channel::init(SemanticVersion::default(), cx);
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut server = TestServer::start(executor.clone()).await;
|
let mut server = TestServer::start(executor.clone()).await;
|
||||||
@@ -396,10 +393,10 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
|||||||
server_cx.set_name("server");
|
server_cx.set_name("server");
|
||||||
|
|
||||||
cx_a.update(|cx| {
|
cx_a.update(|cx| {
|
||||||
release_channel::init(SemanticVersion::default(), cx);
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||||
});
|
});
|
||||||
server_cx.update(|cx| {
|
server_cx.update(|cx| {
|
||||||
release_channel::init(SemanticVersion::default(), cx);
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut server = TestServer::start(executor.clone()).await;
|
let mut server = TestServer::start(executor.clone()).await;
|
||||||
@@ -583,13 +580,13 @@ async fn test_remote_server_debugger(
|
|||||||
executor: BackgroundExecutor,
|
executor: BackgroundExecutor,
|
||||||
) {
|
) {
|
||||||
cx_a.update(|cx| {
|
cx_a.update(|cx| {
|
||||||
release_channel::init(SemanticVersion::default(), cx);
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||||
command_palette_hooks::init(cx);
|
command_palette_hooks::init(cx);
|
||||||
zlog::init_test();
|
zlog::init_test();
|
||||||
dap_adapters::init(cx);
|
dap_adapters::init(cx);
|
||||||
});
|
});
|
||||||
server_cx.update(|cx| {
|
server_cx.update(|cx| {
|
||||||
release_channel::init(SemanticVersion::default(), cx);
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||||
dap_adapters::init(cx);
|
dap_adapters::init(cx);
|
||||||
});
|
});
|
||||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||||
@@ -691,13 +688,13 @@ async fn test_slow_adapter_startup_retries(
|
|||||||
executor: BackgroundExecutor,
|
executor: BackgroundExecutor,
|
||||||
) {
|
) {
|
||||||
cx_a.update(|cx| {
|
cx_a.update(|cx| {
|
||||||
release_channel::init(SemanticVersion::default(), cx);
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||||
command_palette_hooks::init(cx);
|
command_palette_hooks::init(cx);
|
||||||
zlog::init_test();
|
zlog::init_test();
|
||||||
dap_adapters::init(cx);
|
dap_adapters::init(cx);
|
||||||
});
|
});
|
||||||
server_cx.update(|cx| {
|
server_cx.update(|cx| {
|
||||||
release_channel::init(SemanticVersion::default(), cx);
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||||
dap_adapters::init(cx);
|
dap_adapters::init(cx);
|
||||||
});
|
});
|
||||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ use rpc::{
|
|||||||
RECEIVE_TIMEOUT,
|
RECEIVE_TIMEOUT,
|
||||||
proto::{self, ChannelRole},
|
proto::{self, ChannelRole},
|
||||||
};
|
};
|
||||||
use semantic_version::SemanticVersion;
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use session::{AppSession, Session};
|
use session::{AppSession, Session};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
@@ -173,7 +172,7 @@ impl TestServer {
|
|||||||
let settings = SettingsStore::test(cx);
|
let settings = SettingsStore::test(cx);
|
||||||
cx.set_global(settings);
|
cx.set_global(settings);
|
||||||
theme::init(theme::LoadThemes::JustBase, cx);
|
theme::init(theme::LoadThemes::JustBase, cx);
|
||||||
release_channel::init(SemanticVersion::default(), cx);
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let clock = Arc::new(FakeSystemClock::new());
|
let clock = Arc::new(FakeSystemClock::new());
|
||||||
@@ -295,7 +294,7 @@ impl TestServer {
|
|||||||
server_conn,
|
server_conn,
|
||||||
client_name,
|
client_name,
|
||||||
Principal::User(user),
|
Principal::User(user),
|
||||||
ZedVersion(SemanticVersion::new(1, 0, 0)),
|
ZedVersion(semver::Version::new(1, 0, 0)),
|
||||||
Some("test".to_string()),
|
Some("test".to_string()),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use ui::{Avatar, CheckboxWithLabel, ContextMenu, ListItem, ListItemSpacing, prelude::*};
|
use ui::{Avatar, Checkbox, ContextMenu, ListItem, ListItemSpacing, prelude::*};
|
||||||
use util::TryFutureExt;
|
use util::TryFutureExt;
|
||||||
use workspace::{ModalView, notifications::DetachAndPromptErr};
|
use workspace::{ModalView, notifications::DetachAndPromptErr};
|
||||||
|
|
||||||
@@ -165,16 +165,18 @@ impl Render for ChannelModal {
|
|||||||
.h(rems_from_px(22.))
|
.h(rems_from_px(22.))
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.line_height(rems(1.25))
|
.line_height(rems(1.25))
|
||||||
.child(CheckboxWithLabel::new(
|
.child(
|
||||||
"is-public",
|
Checkbox::new(
|
||||||
Label::new("Public").size(LabelSize::Small),
|
"is-public",
|
||||||
if visibility == ChannelVisibility::Public {
|
if visibility == ChannelVisibility::Public {
|
||||||
ui::ToggleState::Selected
|
ui::ToggleState::Selected
|
||||||
} else {
|
} else {
|
||||||
ui::ToggleState::Unselected
|
ui::ToggleState::Unselected
|
||||||
},
|
},
|
||||||
cx.listener(Self::set_channel_visibility),
|
)
|
||||||
))
|
.label("Public")
|
||||||
|
.on_click(cx.listener(Self::set_channel_visibility)),
|
||||||
|
)
|
||||||
.children(
|
.children(
|
||||||
Some(
|
Some(
|
||||||
Button::new("copy-link", "Copy Link")
|
Button::new("copy-link", "Copy Link")
|
||||||
|
|||||||
@@ -353,6 +353,8 @@ pub enum ToolCallContent {
|
|||||||
pub struct FunctionContent {
|
pub struct FunctionContent {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub arguments: String,
|
pub arguments: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub thought_signature: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
@@ -396,6 +398,7 @@ pub struct ToolCallChunk {
|
|||||||
pub struct FunctionChunk {
|
pub struct FunctionChunk {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub arguments: Option<String>,
|
pub arguments: Option<String>,
|
||||||
|
pub thought_signature: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ impl EditPredictionProvider for CopilotCompletionProvider {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_refreshing(&self) -> bool {
|
fn is_refreshing(&self, _cx: &App) -> bool {
|
||||||
self.pending_refresh.is_some() && self.completions.is_empty()
|
self.pending_refresh.is_some() && self.completions.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,8 @@ pub enum ResponseInputItem {
|
|||||||
arguments: String,
|
arguments: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
status: Option<ItemStatus>,
|
status: Option<ItemStatus>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
thought_signature: Option<String>,
|
||||||
},
|
},
|
||||||
FunctionCallOutput {
|
FunctionCallOutput {
|
||||||
call_id: String,
|
call_id: String,
|
||||||
@@ -251,6 +253,8 @@ pub enum ResponseOutputItem {
|
|||||||
arguments: String,
|
arguments: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
status: Option<ItemStatus>,
|
status: Option<ItemStatus>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
thought_signature: Option<String>,
|
||||||
},
|
},
|
||||||
Reasoning {
|
Reasoning {
|
||||||
id: String,
|
id: String,
|
||||||
|
|||||||
@@ -51,11 +51,13 @@ pub async fn init(crash_init: InitCrashHandler) {
|
|||||||
unsafe { env::set_var("RUST_BACKTRACE", "1") };
|
unsafe { env::set_var("RUST_BACKTRACE", "1") };
|
||||||
old_hook(info);
|
old_hook(info);
|
||||||
// prevent the macOS crash dialog from popping up
|
// prevent the macOS crash dialog from popping up
|
||||||
std::process::exit(1);
|
if cfg!(target_os = "macos") {
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(Some(true), _) | (None, _) => {
|
_ => {
|
||||||
panic::set_hook(Box::new(panic_hook));
|
panic::set_hook(Box::new(panic_hook));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,11 +302,18 @@ pub fn panic_hook(info: &PanicHookInfo) {
|
|||||||
.map(|loc| format!("{}:{}", loc.file(), loc.line()))
|
.map(|loc| format!("{}:{}", loc.file(), loc.line()))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let current_thread = std::thread::current();
|
||||||
|
let thread_name = current_thread.name().unwrap_or("<unnamed>");
|
||||||
|
|
||||||
// wait 500ms for the crash handler process to start up
|
// wait 500ms for the crash handler process to start up
|
||||||
// if it's still not there just write panic info and no minidump
|
// if it's still not there just write panic info and no minidump
|
||||||
let retry_frequency = Duration::from_millis(100);
|
let retry_frequency = Duration::from_millis(100);
|
||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
if let Some(client) = CRASH_HANDLER.get() {
|
if let Some(client) = CRASH_HANDLER.get() {
|
||||||
|
let location = info
|
||||||
|
.location()
|
||||||
|
.map_or_else(|| "<unknown>".to_owned(), |location| location.to_string());
|
||||||
|
log::error!("thread '{thread_name}' panicked at {location}:\n{message}...");
|
||||||
client
|
client
|
||||||
.send_message(
|
.send_message(
|
||||||
2,
|
2,
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ theme.workspace = true
|
|||||||
tree-sitter-json.workspace = true
|
tree-sitter-json.workspace = true
|
||||||
tree-sitter.workspace = true
|
tree-sitter.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
|
ui_input.workspace = true
|
||||||
unindent = { workspace = true, optional = true }
|
unindent = { workspace = true, optional = true }
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
|||||||
@@ -651,6 +651,17 @@ impl DebugPanel {
|
|||||||
.tooltip(Tooltip::text("Open Debug Adapter Logs"))
|
.tooltip(Tooltip::text("Open Debug Adapter Logs"))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let close_bottom_panel_button = {
|
||||||
|
h_flex().pl_0p5().gap_1().child(Divider::vertical()).child(
|
||||||
|
IconButton::new("debug-close-panel", IconName::Close)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
window.dispatch_action(workspace::ToggleBottomDock.boxed_clone(), cx)
|
||||||
|
})
|
||||||
|
.tooltip(Tooltip::text("Close Panel")),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
div.w_full()
|
div.w_full()
|
||||||
.py_1()
|
.py_1()
|
||||||
@@ -658,7 +669,7 @@ impl DebugPanel {
|
|||||||
.justify_between()
|
.justify_between()
|
||||||
.border_b_1()
|
.border_b_1()
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
.when(is_side, |this| this.gap_1())
|
.when(is_side, |this| this.gap_1().h(Tab::container_height(cx)))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
@@ -957,6 +968,7 @@ impl DebugPanel {
|
|||||||
.child(edit_debug_json_button())
|
.child(edit_debug_json_button())
|
||||||
.child(documentation_button())
|
.child(documentation_button())
|
||||||
.child(logs_button())
|
.child(logs_button())
|
||||||
|
.child(close_bottom_panel_button)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1692,7 +1704,7 @@ impl Render for DebugPanel {
|
|||||||
.child(
|
.child(
|
||||||
Button::new("spawn-new-session-empty-state", "New Session")
|
Button::new("spawn-new-session-empty-state", "New Session")
|
||||||
.icon(IconName::Plus)
|
.icon(IconName::Plus)
|
||||||
.icon_size(IconSize::XSmall)
|
.icon_size(IconSize::Small)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.on_click(|_, window, cx| {
|
.on_click(|_, window, cx| {
|
||||||
@@ -1702,8 +1714,7 @@ impl Render for DebugPanel {
|
|||||||
.child(
|
.child(
|
||||||
Button::new("edit-debug-settings", "Edit debug.json")
|
Button::new("edit-debug-settings", "Edit debug.json")
|
||||||
.icon(IconName::Code)
|
.icon(IconName::Code)
|
||||||
.icon_size(IconSize::XSmall)
|
.icon_size(IconSize::Small)
|
||||||
.color(Color::Muted)
|
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.on_click(|_, window, cx| {
|
.on_click(|_, window, cx| {
|
||||||
@@ -1716,8 +1727,7 @@ impl Render for DebugPanel {
|
|||||||
.child(
|
.child(
|
||||||
Button::new("open-debugger-docs", "Debugger Docs")
|
Button::new("open-debugger-docs", "Debugger Docs")
|
||||||
.icon(IconName::Book)
|
.icon(IconName::Book)
|
||||||
.color(Color::Muted)
|
.icon_size(IconSize::Small)
|
||||||
.icon_size(IconSize::XSmall)
|
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/debugger")),
|
.on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/debugger")),
|
||||||
@@ -1728,8 +1738,7 @@ impl Render for DebugPanel {
|
|||||||
"Debugger Extensions",
|
"Debugger Extensions",
|
||||||
)
|
)
|
||||||
.icon(IconName::Blocks)
|
.icon(IconName::Blocks)
|
||||||
.color(Color::Muted)
|
.icon_size(IconSize::Small)
|
||||||
.icon_size(IconSize::XSmall)
|
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.on_click(|_, window, cx| {
|
.on_click(|_, window, cx| {
|
||||||
@@ -1746,6 +1755,15 @@ impl Render for DebugPanel {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let has_breakpoints = self
|
||||||
|
.project
|
||||||
|
.read(cx)
|
||||||
|
.breakpoint_store()
|
||||||
|
.read(cx)
|
||||||
|
.all_source_breakpoints(cx)
|
||||||
|
.values()
|
||||||
|
.any(|breakpoints| !breakpoints.is_empty());
|
||||||
|
|
||||||
let breakpoint_list = v_flex()
|
let breakpoint_list = v_flex()
|
||||||
.group("base-breakpoint-list")
|
.group("base-breakpoint-list")
|
||||||
.when_else(
|
.when_else(
|
||||||
@@ -1769,7 +1787,18 @@ impl Render for DebugPanel {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(self.breakpoint_list.clone());
|
.when(has_breakpoints, |this| {
|
||||||
|
this.child(self.breakpoint_list.clone())
|
||||||
|
})
|
||||||
|
.when(!has_breakpoints, |this| {
|
||||||
|
this.child(
|
||||||
|
v_flex().size_full().items_center().justify_center().child(
|
||||||
|
Label::new("No Breakpoints Set")
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
this.child(
|
this.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
|||||||
@@ -12,23 +12,22 @@ use tasks_ui::{TaskOverrides, TasksModal};
|
|||||||
use dap::{
|
use dap::{
|
||||||
DapRegistry, DebugRequest, TelemetrySpawnLocation, adapters::DebugAdapterName, send_telemetry,
|
DapRegistry, DebugRequest, TelemetrySpawnLocation, adapters::DebugAdapterName, send_telemetry,
|
||||||
};
|
};
|
||||||
use editor::{Editor, EditorElement, EditorStyle};
|
use editor::Editor;
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
KeyContext, Render, Subscription, Task, TextStyle, WeakEntity,
|
KeyContext, Render, Subscription, Task, WeakEntity,
|
||||||
};
|
};
|
||||||
use itertools::Itertools as _;
|
use itertools::Itertools as _;
|
||||||
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
|
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
|
||||||
use project::{DebugScenarioContext, Project, TaskContexts, TaskSourceKind, task_store::TaskStore};
|
use project::{DebugScenarioContext, Project, TaskContexts, TaskSourceKind, task_store::TaskStore};
|
||||||
use settings::Settings;
|
|
||||||
use task::{DebugScenario, RevealTarget, VariableName, ZedDebugConfig};
|
use task::{DebugScenario, RevealTarget, VariableName, ZedDebugConfig};
|
||||||
use theme::ThemeSettings;
|
|
||||||
use ui::{
|
use ui::{
|
||||||
CheckboxWithLabel, ContextMenu, DropdownMenu, FluentBuilder, IconWithIndicator, Indicator,
|
ContextMenu, DropdownMenu, FluentBuilder, IconWithIndicator, Indicator, KeyBinding, ListItem,
|
||||||
KeyBinding, ListItem, ListItemSpacing, ToggleButtonGroup, ToggleButtonSimple, ToggleState,
|
ListItemSpacing, Switch, SwitchLabelPosition, ToggleButtonGroup, ToggleButtonSimple,
|
||||||
Tooltip, prelude::*,
|
ToggleState, Tooltip, prelude::*,
|
||||||
};
|
};
|
||||||
|
use ui_input::InputField;
|
||||||
use util::{ResultExt, debug_panic, rel_path::RelPath, shell::ShellKind};
|
use util::{ResultExt, debug_panic, rel_path::RelPath, shell::ShellKind};
|
||||||
use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane};
|
use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane};
|
||||||
|
|
||||||
@@ -448,7 +447,7 @@ impl NewProcessModal {
|
|||||||
&mut self,
|
&mut self,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> ui::DropdownMenu {
|
) -> DropdownMenu {
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
let weak = cx.weak_entity();
|
let weak = cx.weak_entity();
|
||||||
let active_buffer = self.task_contexts(cx).and_then(|tc| {
|
let active_buffer = self.task_contexts(cx).and_then(|tc| {
|
||||||
@@ -508,6 +507,13 @@ impl NewProcessModal {
|
|||||||
menu
|
menu
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.style(ui::DropdownStyle::Outlined)
|
||||||
|
.tab_index(0)
|
||||||
|
.attach(gpui::Corner::BottomLeft)
|
||||||
|
.offset(gpui::Point {
|
||||||
|
x: px(0.0),
|
||||||
|
y: px(2.0),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,44 +546,6 @@ impl Focusable for NewProcessMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl IntoElement {
|
|
||||||
let settings = ThemeSettings::get_global(cx);
|
|
||||||
let theme = cx.theme();
|
|
||||||
|
|
||||||
let text_style = TextStyle {
|
|
||||||
color: cx.theme().colors().text,
|
|
||||||
font_family: settings.buffer_font.family.clone(),
|
|
||||||
font_features: settings.buffer_font.features.clone(),
|
|
||||||
font_size: settings.buffer_font_size(cx).into(),
|
|
||||||
font_weight: settings.buffer_font.weight,
|
|
||||||
line_height: relative(settings.buffer_line_height.value()),
|
|
||||||
background_color: Some(theme.colors().editor_background),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let element = EditorElement::new(
|
|
||||||
editor,
|
|
||||||
EditorStyle {
|
|
||||||
background: theme.colors().editor_background,
|
|
||||||
local_player: theme.players().local(),
|
|
||||||
text: text_style,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
div()
|
|
||||||
.rounded_md()
|
|
||||||
.p_1()
|
|
||||||
.border_1()
|
|
||||||
.border_color(theme.colors().border_variant)
|
|
||||||
.when(
|
|
||||||
editor.focus_handle(cx).contains_focused(window, cx),
|
|
||||||
|this| this.border_color(theme.colors().border_focused),
|
|
||||||
)
|
|
||||||
.child(element)
|
|
||||||
.bg(theme.colors().editor_background)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for NewProcessModal {
|
impl Render for NewProcessModal {
|
||||||
fn render(
|
fn render(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -788,22 +756,26 @@ impl RenderOnce for AttachMode {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(super) struct ConfigureMode {
|
pub(super) struct ConfigureMode {
|
||||||
program: Entity<Editor>,
|
program: Entity<InputField>,
|
||||||
cwd: Entity<Editor>,
|
cwd: Entity<InputField>,
|
||||||
stop_on_entry: ToggleState,
|
stop_on_entry: ToggleState,
|
||||||
save_to_debug_json: ToggleState,
|
save_to_debug_json: ToggleState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigureMode {
|
impl ConfigureMode {
|
||||||
pub(super) fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
pub(super) fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
let program = cx.new(|cx| Editor::single_line(window, cx));
|
let program = cx.new(|cx| {
|
||||||
program.update(cx, |this, cx| {
|
InputField::new(window, cx, "ENV=Zed ~/bin/program --option")
|
||||||
this.set_placeholder_text("ENV=Zed ~/bin/program --option", window, cx);
|
.label("Program")
|
||||||
|
.tab_stop(true)
|
||||||
|
.tab_index(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
let cwd = cx.new(|cx| Editor::single_line(window, cx));
|
let cwd = cx.new(|cx| {
|
||||||
cwd.update(cx, |this, cx| {
|
InputField::new(window, cx, "Ex: $ZED_WORKTREE_ROOT")
|
||||||
this.set_placeholder_text("Ex: $ZED_WORKTREE_ROOT", window, cx);
|
.label("Working Directory")
|
||||||
|
.tab_stop(true)
|
||||||
|
.tab_index(2)
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.new(|_| Self {
|
cx.new(|_| Self {
|
||||||
@@ -815,9 +787,9 @@ impl ConfigureMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load(&mut self, cwd: PathBuf, window: &mut Window, cx: &mut App) {
|
fn load(&mut self, cwd: PathBuf, window: &mut Window, cx: &mut App) {
|
||||||
self.cwd.update(cx, |editor, cx| {
|
self.cwd.update(cx, |input_field, cx| {
|
||||||
if editor.is_empty(cx) {
|
if input_field.is_empty(cx) {
|
||||||
editor.set_text(cwd.to_string_lossy(), window, cx);
|
input_field.set_text(cwd.to_string_lossy(), window, cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -868,55 +840,49 @@ impl ConfigureMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
|
||||||
|
window.focus_next();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_tab_prev(
|
||||||
|
&mut self,
|
||||||
|
_: &menu::SelectPrevious,
|
||||||
|
window: &mut Window,
|
||||||
|
_: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
window.focus_prev();
|
||||||
|
}
|
||||||
|
|
||||||
fn render(
|
fn render(
|
||||||
&mut self,
|
&mut self,
|
||||||
adapter_menu: DropdownMenu,
|
adapter_menu: DropdownMenu,
|
||||||
window: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut ui::Context<Self>,
|
cx: &mut ui::Context<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.tab_group()
|
||||||
|
.track_focus(&self.program.focus_handle(cx))
|
||||||
|
.on_action(cx.listener(Self::on_tab))
|
||||||
|
.on_action(cx.listener(Self::on_tab_prev))
|
||||||
.p_2()
|
.p_2()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_2()
|
.gap_3()
|
||||||
.track_focus(&self.program.focus_handle(cx))
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_1()
|
||||||
.child(
|
.child(Label::new("Debugger:").color(Color::Muted))
|
||||||
Label::new("Debugger")
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.color(Color::Muted),
|
|
||||||
)
|
|
||||||
.child(adapter_menu),
|
.child(adapter_menu),
|
||||||
)
|
)
|
||||||
|
.child(self.program.clone())
|
||||||
|
.child(self.cwd.clone())
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
Switch::new("debugger-stop-on-entry", self.stop_on_entry)
|
||||||
.gap_0p5()
|
.tab_index(3_isize)
|
||||||
.child(
|
.label("Stop on Entry")
|
||||||
Label::new("Program")
|
.label_position(SwitchLabelPosition::Start)
|
||||||
.size(LabelSize::Small)
|
.label_size(LabelSize::Default)
|
||||||
.color(Color::Muted),
|
.color(ui::SwitchColor::Accent)
|
||||||
)
|
.on_click({
|
||||||
.child(render_editor(&self.program, window, cx)),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_0p5()
|
|
||||||
.child(
|
|
||||||
Label::new("Working Directory")
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.color(Color::Muted),
|
|
||||||
)
|
|
||||||
.child(render_editor(&self.cwd, window, cx)),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
CheckboxWithLabel::new(
|
|
||||||
"debugger-stop-on-entry",
|
|
||||||
Label::new("Stop on Entry")
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.color(Color::Muted),
|
|
||||||
self.stop_on_entry,
|
|
||||||
{
|
|
||||||
let this = cx.weak_entity();
|
let this = cx.weak_entity();
|
||||||
move |state, _, cx| {
|
move |state, _, cx| {
|
||||||
this.update(cx, |this, _| {
|
this.update(cx, |this, _| {
|
||||||
@@ -924,9 +890,7 @@ impl ConfigureMode {
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
)
|
|
||||||
.checkbox_position(ui::IconPosition::End),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1407,7 +1407,6 @@ impl RenderOnce for BreakpointOptionsStrip {
|
|||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_px()
|
.gap_px()
|
||||||
.mr_3() // Space to avoid overlapping with the scrollbar
|
|
||||||
.justify_end()
|
.justify_end()
|
||||||
.when(has_logs || self.is_selected, |this| {
|
.when(has_logs || self.is_selected, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user