Compare commits
86 Commits
git-clone
...
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
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Feature Request
|
||||
- name: Feature request
|
||||
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
|
||||
- name: "Zed Discord"
|
||||
url: https://zed.dev/community-links
|
||||
about: Real-time discussion and user support
|
||||
about: To request a feature, open a new discussion under one of the appropriate categories.
|
||||
- name: Our Discord community
|
||||
url: https://discord.com/invite/zedindustries
|
||||
about: Join our Discord server for real-time discussion and user support.
|
||||
|
||||
@@ -13,13 +13,65 @@ jobs:
|
||||
steps:
|
||||
- name: Check if author is a community champion and apply label
|
||||
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:
|
||||
script: |
|
||||
const communityChampionBody = `${{ secrets.COMMUNITY_CHAMPIONS }}`;
|
||||
|
||||
const communityChampions = communityChampionBody
|
||||
const communityChampions = process.env.COMMUNITY_CHAMPIONS
|
||||
.split('\n')
|
||||
.map(handle => handle.trim().toLowerCase());
|
||||
.map(handle => handle.trim().toLowerCase())
|
||||
.filter(handle => handle.length > 0);
|
||||
|
||||
let author;
|
||||
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
|
||||
- name: steps::cargo_nextest
|
||||
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}
|
||||
timeout-minutes: 3
|
||||
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
|
||||
shell: bash -euxo pipefail {0}
|
||||
- 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}
|
||||
- name: steps::cleanup_cargo_config
|
||||
if: always()
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
run: ./script/clear-target-dir-if-larger-than 250
|
||||
shell: bash -euxo pipefail {0}
|
||||
- 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}
|
||||
- name: steps::cleanup_cargo_config
|
||||
if: always()
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
||||
shell: pwsh
|
||||
- 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
|
||||
- name: steps::cleanup_cargo_config
|
||||
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
|
||||
shell: pwsh
|
||||
- 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
|
||||
- name: steps::cleanup_cargo_config
|
||||
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
|
||||
shell: pwsh
|
||||
- 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
|
||||
- name: steps::cleanup_cargo_config
|
||||
if: always()
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
run: ./script/clear-target-dir-if-larger-than 250
|
||||
shell: bash -euxo pipefail {0}
|
||||
- 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}
|
||||
- name: steps::cleanup_cargo_config
|
||||
if: always()
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
run: ./script/clear-target-dir-if-larger-than 300
|
||||
shell: bash -euxo pipefail {0}
|
||||
- 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}
|
||||
- name: steps::cleanup_cargo_config
|
||||
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/schema_generator",
|
||||
"crates/search",
|
||||
"crates/semantic_version",
|
||||
"crates/session",
|
||||
"crates/settings",
|
||||
"crates/settings_json",
|
||||
@@ -202,7 +201,6 @@ members = [
|
||||
"crates/zed_actions",
|
||||
"crates/zed_env_vars",
|
||||
"crates/zeta",
|
||||
"crates/zeta2",
|
||||
"crates/zeta_cli",
|
||||
"crates/zlog",
|
||||
"crates/zlog_settings",
|
||||
@@ -381,7 +379,6 @@ rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
rules_library = { path = "crates/rules_library" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
session = { path = "crates/session" }
|
||||
settings = { path = "crates/settings" }
|
||||
settings_json = { path = "crates/settings_json" }
|
||||
@@ -435,7 +432,6 @@ zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
zed_env_vars = { path = "crates/zed_env_vars" }
|
||||
zeta = { path = "crates/zeta" }
|
||||
zeta2 = { path = "crates/zeta2" }
|
||||
zlog = { path = "crates/zlog" }
|
||||
zlog_settings = { path = "crates/zlog_settings" }
|
||||
|
||||
@@ -607,7 +603,6 @@ pulldown-cmark = { version = "0.12.0", default-features = false }
|
||||
quote = "1.0.9"
|
||||
rand = "0.9"
|
||||
rayon = "1.8"
|
||||
ref-cast = "1.0.24"
|
||||
regex = "1.5"
|
||||
# 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 = [
|
||||
@@ -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
|
||||
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"] }
|
||||
semver = "1.0"
|
||||
semver = { version = "1.0", features = ["serde"] }
|
||||
serde = { version = "1.0.221", features = ["derive", "rc"] }
|
||||
serde_derive = "1.0.221"
|
||||
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_repr = "0.1"
|
||||
serde_urlencoded = "0.7"
|
||||
serde_with = "3.4.0"
|
||||
sha2 = "0.10"
|
||||
shellexpand = "2.1.0"
|
||||
shlex = "1.3.0"
|
||||
@@ -847,7 +841,6 @@ refineable = { codegen-units = 1 }
|
||||
release_channel = { codegen-units = 1 }
|
||||
reqwest_client = { codegen-units = 1 }
|
||||
rich_text = { codegen-units = 1 }
|
||||
semantic_version = { codegen-units = 1 }
|
||||
session = { codegen-units = 1 }
|
||||
snippet = { codegen-units = 1 }
|
||||
snippets_ui = { codegen-units = 1 }
|
||||
@@ -873,6 +866,10 @@ debug = "full"
|
||||
lto = false
|
||||
codegen-units = 16
|
||||
|
||||
[profile.profiling]
|
||||
inherits = "release"
|
||||
debug = "full"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "allow" }
|
||||
|
||||
|
||||
@@ -239,13 +239,11 @@
|
||||
"ctrl-alt-l": "agent::OpenRulesLibrary",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "agent::ToggleModelSelector",
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
"ctrl-shift-j": "agent::ToggleNavigationMenu",
|
||||
"ctrl-alt-i": "agent::ToggleOptionsMenu",
|
||||
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl->": "agent::AddSelectionToThread",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-enter": "agent::ContinueThread",
|
||||
"super-ctrl-b": "agent::ToggleBurnMode",
|
||||
@@ -322,17 +320,6 @@
|
||||
"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",
|
||||
"bindings": {
|
||||
@@ -824,8 +811,7 @@
|
||||
"context": "PromptEditor",
|
||||
"bindings": {
|
||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "agent::CycleNextInlineAssist",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||
"ctrl-]": "agent::CycleNextInlineAssist"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1251,11 +1237,25 @@
|
||||
"context": "Onboarding",
|
||||
"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 }],
|
||||
"ctrl-enter": "onboarding::Finish",
|
||||
"alt-shift-l": "onboarding::SignIn",
|
||||
"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",
|
||||
"use_key_equivalents": true,
|
||||
|
||||
@@ -278,13 +278,11 @@
|
||||
"cmd-alt-p": "agent::ManageProfiles",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||
"cmd-shift-a": "agent::ToggleContextPicker",
|
||||
"cmd-shift-j": "agent::ToggleNavigationMenu",
|
||||
"cmd-alt-m": "agent::ToggleOptionsMenu",
|
||||
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"cmd->": "agent::AddSelectionToThread",
|
||||
"cmd-alt-e": "agent::RemoveAllContext",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-ctrl-b": "agent::ToggleBurnMode",
|
||||
"cmd-shift-enter": "agent::ContinueThread",
|
||||
@@ -365,18 +363,6 @@
|
||||
"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",
|
||||
"bindings": {
|
||||
@@ -889,9 +875,7 @@
|
||||
"context": "PromptEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-a": "agent::ToggleContextPicker",
|
||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||
"cmd-alt-e": "agent::RemoveAllContext",
|
||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "agent::CycleNextInlineAssist"
|
||||
}
|
||||
@@ -1234,23 +1218,23 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "RateCompletionModal",
|
||||
"context": "RatePredictionsModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
|
||||
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion",
|
||||
"cmd-shift-enter": "zeta::ThumbsUpActivePrediction",
|
||||
"cmd-shift-backspace": "zeta::ThumbsDownActivePrediction",
|
||||
"shift-down": "zeta::NextEdit",
|
||||
"shift-up": "zeta::PreviousEdit",
|
||||
"right": "zeta::PreviewCompletion"
|
||||
"right": "zeta::PreviewPrediction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "RateCompletionModal > Editor",
|
||||
"context": "RatePredictionsModal > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "zeta::FocusCompletions",
|
||||
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
|
||||
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
|
||||
"escape": "zeta::FocusPredictions",
|
||||
"cmd-shift-enter": "zeta::ThumbsUpActivePrediction",
|
||||
"cmd-shift-backspace": "zeta::ThumbsDownActivePrediction"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1356,11 +1340,25 @@
|
||||
"context": "Onboarding",
|
||||
"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 }],
|
||||
"cmd-enter": "onboarding::Finish",
|
||||
"alt-tab": "onboarding::SignIn",
|
||||
"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",
|
||||
"use_key_equivalents": true,
|
||||
|
||||
@@ -240,13 +240,11 @@
|
||||
"shift-alt-p": "agent::ManageProfiles",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-alt-/": "agent::ToggleModelSelector",
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
"ctrl-shift-j": "agent::ToggleNavigationMenu",
|
||||
"ctrl-alt-i": "agent::ToggleOptionsMenu",
|
||||
// "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl-shift-.": "agent::AddSelectionToThread",
|
||||
"shift-alt-e": "agent::RemoveAllContext",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-enter": "agent::ContinueThread",
|
||||
"super-ctrl-b": "agent::ToggleBurnMode",
|
||||
@@ -328,18 +326,6 @@
|
||||
"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",
|
||||
"bindings": {
|
||||
@@ -837,8 +823,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "agent::CycleNextInlineAssist",
|
||||
"shift-alt-e": "agent::RemoveAllContext"
|
||||
"ctrl-]": "agent::CycleNextInlineAssist"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1285,11 +1270,25 @@
|
||||
"context": "Onboarding",
|
||||
"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 }],
|
||||
"ctrl-enter": "onboarding::Finish",
|
||||
"alt-shift-l": "onboarding::SignIn",
|
||||
"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)",
|
||||
"use_key_equivalents": true,
|
||||
|
||||
@@ -414,8 +414,9 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == helix_normal && !menu",
|
||||
"context": "VimControl && vim_mode == helix_normal && !menu",
|
||||
"bindings": {
|
||||
"escape": "vim::SwitchToHelixNormalMode",
|
||||
"i": "vim::HelixInsert",
|
||||
"a": "vim::HelixAppend",
|
||||
"ctrl-[": "editor::Cancel"
|
||||
|
||||
@@ -175,6 +175,16 @@
|
||||
//
|
||||
// Default: 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.
|
||||
// When set to false, Zed will use the built-in keyboard-first pickers.
|
||||
"use_system_path_prompts": true,
|
||||
@@ -1431,7 +1441,7 @@
|
||||
"default_height": 320,
|
||||
// What working directory to use when launching the terminal.
|
||||
// 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
|
||||
// "working_directory": "current_project_directory"
|
||||
// 2. Use the first project in this workspace's directory
|
||||
@@ -1585,7 +1595,59 @@
|
||||
//
|
||||
// Most terminal themes have APCA values of 40-70.
|
||||
// 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": {},
|
||||
// Settings related to running tasks.
|
||||
@@ -1827,7 +1889,7 @@
|
||||
}
|
||||
},
|
||||
"PHP": {
|
||||
"language_servers": ["phpactor", "!intelephense", "..."],
|
||||
"language_servers": ["phpactor", "!intelephense", "!phptools", "..."],
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["@prettier/plugin-php"],
|
||||
@@ -2076,7 +2138,7 @@
|
||||
"windows": {
|
||||
"languages": {
|
||||
"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<()>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns whether the model picker should render a footer.
|
||||
fn should_render_footer(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
||||
@@ -23,6 +23,7 @@ gpui.workspace = true
|
||||
language.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
semver.workspace = true
|
||||
smallvec.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -925,15 +925,15 @@ impl StatusItemView for ActivityIndicator {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::SemanticVersion;
|
||||
use release_channel::AppCommitSha;
|
||||
use semver::Version;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_version_tooltip_message() {
|
||||
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");
|
||||
|
||||
@@ -961,6 +961,10 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
|
||||
fn watch(&self, cx: &mut App) -> Option<watch::Receiver<()>> {
|
||||
Some(self.connection.0.read(cx).models.watch())
|
||||
}
|
||||
|
||||
fn should_render_footer(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
|
||||
@@ -182,6 +182,7 @@ impl DbThread {
|
||||
crate::Message::Agent(AgentMessage {
|
||||
content,
|
||||
tool_results,
|
||||
reasoning_details: None,
|
||||
})
|
||||
}
|
||||
language_model::Role::System => {
|
||||
|
||||
@@ -703,6 +703,7 @@ impl EditAgent {
|
||||
role: Role::User,
|
||||
content: vec![MessageContent::Text(prompt)],
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
});
|
||||
|
||||
// Include tools in the request so that we can take advantage of
|
||||
|
||||
@@ -1081,6 +1081,7 @@ fn message(
|
||||
role,
|
||||
content: contents.into_iter().collect(),
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1268,6 +1269,7 @@ impl EvalAssertion {
|
||||
role: Role::User,
|
||||
content: vec![prompt.into()],
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
}],
|
||||
thinking_allowed: true,
|
||||
..Default::default()
|
||||
@@ -1594,6 +1596,7 @@ impl EditAgentTest {
|
||||
role: Role::System,
|
||||
content: vec![MessageContent::Text(system_prompt)],
|
||||
cache: true,
|
||||
reasoning_details: None,
|
||||
}]
|
||||
.into_iter()
|
||||
.chain(eval.conversation)
|
||||
|
||||
@@ -48,7 +48,7 @@ pub async fn get_buffer_content_or_outline(
|
||||
if outline_items.is_empty() {
|
||||
let text = buffer.read_with(cx, |buffer, _| {
|
||||
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>();
|
||||
if let Some(path) = path {
|
||||
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 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 buffer = project
|
||||
.update(cx, |project, cx| project.create_buffer(true, cx))
|
||||
@@ -194,7 +194,7 @@ mod tests {
|
||||
|
||||
// Should contain some of the actual file content
|
||||
assert!(
|
||||
result.text.contains("AAAAAAAAAA"),
|
||||
result.text.contains("⚡⚡⚡⚡⚡⚡⚡"),
|
||||
"Result did not contain content subset"
|
||||
);
|
||||
|
||||
|
||||
@@ -215,7 +215,8 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
|
||||
vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec!["Message 1".into()],
|
||||
cache: true
|
||||
cache: true,
|
||||
reasoning_details: None,
|
||||
}]
|
||||
);
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text(
|
||||
@@ -239,17 +240,20 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec!["Message 1".into()],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec!["Response to Message 1".into()],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
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 {
|
||||
role: Role::User,
|
||||
content: vec!["Message 1".into()],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec!["Response to Message 1".into()],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec!["Message 2".into()],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec!["Response to Message 2".into()],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec!["Use the echo tool".into()],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec![MessageContent::ToolUse(tool_use)],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
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 {
|
||||
role: Role::User,
|
||||
content: vec!["abc".into()],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec![MessageContent::ToolUse(tool_use.clone())],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
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 {
|
||||
role: Role::User,
|
||||
content: vec!["abc".into()],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec![MessageContent::ToolUse(tool_use)],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![MessageContent::ToolResult(tool_result)],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
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 {
|
||||
role: Role::User,
|
||||
content: vec!["abc".into()],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec![MessageContent::ToolUse(tool_use)],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![MessageContent::ToolResult(tool_result)],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
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 {
|
||||
role: Role::User,
|
||||
content: vec!["Hey!".into()],
|
||||
cache: true
|
||||
cache: true,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
@@ -1835,7 +1858,8 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
|
||||
MessageContent::Text("Hi!".into()),
|
||||
MessageContent::ToolUse(echo_tool_use.clone())
|
||||
],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
@@ -1846,7 +1870,8 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
|
||||
content: "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 {
|
||||
role: Role::User,
|
||||
content: vec!["Call the echo tool!".into()],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())],
|
||||
cache: false
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
},
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
@@ -2262,7 +2289,8 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
|
||||
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(),
|
||||
Some(Message::Agent(AgentMessage {
|
||||
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,
|
||||
content: vec!["Continue where you left off".into()],
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
}],
|
||||
}
|
||||
}
|
||||
@@ -177,6 +178,7 @@ impl UserMessage {
|
||||
role: Role::User,
|
||||
content: Vec::with_capacity(self.content.len()),
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
};
|
||||
|
||||
const OPEN_CONTEXT: &str = "<context>\n\
|
||||
@@ -444,6 +446,7 @@ impl AgentMessage {
|
||||
role: Role::Assistant,
|
||||
content: Vec::with_capacity(self.content.len()),
|
||||
cache: false,
|
||||
reasoning_details: self.reasoning_details.clone(),
|
||||
};
|
||||
for chunk in &self.content {
|
||||
match chunk {
|
||||
@@ -479,6 +482,7 @@ impl AgentMessage {
|
||||
role: Role::User,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
};
|
||||
|
||||
for tool_result in self.tool_results.values() {
|
||||
@@ -508,6 +512,7 @@ impl AgentMessage {
|
||||
pub struct AgentMessage {
|
||||
pub content: Vec<AgentMessageContent>,
|
||||
pub tool_results: IndexMap<LanguageModelToolUseId, LanguageModelToolResult>,
|
||||
pub reasoning_details: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -1398,6 +1403,18 @@ impl Thread {
|
||||
self.handle_thinking_event(text, signature, event_stream, 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) => {
|
||||
return Ok(self.handle_tool_use_event(tool_use, event_stream, cx));
|
||||
}
|
||||
@@ -1673,6 +1690,7 @@ impl Thread {
|
||||
role: Role::User,
|
||||
content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()],
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
});
|
||||
|
||||
let task = cx
|
||||
@@ -1737,6 +1755,7 @@ impl Thread {
|
||||
role: Role::User,
|
||||
content: vec![SUMMARIZE_THREAD_PROMPT.into()],
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
});
|
||||
self.pending_title_generation = Some(cx.spawn(async move |this, cx| {
|
||||
let mut title = String::new();
|
||||
@@ -1984,6 +2003,7 @@ impl Thread {
|
||||
role: Role::System,
|
||||
content: vec![system_prompt.into()],
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
}];
|
||||
for message in &self.messages {
|
||||
messages.extend(message.to_request());
|
||||
|
||||
@@ -69,7 +69,6 @@ postage.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
ref-cast.workspace = true
|
||||
release_channel.workspace = true
|
||||
rope.workspace = true
|
||||
rules_library.workspace = true
|
||||
@@ -93,7 +92,6 @@ time_format.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
url.workspace = true
|
||||
urlencoding.workspace = true
|
||||
util.workspace = true
|
||||
watch.workspace = true
|
||||
workspace.workspace = true
|
||||
@@ -115,6 +113,7 @@ languages = { workspace = true, features = ["test-support"] }
|
||||
language_model = { workspace = true, "features" = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
semver.workspace = true
|
||||
rand.workspace = true
|
||||
tree-sitter-md.workspace = true
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
mod completion_provider;
|
||||
mod entry_view_state;
|
||||
mod message_editor;
|
||||
mod mode_selector;
|
||||
|
||||
@@ -405,7 +405,7 @@ mod tests {
|
||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||
use editor::RowInfo;
|
||||
use fs::FakeFs;
|
||||
use gpui::{AppContext as _, SemanticVersion, TestAppContext};
|
||||
use gpui::{AppContext as _, TestAppContext};
|
||||
|
||||
use crate::acp::entry_view_state::EntryViewState;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
@@ -539,7 +539,7 @@ mod tests {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
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 futures::FutureExt;
|
||||
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 picker::{Picker, PickerDelegate};
|
||||
use ui::{
|
||||
DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, ListItem,
|
||||
DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, KeyBinding, ListItem,
|
||||
ListItemSpacing, prelude::*,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use zed_actions::agent::OpenSettings;
|
||||
|
||||
use crate::ui::HoldForDefault;
|
||||
|
||||
@@ -24,10 +27,12 @@ pub fn acp_model_selector(
|
||||
selector: Rc<dyn AgentModelSelector>,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<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)
|
||||
.show_scrollbar(true)
|
||||
.width(rems(20.))
|
||||
@@ -49,6 +54,7 @@ pub struct AcpModelPickerDelegate {
|
||||
selected_description: Option<(usize, SharedString, bool)>,
|
||||
selected_model: Option<AgentModelInfo>,
|
||||
_refresh_models_task: Task<()>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl AcpModelPickerDelegate {
|
||||
@@ -56,6 +62,7 @@ impl AcpModelPickerDelegate {
|
||||
selector: Rc<dyn AgentModelSelector>,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<AcpModelSelector>,
|
||||
) -> Self {
|
||||
@@ -104,6 +111,7 @@ impl AcpModelPickerDelegate {
|
||||
selected_index: 0,
|
||||
selected_description: None,
|
||||
_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(
|
||||
|
||||
@@ -30,8 +30,18 @@ impl AcpModelSelectorPopover {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle_clone = focus_handle.clone();
|
||||
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,
|
||||
focus_handle,
|
||||
}
|
||||
|
||||
@@ -297,6 +297,7 @@ pub struct AcpThreadView {
|
||||
_cancel_task: Option<Task<()>>,
|
||||
_subscriptions: [Subscription; 5],
|
||||
show_codex_windows_warning: bool,
|
||||
in_flight_prompt: Option<Vec<acp::ContentBlock>>,
|
||||
}
|
||||
|
||||
enum ThreadState {
|
||||
@@ -437,6 +438,7 @@ impl AcpThreadView {
|
||||
new_server_version_available: None,
|
||||
resume_thread_metadata: resume_thread,
|
||||
show_codex_windows_warning,
|
||||
in_flight_prompt: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,7 +653,6 @@ impl AcpThreadView {
|
||||
mode_selector,
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
this.message_editor.focus_handle(cx).focus(window);
|
||||
|
||||
this.profile_selector = this.as_native_thread(cx).map(|thread| {
|
||||
cx.new(|cx| {
|
||||
@@ -1155,6 +1156,7 @@ impl AcpThreadView {
|
||||
}
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.in_flight_prompt = Some(contents.clone());
|
||||
this.set_editor_is_expanded(false, cx);
|
||||
this.scroll_to_bottom(cx);
|
||||
this.message_editor.update(cx, |message_editor, cx| {
|
||||
@@ -1182,7 +1184,12 @@ impl AcpThreadView {
|
||||
})?;
|
||||
let res = send.await;
|
||||
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!(
|
||||
"Agent Turn Completed",
|
||||
agent = agent_telemetry_id,
|
||||
@@ -1265,6 +1272,28 @@ impl AcpThreadView {
|
||||
};
|
||||
|
||||
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
|
||||
.update(cx, |thread, cx| thread.rewind(user_message_id, cx))?
|
||||
.await?;
|
||||
@@ -4767,7 +4796,7 @@ impl AcpThreadView {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_text(markdown, 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| {
|
||||
@@ -5694,6 +5723,11 @@ impl AcpThreadView {
|
||||
provider_id: None,
|
||||
};
|
||||
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();
|
||||
window.defer(cx, |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 editor::MultiBufferOffset;
|
||||
use fs::FakeFs;
|
||||
use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext};
|
||||
use gpui::{EventEmitter, TestAppContext, VisualTestContext};
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
@@ -6590,7 +6624,7 @@ pub(crate) mod tests {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
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)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -253,6 +253,7 @@ impl ManageProfilesModal {
|
||||
});
|
||||
},
|
||||
false, // Do not use popover styles for the model picker
|
||||
self.focus_handle.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -25,6 +25,8 @@ impl AgentModelSelector {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle_clone = focus_handle.clone();
|
||||
|
||||
Self {
|
||||
selector: cx.new(move |cx| {
|
||||
let fs = fs.clone();
|
||||
@@ -48,6 +50,7 @@ impl AgentModelSelector {
|
||||
}
|
||||
},
|
||||
true, // Use popover styles for picker
|
||||
focus_handle_clone,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ use settings::{
|
||||
|
||||
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
|
||||
|
||||
use crate::ManageProfiles;
|
||||
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
|
||||
@@ -34,7 +35,6 @@ use crate::{
|
||||
acp::{AcpThreadHistory, ThreadHistoryEvent},
|
||||
};
|
||||
use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary};
|
||||
use crate::{ManageProfiles, context_store::ContextStore};
|
||||
use agent_settings::AgentSettings;
|
||||
use ai_onboarding::AgentPanelOnboarding;
|
||||
use anyhow::{Result, anyhow};
|
||||
@@ -431,7 +431,6 @@ pub struct AgentPanel {
|
||||
text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
inline_assist_context_store: Entity<ContextStore>,
|
||||
configuration: Option<Entity<AgentConfiguration>>,
|
||||
configuration_subscription: Option<Subscription>,
|
||||
active_view: ActiveView,
|
||||
@@ -543,7 +542,6 @@ impl AgentPanel {
|
||||
let client = workspace.client().clone();
|
||||
let workspace = workspace.weak_handle();
|
||||
|
||||
let inline_assist_context_store = cx.new(|_cx| ContextStore::new(project.downgrade()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
|
||||
@@ -680,7 +678,6 @@ impl AgentPanel {
|
||||
configuration: None,
|
||||
configuration_subscription: None,
|
||||
context_server_registry,
|
||||
inline_assist_context_store,
|
||||
previous_view: None,
|
||||
new_thread_menu_handle: PopoverMenuHandle::default(),
|
||||
agent_panel_menu_handle: PopoverMenuHandle::default(),
|
||||
@@ -721,10 +718,6 @@ impl AgentPanel {
|
||||
&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> {
|
||||
&self.history_store
|
||||
}
|
||||
@@ -823,6 +816,7 @@ impl AgentPanel {
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
true,
|
||||
window,
|
||||
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);
|
||||
@@ -960,10 +959,10 @@ impl AgentPanel {
|
||||
fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if matches!(self.active_view, ActiveView::History) {
|
||||
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 {
|
||||
self.set_active_view(ActiveView::History, window, cx);
|
||||
self.set_active_view(ActiveView::History, true, window, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1019,6 +1018,7 @@ impl AgentPanel {
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -1164,7 +1164,7 @@ impl AgentPanel {
|
||||
let context_server_store = self.project.read(cx).context_server_store();
|
||||
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| {
|
||||
AgentConfiguration::new(
|
||||
fs,
|
||||
@@ -1281,6 +1281,7 @@ impl AgentPanel {
|
||||
fn set_active_view(
|
||||
&mut self,
|
||||
new_view: ActiveView,
|
||||
focus: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -1319,7 +1320,9 @@ impl AgentPanel {
|
||||
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(
|
||||
@@ -2664,23 +2667,19 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
|
||||
cx: &mut Context<RulesLibrary>,
|
||||
) {
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
let Some(project) = self
|
||||
.workspace
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).project().downgrade())
|
||||
else {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let prompt_store = None;
|
||||
let thread_store = None;
|
||||
let context_store = cx.new(|_| ContextStore::new(project.clone()));
|
||||
let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
let project = workspace.read(cx).project().downgrade();
|
||||
assistant.assist(
|
||||
prompt_editor,
|
||||
self.workspace.clone(),
|
||||
context_store,
|
||||
project,
|
||||
prompt_store,
|
||||
thread_store,
|
||||
panel.read(cx).thread_store().clone(),
|
||||
None,
|
||||
initial_prompt,
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -4,14 +4,13 @@ mod agent_diff;
|
||||
mod agent_model_selector;
|
||||
mod agent_panel;
|
||||
mod buffer_codegen;
|
||||
mod completion_provider;
|
||||
mod context;
|
||||
mod context_picker;
|
||||
mod context_server_configuration;
|
||||
mod context_store;
|
||||
mod context_strip;
|
||||
mod inline_assistant;
|
||||
mod inline_prompt_editor;
|
||||
mod language_model_selector;
|
||||
mod mention_set;
|
||||
mod profile_selector;
|
||||
mod slash_command;
|
||||
mod slash_command_picker;
|
||||
@@ -35,7 +34,7 @@ use language::{
|
||||
language_settings::{AllLanguageSettings, EditPredictionProvider},
|
||||
};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
||||
ConfiguredModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
||||
};
|
||||
use project::DisableAiSettings;
|
||||
use prompt_store::PromptBuilder;
|
||||
@@ -56,8 +55,6 @@ actions!(
|
||||
[
|
||||
/// Creates a new text-based conversation thread.
|
||||
NewTextThread,
|
||||
/// Toggles the context picker interface for adding files, symbols, or other context.
|
||||
ToggleContextPicker,
|
||||
/// Toggles the menu to create new agent threads.
|
||||
ToggleNewThreadMenu,
|
||||
/// Toggles the navigation menu for switching between threads and views.
|
||||
@@ -70,8 +67,6 @@ actions!(
|
||||
ToggleProfileSelector,
|
||||
/// Cycles through available session modes.
|
||||
CycleModeSelector,
|
||||
/// Removes all added context from the current conversation.
|
||||
RemoveAllContext,
|
||||
/// Expands the message editor to full size.
|
||||
ExpandMessageEditor,
|
||||
/// Opens the conversation history view.
|
||||
@@ -94,10 +89,6 @@ actions!(
|
||||
FocusLeft,
|
||||
/// Moves focus right in the interface.
|
||||
FocusRight,
|
||||
/// Removes the currently focused context item.
|
||||
RemoveFocusedContext,
|
||||
/// Accepts the suggested context item.
|
||||
AcceptSuggestedContext,
|
||||
/// Opens the active thread as a markdown file.
|
||||
OpenActiveThreadAsMarkdown,
|
||||
/// 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.
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use crate::{
|
||||
context::load_context, context_store::ContextStore, inline_prompt_editor::CodegenStatus,
|
||||
};
|
||||
use crate::{context::LoadedContext, inline_prompt_editor::CodegenStatus};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
@@ -8,9 +6,12 @@ use cloud_llm_client::CompletionIntent;
|
||||
use collections::HashSet;
|
||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||
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_model::{
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
@@ -18,8 +19,7 @@ use language_model::{
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::Project;
|
||||
use prompt_store::{PromptBuilder, PromptStore};
|
||||
use prompt_store::PromptBuilder;
|
||||
use rope::Rope;
|
||||
use smol::future::FutureExt;
|
||||
use std::{
|
||||
@@ -43,9 +43,6 @@ pub struct BufferCodegen {
|
||||
buffer: Entity<MultiBuffer>,
|
||||
range: Range<Anchor>,
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
context_store: Entity<ContextStore>,
|
||||
project: WeakEntity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
builder: Arc<PromptBuilder>,
|
||||
pub is_insertion: bool,
|
||||
@@ -56,9 +53,6 @@ impl BufferCodegen {
|
||||
buffer: Entity<MultiBuffer>,
|
||||
range: Range<Anchor>,
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
context_store: Entity<ContextStore>,
|
||||
project: WeakEntity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
builder: Arc<PromptBuilder>,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -68,9 +62,6 @@ impl BufferCodegen {
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
false,
|
||||
Some(context_store.clone()),
|
||||
project.clone(),
|
||||
prompt_store.clone(),
|
||||
Some(telemetry.clone()),
|
||||
builder.clone(),
|
||||
cx,
|
||||
@@ -85,9 +76,6 @@ impl BufferCodegen {
|
||||
buffer,
|
||||
range,
|
||||
initial_transaction_id,
|
||||
context_store,
|
||||
project,
|
||||
prompt_store,
|
||||
telemetry,
|
||||
builder,
|
||||
};
|
||||
@@ -148,6 +136,7 @@ impl BufferCodegen {
|
||||
&mut self,
|
||||
primary_model: Arc<dyn LanguageModel>,
|
||||
user_prompt: String,
|
||||
context_task: Shared<Task<Option<LoadedContext>>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
let alternative_models = LanguageModelRegistry::read_global(cx)
|
||||
@@ -165,9 +154,6 @@ impl BufferCodegen {
|
||||
self.buffer.clone(),
|
||||
self.range.clone(),
|
||||
false,
|
||||
Some(self.context_store.clone()),
|
||||
self.project.clone(),
|
||||
self.prompt_store.clone(),
|
||||
Some(self.telemetry.clone()),
|
||||
self.builder.clone(),
|
||||
cx,
|
||||
@@ -180,7 +166,7 @@ impl BufferCodegen {
|
||||
.zip(&self.alternatives)
|
||||
{
|
||||
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,
|
||||
generation: Task<()>,
|
||||
diff: Diff,
|
||||
context_store: Option<Entity<ContextStore>>,
|
||||
project: WeakEntity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
_subscription: gpui::Subscription,
|
||||
builder: Arc<PromptBuilder>,
|
||||
@@ -264,9 +247,6 @@ impl CodegenAlternative {
|
||||
buffer: Entity<MultiBuffer>,
|
||||
range: Range<Anchor>,
|
||||
active: bool,
|
||||
context_store: Option<Entity<ContextStore>>,
|
||||
project: WeakEntity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
builder: Arc<PromptBuilder>,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -307,9 +287,6 @@ impl CodegenAlternative {
|
||||
status: CodegenStatus::Idle,
|
||||
generation: Task::ready(()),
|
||||
diff: Diff::default(),
|
||||
context_store,
|
||||
project,
|
||||
prompt_store,
|
||||
telemetry,
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
builder,
|
||||
@@ -366,6 +343,7 @@ impl CodegenAlternative {
|
||||
pub fn start(
|
||||
&mut self,
|
||||
user_prompt: String,
|
||||
context_task: Shared<Task<Option<LoadedContext>>>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
@@ -384,7 +362,7 @@ impl CodegenAlternative {
|
||||
if user_prompt.trim().to_lowercase() == "delete" {
|
||||
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
||||
} 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| {
|
||||
Ok(model.stream_completion_text(request.await, cx).await?)
|
||||
})
|
||||
@@ -398,6 +376,7 @@ impl CodegenAlternative {
|
||||
&self,
|
||||
model: &Arc<dyn LanguageModel>,
|
||||
user_prompt: String,
|
||||
context_task: Shared<Task<Option<LoadedContext>>>,
|
||||
cx: &mut App,
|
||||
) -> Result<Task<LanguageModelRequest>> {
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
@@ -437,19 +416,6 @@ impl CodegenAlternative {
|
||||
)
|
||||
.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);
|
||||
|
||||
Ok(cx.spawn(async move |_cx| {
|
||||
@@ -457,12 +423,11 @@ impl CodegenAlternative {
|
||||
role: Role::User,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
};
|
||||
|
||||
if let Some(context_task) = context_task {
|
||||
context_task
|
||||
.await
|
||||
.add_to_request_message(&mut request_message);
|
||||
if let Some(context) = context_task.await {
|
||||
context.add_to_request_message(&mut request_message);
|
||||
}
|
||||
|
||||
request_message.content.push(prompt.into());
|
||||
@@ -1088,7 +1053,6 @@ impl Diff {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fs::FakeFs;
|
||||
use futures::{
|
||||
Stream,
|
||||
stream::{self},
|
||||
@@ -1120,17 +1084,12 @@ mod tests {
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||
});
|
||||
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| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
None,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
)
|
||||
@@ -1187,17 +1146,12 @@ mod tests {
|
||||
snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
|
||||
});
|
||||
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| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
None,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
)
|
||||
@@ -1256,17 +1210,12 @@ mod tests {
|
||||
snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
|
||||
});
|
||||
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| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
None,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
)
|
||||
@@ -1325,17 +1274,12 @@ mod tests {
|
||||
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2))
|
||||
});
|
||||
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| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
None,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
)
|
||||
@@ -1382,17 +1326,12 @@ mod tests {
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 14))
|
||||
});
|
||||
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| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
false,
|
||||
None,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
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::sync::Arc;
|
||||
|
||||
use crate::context::load_context;
|
||||
use crate::mention_set::MentionSet;
|
||||
use crate::{
|
||||
AgentPanel,
|
||||
buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent},
|
||||
context_store::ContextStore,
|
||||
inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent},
|
||||
terminal_inline_assistant::TerminalInlineAssistant,
|
||||
};
|
||||
@@ -31,6 +32,7 @@ use editor::{
|
||||
},
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
|
||||
WeakEntity, Window, point,
|
||||
@@ -214,16 +216,10 @@ impl InlineAssistant {
|
||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
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(
|
||||
Rc::new(AssistantCodeActionProvider {
|
||||
editor: cx.entity().downgrade(),
|
||||
workspace: workspace.downgrade(),
|
||||
thread_store,
|
||||
}),
|
||||
window,
|
||||
cx,
|
||||
@@ -235,9 +231,6 @@ impl InlineAssistant {
|
||||
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 {
|
||||
editor.remove_code_action_provider(
|
||||
ASSISTANT_CODE_ACTION_PROVIDER_ID.into(),
|
||||
@@ -279,8 +272,7 @@ impl InlineAssistant {
|
||||
let agent_panel = agent_panel.read(cx);
|
||||
|
||||
let prompt_store = agent_panel.prompt_store().as_ref().cloned();
|
||||
let thread_store = Some(agent_panel.thread_store().downgrade());
|
||||
let context_store = agent_panel.inline_assist_context_store().clone();
|
||||
let thread_store = agent_panel.thread_store().clone();
|
||||
|
||||
let handle_assist =
|
||||
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
|
||||
@@ -289,10 +281,9 @@ impl InlineAssistant {
|
||||
assistant.assist(
|
||||
&active_editor,
|
||||
cx.entity().downgrade(),
|
||||
context_store,
|
||||
workspace.project().downgrade(),
|
||||
prompt_store,
|
||||
thread_store,
|
||||
prompt_store,
|
||||
action.prompt.clone(),
|
||||
window,
|
||||
cx,
|
||||
@@ -305,8 +296,8 @@ impl InlineAssistant {
|
||||
&active_terminal,
|
||||
cx.entity().downgrade(),
|
||||
workspace.project().downgrade(),
|
||||
prompt_store,
|
||||
thread_store,
|
||||
prompt_store,
|
||||
action.prompt.clone(),
|
||||
window,
|
||||
cx,
|
||||
@@ -477,10 +468,9 @@ impl InlineAssistant {
|
||||
&mut self,
|
||||
editor: &Entity<Editor>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: Entity<ContextStore>,
|
||||
project: WeakEntity<Project>,
|
||||
thread_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
||||
initial_prompt: Option<String>,
|
||||
window: &mut Window,
|
||||
codegen_ranges: &[Range<Anchor>],
|
||||
@@ -508,9 +498,6 @@ impl InlineAssistant {
|
||||
editor.read(cx).buffer().clone(),
|
||||
range.clone(),
|
||||
initial_transaction_id,
|
||||
context_store.clone(),
|
||||
project.clone(),
|
||||
prompt_store.clone(),
|
||||
self.telemetry.clone(),
|
||||
self.prompt_builder.clone(),
|
||||
cx,
|
||||
@@ -526,10 +513,10 @@ impl InlineAssistant {
|
||||
prompt_buffer.clone(),
|
||||
codegen.clone(),
|
||||
self.fs.clone(),
|
||||
context_store.clone(),
|
||||
workspace.clone(),
|
||||
thread_store.clone(),
|
||||
prompt_store.as_ref().map(|s| s.downgrade()),
|
||||
prompt_store.clone(),
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -606,10 +593,9 @@ impl InlineAssistant {
|
||||
&mut self,
|
||||
editor: &Entity<Editor>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: Entity<ContextStore>,
|
||||
project: WeakEntity<Project>,
|
||||
thread_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
||||
initial_prompt: Option<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -625,10 +611,9 @@ impl InlineAssistant {
|
||||
let assist_to_focus = self.batch_assist(
|
||||
editor,
|
||||
workspace,
|
||||
context_store,
|
||||
project,
|
||||
prompt_store,
|
||||
thread_store,
|
||||
prompt_store,
|
||||
initial_prompt,
|
||||
window,
|
||||
&codegen_ranges,
|
||||
@@ -650,8 +635,8 @@ impl InlineAssistant {
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
focus: bool,
|
||||
workspace: Entity<Workspace>,
|
||||
thread_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> InlineAssistId {
|
||||
@@ -663,16 +648,14 @@ impl InlineAssistant {
|
||||
}
|
||||
|
||||
let project = workspace.read(cx).project().downgrade();
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project.clone()));
|
||||
|
||||
let assist_id = self
|
||||
.batch_assist(
|
||||
editor,
|
||||
workspace.downgrade(),
|
||||
context_store,
|
||||
project,
|
||||
prompt_store,
|
||||
thread_store,
|
||||
prompt_store,
|
||||
Some(initial_prompt),
|
||||
window,
|
||||
&[range],
|
||||
@@ -1294,7 +1277,8 @@ impl InlineAssistant {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1310,9 +1294,12 @@ impl InlineAssistant {
|
||||
return;
|
||||
};
|
||||
|
||||
let context_task = load_context(&mention_set, cx).shared();
|
||||
assist
|
||||
.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();
|
||||
}
|
||||
|
||||
@@ -1458,6 +1445,7 @@ impl InlineAssistant {
|
||||
multi_buffer.update(cx, |multi_buffer, cx| {
|
||||
multi_buffer.push_excerpts(
|
||||
old_buffer.clone(),
|
||||
// todo(lw): buffer_start and buffer_end might come from different snapshots!
|
||||
Some(ExcerptRange::new(buffer_start..buffer_end)),
|
||||
cx,
|
||||
);
|
||||
@@ -1778,6 +1766,11 @@ impl InlineAssist {
|
||||
let decorations = self.decorations.as_ref()?;
|
||||
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 {
|
||||
@@ -1790,10 +1783,9 @@ struct InlineAssistDecorations {
|
||||
struct AssistantCodeActionProvider {
|
||||
editor: WeakEntity<Editor>,
|
||||
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 {
|
||||
fn id(&self) -> Arc<str> {
|
||||
@@ -1861,10 +1853,20 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
) -> Task<Result<ProjectTransaction>> {
|
||||
let editor = self.editor.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
let prompt_store = PromptStore::global(cx);
|
||||
window.spawn(cx, async move |cx| {
|
||||
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 range = editor
|
||||
.update(cx, |editor, cx| {
|
||||
@@ -1907,8 +1909,8 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
None,
|
||||
true,
|
||||
workspace,
|
||||
prompt_store,
|
||||
thread_store,
|
||||
prompt_store,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
use agent::HistoryStore;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::actions::Paste;
|
||||
use editor::code_context_menus::CodeContextMenu;
|
||||
use editor::display_map::{CreaseId, EditorMargins};
|
||||
use editor::{Addon, AnchorRangeExt as _, MultiBufferOffset};
|
||||
use editor::{AnchorRangeExt as _, MultiBufferOffset, ToOffset as _};
|
||||
use editor::{
|
||||
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
||||
actions::{MoveDown, MoveUp},
|
||||
};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
AnyElement, App, ClipboardEntry, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, Subscription, TextStyle, WeakEntity, Window,
|
||||
AnyElement, App, Context, CursorStyle, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Subscription, TextStyle, WeakEntity, Window,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||
use parking_lot::Mutex;
|
||||
use project::Project;
|
||||
use prompt_store::PromptStore;
|
||||
use settings::Settings;
|
||||
use std::cmp;
|
||||
@@ -28,22 +30,21 @@ use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
use crate::agent_model_selector::AgentModelSelector;
|
||||
use crate::buffer_codegen::BufferCodegen;
|
||||
use crate::context::{AgentContextHandle, AgentContextKey};
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
||||
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::completion_provider::{
|
||||
PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextType,
|
||||
};
|
||||
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 editor: Entity<Editor>,
|
||||
mode: PromptEditorMode,
|
||||
context_store: Entity<ContextStore>,
|
||||
context_strip: Entity<ContextStrip>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
mention_set: Entity<MentionSet>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
model_selector: Entity<AgentModelSelector>,
|
||||
edited_since_done: bool,
|
||||
prompt_history: VecDeque<String>,
|
||||
@@ -51,7 +52,6 @@ pub struct PromptEditor<T> {
|
||||
pending_prompt: String,
|
||||
_codegen_subscription: Subscription,
|
||||
editor_subscriptions: Vec<Subscription>,
|
||||
_context_strip_subscription: Subscription,
|
||||
show_rate_limit_notice: bool,
|
||||
_phantom: std::marker::PhantomData<T>,
|
||||
}
|
||||
@@ -98,6 +98,19 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
|
||||
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()
|
||||
.key_context("PromptEditor")
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
@@ -114,7 +127,6 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
h_flex()
|
||||
.items_start()
|
||||
.cursor(CursorStyle::Arrow)
|
||||
.on_action(cx.listener(Self::toggle_context_picker))
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
.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::move_up))
|
||||
.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_next))
|
||||
.child(
|
||||
@@ -182,7 +193,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
.pl_1()
|
||||
.items_start()
|
||||
.justify_between()
|
||||
.child(self.context_strip.clone())
|
||||
.child(add_context_button)
|
||||
.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(
|
||||
&mut self,
|
||||
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>) {
|
||||
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 mut creases = vec![];
|
||||
self.editor = cx.new(|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_placeholder_text("Add a prompt…", window, cx);
|
||||
editor.set_text(prompt, window, cx);
|
||||
insert_message_creases(
|
||||
&mut editor,
|
||||
&existing_creases,
|
||||
&self.context_store,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
creases = insert_message_creases(&mut editor, &existing_creases, window, cx);
|
||||
|
||||
if focus {
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -274,43 +311,29 @@ impl<T: 'static> PromptEditor<T> {
|
||||
self.editor.read(cx).text(cx)
|
||||
}
|
||||
|
||||
fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let images = cx
|
||||
.read_from_clipboard()
|
||||
.map(|item| {
|
||||
item.into_entries()
|
||||
.filter_map(|entry| {
|
||||
if let ClipboardEntry::Image(image) = entry {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if images.is_empty() {
|
||||
return;
|
||||
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if inline_assistant_model_supports_images(cx)
|
||||
&& let Some(task) =
|
||||
paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
|
||||
{
|
||||
task.detach();
|
||||
}
|
||||
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(
|
||||
&mut self,
|
||||
_: &Entity<Editor>,
|
||||
editor: &Entity<Editor>,
|
||||
event: &EditorEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
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() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
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);
|
||||
});
|
||||
}
|
||||
let prompt = self.editor.read(cx).text(cx);
|
||||
let prompt = snapshot.text();
|
||||
if self
|
||||
.prompt_history_ix
|
||||
.is_none_or(|ix| self.prompt_history[ix] != prompt)
|
||||
@@ -343,23 +366,44 @@ impl<T: 'static> PromptEditor<T> {
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_context_picker(
|
||||
&mut self,
|
||||
_: &ToggleContextPicker,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.context_picker_menu_handle.toggle(window, cx);
|
||||
pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
|
||||
self.editor
|
||||
.read(cx)
|
||||
.context_menu()
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
|
||||
}
|
||||
|
||||
pub fn remove_all_context(
|
||||
&mut self,
|
||||
_: &RemoveAllContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.context_store.update(cx, |store, cx| store.clear(cx));
|
||||
cx.notify();
|
||||
pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let menu_is_open = editor.context_menu().borrow().as_ref().is_some_and(|menu| {
|
||||
matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
|
||||
});
|
||||
|
||||
let has_at_sign = {
|
||||
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(
|
||||
@@ -434,8 +478,6 @@ impl<T: 'static> PromptEditor<T> {
|
||||
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 {
|
||||
background: colors.editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -716,21 +759,6 @@ impl<T: 'static> PromptEditor<T> {
|
||||
})
|
||||
.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 {
|
||||
@@ -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> {
|
||||
pub fn new_buffer(
|
||||
id: InlineAssistId,
|
||||
@@ -773,15 +831,14 @@ impl PromptEditor<BufferCodegen> {
|
||||
prompt_buffer: Entity<MultiBuffer>,
|
||||
codegen: Entity<BufferCodegen>,
|
||||
fs: Arc<dyn Fs>,
|
||||
context_store: Entity<ContextStore>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
project: WeakEntity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
||||
prompt_store: Option<WeakEntity<PromptStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<PromptEditor<BufferCodegen>>,
|
||||
) -> PromptEditor<BufferCodegen> {
|
||||
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 {
|
||||
id,
|
||||
codegen,
|
||||
@@ -805,7 +862,6 @@ impl PromptEditor<BufferCodegen> {
|
||||
// typing in one will make what you typed appear in all of them.
|
||||
editor.set_show_cursor_when_unfocused(true, cx);
|
||||
editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
|
||||
editor.register_addon(ContextCreasesAddon::new());
|
||||
editor.set_context_menu_options(ContextMenuOptions {
|
||||
min_entries_visible: 12,
|
||||
max_entries_visible: 12,
|
||||
@@ -815,43 +871,17 @@ impl PromptEditor<BufferCodegen> {
|
||||
editor
|
||||
});
|
||||
|
||||
let prompt_editor_entity = prompt_editor.downgrade();
|
||||
prompt_editor.update(cx, |editor, _| {
|
||||
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 mention_set =
|
||||
cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
|
||||
|
||||
let context_picker_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 {
|
||||
editor: prompt_editor.clone(),
|
||||
context_store,
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
mention_set,
|
||||
history_store,
|
||||
prompt_store,
|
||||
workspace,
|
||||
model_selector: cx.new(|cx| {
|
||||
AgentModelSelector::new(
|
||||
fs,
|
||||
@@ -868,12 +898,12 @@ impl PromptEditor<BufferCodegen> {
|
||||
pending_prompt: String::new(),
|
||||
_codegen_subscription: codegen_subscription,
|
||||
editor_subscriptions: Vec::new(),
|
||||
_context_strip_subscription: context_strip_subscription,
|
||||
show_rate_limit_notice: false,
|
||||
mode,
|
||||
_phantom: Default::default(),
|
||||
};
|
||||
|
||||
this.assign_completion_provider(cx);
|
||||
this.subscribe_to_editor(window, cx);
|
||||
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>> {
|
||||
match &self.mode {
|
||||
PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
|
||||
@@ -945,10 +979,10 @@ impl PromptEditor<TerminalCodegen> {
|
||||
prompt_buffer: Entity<MultiBuffer>,
|
||||
codegen: Entity<TerminalCodegen>,
|
||||
fs: Arc<dyn Fs>,
|
||||
context_store: Entity<ContextStore>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
project: WeakEntity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
||||
prompt_store: Option<WeakEntity<PromptStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -980,43 +1014,17 @@ impl PromptEditor<TerminalCodegen> {
|
||||
editor
|
||||
});
|
||||
|
||||
let prompt_editor_entity = prompt_editor.downgrade();
|
||||
prompt_editor.update(cx, |editor, _| {
|
||||
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 mention_set =
|
||||
cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
|
||||
|
||||
let context_picker_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 {
|
||||
editor: prompt_editor.clone(),
|
||||
context_store,
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
mention_set,
|
||||
history_store,
|
||||
prompt_store,
|
||||
workspace,
|
||||
model_selector: cx.new(|cx| {
|
||||
AgentModelSelector::new(
|
||||
fs,
|
||||
@@ -1033,12 +1041,12 @@ impl PromptEditor<TerminalCodegen> {
|
||||
pending_prompt: String::new(),
|
||||
_codegen_subscription: codegen_subscription,
|
||||
editor_subscriptions: Vec::new(),
|
||||
_context_strip_subscription: context_strip_subscription,
|
||||
mode,
|
||||
show_rate_limit_notice: false,
|
||||
_phantom: Default::default(),
|
||||
};
|
||||
this.count_lines(cx);
|
||||
this.assign_completion_provider(cx);
|
||||
this.subscribe_to_editor(window, cx);
|
||||
this
|
||||
}
|
||||
@@ -1085,6 +1093,10 @@ impl PromptEditor<TerminalCodegen> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mention_set(&self) -> &Entity<MentionSet> {
|
||||
&self.mention_set
|
||||
}
|
||||
|
||||
pub fn codegen(&self) -> &Entity<TerminalCodegen> {
|
||||
match &self.mode {
|
||||
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.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MessageCrease {
|
||||
pub range: Range<MultiBufferOffset>,
|
||||
pub icon_path: SharedString,
|
||||
pub label: SharedString,
|
||||
/// None for a deserialized message, Some otherwise.
|
||||
pub context: Option<AgentContextHandle>,
|
||||
struct MessageCrease {
|
||||
range: Range<MultiBufferOffset>,
|
||||
icon_path: SharedString,
|
||||
label: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
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(
|
||||
fn extract_message_creases(
|
||||
editor: &mut Editor,
|
||||
cx: &mut Context<'_, Editor>,
|
||||
) -> 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>,
|
||||
mention_set: &Entity<MentionSet>,
|
||||
window: &mut Window,
|
||||
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 creases = message_creases
|
||||
.iter()
|
||||
@@ -1305,12 +1227,5 @@ pub fn insert_message_creases(
|
||||
.collect::<Vec<_>>();
|
||||
let ids = editor.insert_creases(creases.clone(), cx);
|
||||
editor.fold_creases(creases, false, window, cx);
|
||||
if let Some(addon) = editor.addon_mut::<ContextCreasesAddon>() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
ids
|
||||
}
|
||||
|
||||
@@ -2,14 +2,17 @@ use std::{cmp::Reverse, sync::Arc};
|
||||
|
||||
use collections::IndexMap;
|
||||
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::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
||||
LanguageModelRegistry,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
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 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,
|
||||
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<LanguageModelSelector>,
|
||||
) -> LanguageModelSelector {
|
||||
@@ -27,6 +31,7 @@ pub fn language_model_selector(
|
||||
get_active_model,
|
||||
on_model_changed,
|
||||
popover_styles,
|
||||
focus_handle,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -88,6 +93,7 @@ pub struct LanguageModelPickerDelegate {
|
||||
_authenticate_all_providers_task: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl LanguageModelPickerDelegate {
|
||||
@@ -95,6 +101,7 @@ impl LanguageModelPickerDelegate {
|
||||
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
||||
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Self {
|
||||
@@ -128,6 +135,7 @@ impl LanguageModelPickerDelegate {
|
||||
},
|
||||
)],
|
||||
popover_styles,
|
||||
focus_handle,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,6 +529,8 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<gpui::AnyElement> {
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
if !self.popover_styles {
|
||||
return None;
|
||||
}
|
||||
@@ -528,22 +538,19 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
Some(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.p_1p5()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.p_1()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.child(
|
||||
Button::new("configure", "Configure")
|
||||
.icon(IconName::Settings)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.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(
|
||||
zed_actions::agent::OpenSettings.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
window.dispatch_action(OpenSettings.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.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::{
|
||||
context::load_context,
|
||||
context_store::ContextStore,
|
||||
inline_prompt_editor::{
|
||||
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
|
||||
},
|
||||
@@ -73,8 +72,8 @@ impl TerminalInlineAssistant {
|
||||
terminal_view: &Entity<TerminalView>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
thread_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
||||
initial_prompt: Option<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -87,7 +86,6 @@ impl TerminalInlineAssistant {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project));
|
||||
let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
|
||||
|
||||
let prompt_editor = cx.new(|cx| {
|
||||
@@ -97,10 +95,10 @@ impl TerminalInlineAssistant {
|
||||
prompt_buffer.clone(),
|
||||
codegen,
|
||||
self.fs.clone(),
|
||||
context_store.clone(),
|
||||
workspace.clone(),
|
||||
thread_store.clone(),
|
||||
prompt_store.as_ref().map(|s| s.downgrade()),
|
||||
prompt_store.clone(),
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -119,8 +117,6 @@ impl TerminalInlineAssistant {
|
||||
terminal_view,
|
||||
prompt_editor,
|
||||
workspace.clone(),
|
||||
context_store,
|
||||
prompt_store,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -227,6 +223,10 @@ impl TerminalInlineAssistant {
|
||||
assist_id: TerminalInlineAssistId,
|
||||
cx: &mut App,
|
||||
) -> 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 shell = std::env::var("SHELL").ok();
|
||||
@@ -243,45 +243,31 @@ impl TerminalInlineAssistant {
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
let prompt_editor = assist.prompt_editor.clone().context("invalid assist")?;
|
||||
|
||||
let prompt = self.prompt_builder.generate_terminal_assistant_prompt(
|
||||
&assist
|
||||
.prompt_editor
|
||||
.clone()
|
||||
.context("invalid assist")?
|
||||
.read(cx)
|
||||
.prompt(cx),
|
||||
&prompt_editor.read(cx).prompt(cx),
|
||||
shell.as_deref(),
|
||||
working_directory.as_deref(),
|
||||
&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 mention_set = prompt_editor.read(cx).mention_set().clone();
|
||||
let load_context_task = load_context(&mention_set, cx);
|
||||
|
||||
Ok(cx.background_spawn(async move {
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![],
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
};
|
||||
|
||||
context_load_task
|
||||
.await
|
||||
.add_to_request_message(&mut request_message);
|
||||
if let Some(context) = load_context_task.await {
|
||||
context.add_to_request_message(&mut request_message);
|
||||
}
|
||||
|
||||
request_message.content.push(prompt.into());
|
||||
|
||||
@@ -409,8 +395,6 @@ struct TerminalInlineAssist {
|
||||
prompt_editor: Option<Entity<PromptEditor<TerminalCodegen>>>,
|
||||
codegen: Entity<TerminalCodegen>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: Entity<ContextStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -420,8 +404,6 @@ impl TerminalInlineAssist {
|
||||
terminal: &Entity<TerminalView>,
|
||||
prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: Entity<ContextStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
@@ -431,8 +413,6 @@ impl TerminalInlineAssist {
|
||||
prompt_editor: Some(prompt_editor.clone()),
|
||||
codegen: codegen.clone(),
|
||||
workspace,
|
||||
context_store,
|
||||
prompt_store,
|
||||
_subscriptions: vec![
|
||||
window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
|
||||
TerminalInlineAssistant::update_global(cx, |this, cx| {
|
||||
|
||||
@@ -280,6 +280,8 @@ impl TextThreadEditor {
|
||||
.thought_process_output_sections()
|
||||
.to_vec();
|
||||
let slash_commands = text_thread.read(cx).slash_commands().clone();
|
||||
let focus_handle = editor.read(cx).focus_handle(cx);
|
||||
|
||||
let mut this = Self {
|
||||
text_thread,
|
||||
slash_commands,
|
||||
@@ -315,6 +317,7 @@ impl TextThreadEditor {
|
||||
});
|
||||
},
|
||||
true, // Use popover styles for picker
|
||||
focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@ mod acp_onboarding_modal;
|
||||
mod agent_notification;
|
||||
mod burn_mode_tooltip;
|
||||
mod claude_code_onboarding_modal;
|
||||
mod context_pill;
|
||||
mod end_trial_upsell;
|
||||
mod hold_for_default;
|
||||
mod onboarding_modal;
|
||||
@@ -13,7 +12,6 @@ pub use acp_onboarding_modal::*;
|
||||
pub use agent_notification::*;
|
||||
pub use burn_mode_tooltip::*;
|
||||
pub use claude_code_onboarding_modal::*;
|
||||
pub use context_pill::*;
|
||||
pub use end_trial_upsell::*;
|
||||
pub use hold_for_default::*;
|
||||
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"
|
||||
)]
|
||||
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")]
|
||||
ClaudeSonnet4,
|
||||
#[serde(
|
||||
@@ -131,6 +138,14 @@ impl Model {
|
||||
}
|
||||
|
||||
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") {
|
||||
return Ok(Self::ClaudeOpus4_1Thinking);
|
||||
}
|
||||
@@ -208,6 +223,8 @@ impl Model {
|
||||
Self::ClaudeOpus4_1 => "claude-opus-4-1-latest",
|
||||
Self::ClaudeOpus4Thinking => "claude-opus-4-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::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
|
||||
Self::ClaudeSonnet4_5 => "claude-sonnet-4-5-latest",
|
||||
@@ -230,6 +247,7 @@ impl Model {
|
||||
match self {
|
||||
Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
|
||||
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_5 | Self::ClaudeSonnet4_5Thinking => "claude-sonnet-4-5-20250929",
|
||||
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||
@@ -249,6 +267,8 @@ impl Model {
|
||||
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
|
||||
Self::ClaudeOpus4Thinking => "Claude Opus 4 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::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
|
||||
Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5",
|
||||
@@ -274,6 +294,8 @@ impl Model {
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_5Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeSonnet4_5
|
||||
@@ -303,6 +325,8 @@ impl Model {
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_5Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeSonnet4_5
|
||||
@@ -326,6 +350,8 @@ impl Model {
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_5Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeSonnet4_5
|
||||
@@ -348,6 +374,8 @@ impl Model {
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_5Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeSonnet4_5
|
||||
@@ -372,6 +400,7 @@ impl Model {
|
||||
match self {
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4_5
|
||||
| Self::Claude3_5Sonnet
|
||||
@@ -383,6 +412,7 @@ impl Model {
|
||||
| Self::Claude3Haiku => AnthropicModelMode::Default,
|
||||
Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeOpus4_5Thinking
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeSonnet4_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![];
|
||||
|
||||
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 {
|
||||
@@ -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(
|
||||
client: &dyn HttpClient,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
beta_headers: String,
|
||||
beta_headers: Option<String>,
|
||||
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
|
||||
stream_completion_with_rate_limit_info(client, api_url, api_key, request, beta_headers)
|
||||
.await
|
||||
@@ -578,7 +568,7 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
beta_headers: String,
|
||||
beta_headers: Option<String>,
|
||||
) -> Result<
|
||||
(
|
||||
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 request_builder = HttpRequest::builder()
|
||||
let mut 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");
|
||||
|
||||
if let Some(beta_headers) = beta_headers {
|
||||
request_builder = request_builder.header("Anthropic-Beta", beta_headers);
|
||||
}
|
||||
|
||||
let serialized_request =
|
||||
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
|
||||
let request = request_builder
|
||||
|
||||
@@ -250,7 +250,6 @@ impl PasswordProxy {
|
||||
.await
|
||||
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
|
||||
make_file_executable(&askpass_script_path).await?;
|
||||
// todo(shell): There might be no powershell on the system
|
||||
#[cfg(target_os = "windows")]
|
||||
let askpass_helper = format!(
|
||||
"powershell.exe -ExecutionPolicy Bypass -File {}",
|
||||
@@ -375,7 +374,7 @@ fn generate_askpass_script(
|
||||
Ok(format!(
|
||||
r#"
|
||||
$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
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.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 mut section_start = 0;
|
||||
for _ in 0..num_sections {
|
||||
let mut section_end = rng.random_range(section_start..=output_text.len());
|
||||
while !output_text.is_char_boundary(section_end) {
|
||||
section_end += 1;
|
||||
}
|
||||
let section_end = output_text.floor_char_boundary(
|
||||
rng.random_range(section_start..=output_text.len()),
|
||||
);
|
||||
events.push(Ok(SlashCommandEvent::StartSection {
|
||||
icon: IconName::Ai,
|
||||
label: "section".into(),
|
||||
|
||||
@@ -16,6 +16,7 @@ use gpui::{
|
||||
App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
|
||||
Task,
|
||||
};
|
||||
use itertools::Itertools as _;
|
||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
||||
@@ -1417,6 +1418,7 @@ impl TextThread {
|
||||
role: Role::User,
|
||||
content: vec!["Respond only with OK, nothing else.".into()],
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
});
|
||||
req
|
||||
};
|
||||
@@ -1852,14 +1854,17 @@ impl TextThread {
|
||||
}
|
||||
|
||||
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 buffer.contains_str_at(newline_offset, "\n")
|
||||
if let Some((prev_char, '\n')) =
|
||||
buffer.reversed_chars_at(insert_position).next_tuple()
|
||||
&& last_section_range.is_none_or(|last_section_range| {
|
||||
!last_section_range
|
||||
.to_offset(buffer)
|
||||
.contains(&newline_offset)
|
||||
.contains(&(insert_position - prev_char.len_utf8()))
|
||||
})
|
||||
{
|
||||
deletions.push((command_range_end..command_range_end + 1, ""));
|
||||
@@ -2085,6 +2090,11 @@ impl TextThread {
|
||||
);
|
||||
}
|
||||
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) => {
|
||||
stop_reason = reason;
|
||||
}
|
||||
@@ -2308,6 +2318,7 @@ impl TextThread {
|
||||
role: message.role,
|
||||
content: Vec::new(),
|
||||
cache: message.cache.as_ref().is_some_and(|cache| cache.is_anchor),
|
||||
reasoning_details: None,
|
||||
};
|
||||
|
||||
while let Some(content) = contents.peek() {
|
||||
@@ -2679,6 +2690,7 @@ impl TextThread {
|
||||
role: Role::User,
|
||||
content: vec![SUMMARIZE_THREAD_PROMPT.into()],
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
});
|
||||
|
||||
// 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
|
||||
paths.workspace = true
|
||||
release_channel.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
|
||||
@@ -2,12 +2,13 @@ use anyhow::{Context as _, Result};
|
||||
use client::Client;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, Global, SemanticVersion,
|
||||
Task, Window, actions,
|
||||
App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, Global, Task, Window,
|
||||
actions,
|
||||
};
|
||||
use http_client::{HttpClient, HttpClientWithUrl};
|
||||
use paths::remote_servers_dir;
|
||||
use release_channel::{AppCommitSha, ReleaseChannel};
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{RegisterSetting, Settings, SettingsStore};
|
||||
use smol::fs::File;
|
||||
@@ -44,7 +45,7 @@ actions!(
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum VersionCheckType {
|
||||
Sha(AppCommitSha),
|
||||
Semantic(SemanticVersion),
|
||||
Semantic(Version),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
@@ -100,7 +101,7 @@ impl AutoUpdateStatus {
|
||||
|
||||
pub struct AutoUpdater {
|
||||
status: AutoUpdateStatus,
|
||||
current_version: SemanticVersion,
|
||||
current_version: Version,
|
||||
client: Arc<Client>,
|
||||
pending_poll: Option<Task<Option<()>>>,
|
||||
quit_subscription: Option<gpui::Subscription>,
|
||||
@@ -256,7 +257,7 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut App) -> Option<()> {
|
||||
match release_channel {
|
||||
ReleaseChannel::Stable | ReleaseChannel::Preview => {
|
||||
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 path = format!("/releases/{release_channel}/{current_version}");
|
||||
let url = &auto_updater.client.http_client().build_url(&path);
|
||||
@@ -322,7 +323,7 @@ impl AutoUpdater {
|
||||
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
|
||||
// running, so we must wait to overwrite the application until quitting
|
||||
// or restarting. When quitting the app, we spawn the auto update helper
|
||||
@@ -400,8 +401,8 @@ impl AutoUpdater {
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn current_version(&self) -> SemanticVersion {
|
||||
self.current_version
|
||||
pub fn current_version(&self) -> Version {
|
||||
self.current_version.clone()
|
||||
}
|
||||
|
||||
pub fn status(&self) -> AutoUpdateStatus {
|
||||
@@ -422,7 +423,7 @@ impl AutoUpdater {
|
||||
// Ok(None).
|
||||
pub async fn download_remote_server_release(
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<SemanticVersion>,
|
||||
version: Option<Version>,
|
||||
os: &str,
|
||||
arch: &str,
|
||||
set_status: impl Fn(&str, &mut AsyncApp) + Send + 'static,
|
||||
@@ -469,7 +470,7 @@ impl AutoUpdater {
|
||||
|
||||
pub async fn get_remote_server_release_url(
|
||||
channel: ReleaseChannel,
|
||||
version: Option<SemanticVersion>,
|
||||
version: Option<Version>,
|
||||
os: &str,
|
||||
arch: &str,
|
||||
cx: &mut AsyncApp,
|
||||
@@ -491,7 +492,7 @@ impl AutoUpdater {
|
||||
async fn get_release_asset(
|
||||
this: &Entity<Self>,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<SemanticVersion>,
|
||||
version: Option<Version>,
|
||||
asset: &str,
|
||||
os: &str,
|
||||
arch: &str,
|
||||
@@ -554,7 +555,7 @@ impl AutoUpdater {
|
||||
this.read_with(cx, |this, cx| {
|
||||
(
|
||||
this.client.http_client(),
|
||||
this.current_version,
|
||||
this.current_version.clone(),
|
||||
this.status.clone(),
|
||||
ReleaseChannel::try_global(cx).unwrap_or(ReleaseChannel::Stable),
|
||||
)
|
||||
@@ -627,16 +628,19 @@ impl AutoUpdater {
|
||||
fn check_if_fetched_version_is_newer(
|
||||
release_channel: ReleaseChannel,
|
||||
app_commit_sha: Result<Option<String>>,
|
||||
installed_version: SemanticVersion,
|
||||
installed_version: Version,
|
||||
fetched_version: String,
|
||||
status: AutoUpdateStatus,
|
||||
) -> 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 {
|
||||
match 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
|
||||
.then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
|
||||
return Ok(newer_version);
|
||||
@@ -655,7 +659,12 @@ impl AutoUpdater {
|
||||
let should_download = app_commit_sha
|
||||
.ok()
|
||||
.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);
|
||||
let newer_version = should_download
|
||||
.then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
|
||||
@@ -708,8 +717,8 @@ impl AutoUpdater {
|
||||
}
|
||||
|
||||
fn check_if_fetched_version_is_newer_non_nightly(
|
||||
installed_version: SemanticVersion,
|
||||
fetched_version: SemanticVersion,
|
||||
installed_version: Version,
|
||||
fetched_version: Version,
|
||||
) -> Result<Option<VersionCheckType>> {
|
||||
let should_download = fetched_version > installed_version;
|
||||
let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
|
||||
@@ -1020,7 +1029,7 @@ mod tests {
|
||||
cx.update(|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);
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
@@ -1059,7 +1068,7 @@ mod tests {
|
||||
|
||||
auto_updater.read_with(cx, |updater, _| {
|
||||
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);
|
||||
@@ -1078,7 +1087,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
status,
|
||||
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!(
|
||||
status,
|
||||
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();
|
||||
@@ -1122,9 +1131,9 @@ mod tests {
|
||||
fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
|
||||
let release_channel = ReleaseChannel::Stable;
|
||||
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 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(
|
||||
release_channel,
|
||||
@@ -1141,9 +1150,9 @@ mod tests {
|
||||
fn test_stable_does_update_when_fetched_version_is_higher() {
|
||||
let release_channel = ReleaseChannel::Stable;
|
||||
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 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(
|
||||
release_channel,
|
||||
@@ -1163,11 +1172,11 @@ mod tests {
|
||||
fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
|
||||
let release_channel = ReleaseChannel::Stable;
|
||||
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 {
|
||||
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(
|
||||
release_channel,
|
||||
@@ -1184,11 +1193,11 @@ mod tests {
|
||||
fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
|
||||
let release_channel = ReleaseChannel::Stable;
|
||||
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 {
|
||||
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(
|
||||
release_channel,
|
||||
@@ -1208,9 +1217,10 @@ mod tests {
|
||||
fn test_nightly_does_not_update_when_fetched_sha_is_same() {
|
||||
let release_channel = ReleaseChannel::Nightly;
|
||||
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 fetched_sha = "a".to_string();
|
||||
let fetched_sha = "1.0.0+a".to_string();
|
||||
|
||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||
release_channel,
|
||||
@@ -1227,7 +1237,7 @@ mod tests {
|
||||
fn test_nightly_does_update_when_fetched_sha_is_not_same() {
|
||||
let release_channel = ReleaseChannel::Nightly;
|
||||
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 fetched_sha = "b".to_string();
|
||||
|
||||
@@ -1246,14 +1256,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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 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 {
|
||||
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(
|
||||
release_channel,
|
||||
@@ -1270,11 +1281,12 @@ mod tests {
|
||||
fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
|
||||
let release_channel = ReleaseChannel::Nightly;
|
||||
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 {
|
||||
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(
|
||||
release_channel,
|
||||
@@ -1294,7 +1306,7 @@ mod tests {
|
||||
fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
|
||||
let release_channel = ReleaseChannel::Nightly;
|
||||
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 fetched_sha = "a".to_string();
|
||||
|
||||
@@ -1317,11 +1329,11 @@ mod tests {
|
||||
{
|
||||
let release_channel = ReleaseChannel::Nightly;
|
||||
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 {
|
||||
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(
|
||||
release_channel,
|
||||
@@ -1339,7 +1351,7 @@ mod tests {
|
||||
{
|
||||
let release_channel = ReleaseChannel::Nightly;
|
||||
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 {
|
||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||
};
|
||||
|
||||
@@ -51,6 +51,13 @@ pub enum Model {
|
||||
alias = "claude-opus-4-1-thinking-latest"
|
||||
)]
|
||||
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")]
|
||||
Claude3_5SonnetV2,
|
||||
#[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> {
|
||||
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)
|
||||
} else if id.starts_with("claude-3-opus") {
|
||||
Ok(Self::Claude3Opus)
|
||||
@@ -178,6 +197,8 @@ impl Model {
|
||||
Model::ClaudeOpus4_1 => "claude-opus-4-1",
|
||||
Model::ClaudeOpus4Thinking => "claude-opus-4-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_5Sonnet => "claude-3-5-sonnet",
|
||||
Model::Claude3Opus => "claude-3-opus",
|
||||
@@ -245,6 +266,9 @@ impl Model {
|
||||
Model::ClaudeOpus4_1 | Model::ClaudeOpus4_1Thinking => {
|
||||
"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_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-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::ClaudeOpus4Thinking => "Claude Opus 4 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_5Sonnet => "Claude 3.5 Sonnet",
|
||||
Self::Claude3Opus => "Claude 3 Opus",
|
||||
@@ -379,7 +405,9 @@ impl Model {
|
||||
| Self::ClaudeSonnet4_5
|
||||
| Self::ClaudeSonnet4_5Thinking
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1Thinking => 200_000,
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_5Thinking => 200_000,
|
||||
Self::AmazonNovaPremier => 1_000_000,
|
||||
Self::PalmyraWriterX5 => 1_000_000,
|
||||
Self::PalmyraWriterX4 => 128_000,
|
||||
@@ -393,7 +421,11 @@ impl Model {
|
||||
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
|
||||
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_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::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1
|
||||
@@ -418,6 +450,8 @@ impl Model {
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_5Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeSonnet4_5
|
||||
@@ -443,6 +477,8 @@ impl Model {
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_5Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeSonnet4_5
|
||||
@@ -484,7 +520,9 @@ impl Model {
|
||||
| Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| 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
|
||||
Self::Custom {
|
||||
@@ -506,7 +544,9 @@ impl Model {
|
||||
| Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4_1Thinking => Some(BedrockModelCacheConfiguration {
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeOpus4_5Thinking => Some(BedrockModelCacheConfiguration {
|
||||
max_cache_anchors: 4,
|
||||
min_total_token: 1024,
|
||||
}),
|
||||
@@ -535,11 +575,11 @@ impl Model {
|
||||
budget_tokens: Some(4096),
|
||||
}
|
||||
}
|
||||
Model::ClaudeOpus4Thinking | Model::ClaudeOpus4_1Thinking => {
|
||||
BedrockModelMode::Thinking {
|
||||
budget_tokens: Some(4096),
|
||||
}
|
||||
}
|
||||
Model::ClaudeOpus4Thinking
|
||||
| Model::ClaudeOpus4_1Thinking
|
||||
| Model::ClaudeOpus4_5Thinking => BedrockModelMode::Thinking {
|
||||
budget_tokens: Some(4096),
|
||||
},
|
||||
_ => BedrockModelMode::Default,
|
||||
}
|
||||
}
|
||||
@@ -593,6 +633,8 @@ impl Model {
|
||||
| Model::ClaudeOpus4Thinking
|
||||
| Model::ClaudeOpus4_1
|
||||
| Model::ClaudeOpus4_1Thinking
|
||||
| Model::ClaudeOpus4_5
|
||||
| Model::ClaudeOpus4_5Thinking
|
||||
| Model::Claude3Haiku
|
||||
| Model::Claude3Opus
|
||||
| Model::Claude3Sonnet
|
||||
|
||||
@@ -147,6 +147,7 @@ impl std::fmt::Debug for BufferDiffInner {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("BufferDiffSnapshot")
|
||||
.field("hunks", &self.hunks)
|
||||
.field("remote_id", &self.base_text.remote_id())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ collections = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
semver.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
use client::{Client, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{App, AppContext as _, Entity, SemanticVersion};
|
||||
use gpui::{App, AppContext as _, Entity};
|
||||
use http_client::FakeHttpClient;
|
||||
use rpc::proto::{self};
|
||||
use settings::SettingsStore;
|
||||
@@ -236,7 +236,7 @@ fn test_dangling_channel_paths(cx: &mut App) {
|
||||
fn init_test(cx: &mut App) -> Entity<ChannelStore> {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
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 http = FakeHttpClient::with_404_response();
|
||||
|
||||
@@ -34,6 +34,10 @@ util.workspace = true
|
||||
tempfile.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]
|
||||
exec.workspace = true
|
||||
fork.workspace = true
|
||||
|
||||
@@ -23,4 +23,7 @@ fn main() {
|
||||
|
||||
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 parking_lot::Mutex;
|
||||
use std::{
|
||||
env, fs, io,
|
||||
env,
|
||||
ffi::OsStr,
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
process::ExitStatus,
|
||||
sync::Arc,
|
||||
@@ -129,37 +131,177 @@ struct Args {
|
||||
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> {
|
||||
let canonicalized = match Path::new(argument_str).canonicalize() {
|
||||
Ok(existing_path) => PathWithPosition::from_path(existing_path),
|
||||
Err(_) => {
|
||||
let path = PathWithPosition::parse_str(argument_str);
|
||||
match Path::new(argument_str).canonicalize() {
|
||||
Ok(existing_path) => Ok(PathWithPosition::from_path(existing_path)),
|
||||
Err(_) => PathWithPosition::parse_str(argument_str).map_path(|mut path| {
|
||||
let curdir = env::current_dir().context("retrieving current directory")?;
|
||||
path.map_path(|path| match fs::canonicalize(&path) {
|
||||
Ok(path) => Ok(path),
|
||||
Err(e) => {
|
||||
if let Some(mut parent) = path.parent() {
|
||||
if parent == Path::new("") {
|
||||
parent = &curdir
|
||||
}
|
||||
match fs::canonicalize(parent) {
|
||||
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
|
||||
Err(_) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
let mut children = Vec::new();
|
||||
let root;
|
||||
loop {
|
||||
// canonicalize handles './', and '/'.
|
||||
if let Ok(canonicalized) = fs::canonicalize(&path) {
|
||||
root = canonicalized;
|
||||
break;
|
||||
}
|
||||
})
|
||||
}
|
||||
.with_context(|| format!("parsing as path with position {argument_str}"))?,
|
||||
};
|
||||
Ok(canonicalized.to_string(|path| path.to_string_lossy().into_owned()))
|
||||
// The comparison to `curdir` is just a shortcut
|
||||
// since we know it is canonical. The other one
|
||||
// is if `argument_str` is a string that starts
|
||||
// with a name (e.g. "foo/bar").
|
||||
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> {
|
||||
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('@') {
|
||||
if user.is_empty() {
|
||||
@@ -170,22 +312,35 @@ fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
|
||||
(None, wsl)
|
||||
};
|
||||
|
||||
let mut args = vec!["--distribution", distro_name];
|
||||
if let Some(user) = user {
|
||||
command.arg("--user").arg(user);
|
||||
args.push("--user");
|
||||
args.push(user);
|
||||
}
|
||||
|
||||
let output = command
|
||||
.arg("--distribution")
|
||||
.arg(distro_name)
|
||||
.arg("--exec")
|
||||
.arg("wslpath")
|
||||
.arg("-m")
|
||||
.arg(&source.path)
|
||||
.output()?;
|
||||
let command = [
|
||||
OsStr::new("realpath"),
|
||||
OsStr::new("-s"),
|
||||
source.path.as_ref(),
|
||||
];
|
||||
|
||||
let result = String::from_utf8_lossy(&output.stdout);
|
||||
let prefix = format!("//wsl.localhost/{}", distro_name);
|
||||
source.path = Path::new(result.trim().strip_prefix(&prefix).unwrap_or(&result)).to_owned();
|
||||
let output = util::command::new_std_command("wsl.exe")
|
||||
.args(&args)
|
||||
.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()))
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ settings = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
semver.workspace = true
|
||||
windows.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
|
||||
@@ -158,7 +158,7 @@ pub fn os_version() -> String {
|
||||
let mut info = unsafe { std::mem::zeroed() };
|
||||
let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut info) };
|
||||
if status.is_ok() {
|
||||
gpui::SemanticVersion::new(
|
||||
semver::Version::new(
|
||||
info.dwMajorVersion as _,
|
||||
info.dwMinorVersion as _,
|
||||
info.dwBuildNumber as _,
|
||||
|
||||
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt::{Display, Write as _},
|
||||
ops::{Add, Range, Sub},
|
||||
path::{Path, PathBuf},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
use strum::EnumIter;
|
||||
@@ -17,7 +17,7 @@ pub struct PlanContextRetrievalRequest {
|
||||
pub excerpt_path: Arc<Path>,
|
||||
pub excerpt_line_range: Range<Line>,
|
||||
pub cursor_file_max_row: Line,
|
||||
pub events: Vec<Event>,
|
||||
pub events: Vec<Arc<Event>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -36,7 +36,7 @@ pub struct PredictEditsRequest {
|
||||
pub signatures: Vec<Signature>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
pub referenced_declarations: Vec<ReferencedDeclaration>,
|
||||
pub events: Vec<Event>,
|
||||
pub events: Vec<Arc<Event>>,
|
||||
#[serde(default)]
|
||||
pub can_collect_data: bool,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
@@ -80,6 +80,8 @@ pub enum PromptFormat {
|
||||
Minimal,
|
||||
/// One-sentence instructions + FIM-like template
|
||||
MinimalQwen,
|
||||
/// No instructions, Qwen chat + Seed-Coder 1120 FIM-like template
|
||||
SeedCoder1120,
|
||||
}
|
||||
|
||||
impl PromptFormat {
|
||||
@@ -108,6 +110,7 @@ impl std::fmt::Display for PromptFormat {
|
||||
PromptFormat::OldTextNewText => write!(f, "Old Text / New Text"),
|
||||
PromptFormat::Minimal => write!(f, "Minimal"),
|
||||
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")]
|
||||
pub enum Event {
|
||||
BufferChange {
|
||||
path: Option<PathBuf>,
|
||||
old_path: Option<PathBuf>,
|
||||
path: Arc<Path>,
|
||||
old_path: Arc<Path>,
|
||||
diff: String,
|
||||
predicted: bool,
|
||||
in_open_source_repo: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -132,23 +136,21 @@ impl Display for Event {
|
||||
old_path,
|
||||
diff,
|
||||
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 {
|
||||
write!(
|
||||
f,
|
||||
"// User accepted prediction:\n--- a/{}\n+++ b/{}\n{diff}",
|
||||
DiffPathFmt(old_path),
|
||||
DiffPathFmt(new_path)
|
||||
DiffPathFmt(path)
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"--- a/{}\n+++ b/{}\n{diff}",
|
||||
DiffPathFmt(old_path),
|
||||
DiffPathFmt(new_path)
|
||||
DiffPathFmt(path)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -297,10 +299,11 @@ mod tests {
|
||||
#[test]
|
||||
fn test_event_display() {
|
||||
let ev = Event::BufferChange {
|
||||
path: None,
|
||||
old_path: None,
|
||||
path: Path::new("untitled").into(),
|
||||
old_path: Path::new("untitled").into(),
|
||||
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
||||
predicted: false,
|
||||
in_open_source_repo: true,
|
||||
};
|
||||
assert_eq!(
|
||||
ev.to_string(),
|
||||
@@ -314,10 +317,11 @@ mod tests {
|
||||
);
|
||||
|
||||
let ev = Event::BufferChange {
|
||||
path: Some(PathBuf::from("foo/bar.txt")),
|
||||
old_path: Some(PathBuf::from("foo/bar.txt")),
|
||||
path: Path::new("foo/bar.txt").into(),
|
||||
old_path: Path::new("foo/bar.txt").into(),
|
||||
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
||||
predicted: false,
|
||||
in_open_source_repo: true,
|
||||
};
|
||||
assert_eq!(
|
||||
ev.to_string(),
|
||||
@@ -331,10 +335,11 @@ mod tests {
|
||||
);
|
||||
|
||||
let ev = Event::BufferChange {
|
||||
path: Some(PathBuf::from("abc.txt")),
|
||||
old_path: Some(PathBuf::from("123.txt")),
|
||||
path: Path::new("abc.txt").into(),
|
||||
old_path: Path::new("123.txt").into(),
|
||||
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
||||
predicted: false,
|
||||
in_open_source_repo: true,
|
||||
};
|
||||
assert_eq!(
|
||||
ev.to_string(),
|
||||
@@ -348,10 +353,11 @@ mod tests {
|
||||
);
|
||||
|
||||
let ev = Event::BufferChange {
|
||||
path: Some(PathBuf::from("abc.txt")),
|
||||
old_path: Some(PathBuf::from("123.txt")),
|
||||
path: Path::new("abc.txt").into(),
|
||||
old_path: Path::new("123.txt").into(),
|
||||
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
||||
predicted: true,
|
||||
in_open_source_repo: true,
|
||||
};
|
||||
assert_eq!(
|
||||
ev.to_string(),
|
||||
|
||||
@@ -169,15 +169,18 @@ pub fn build_prompt(
|
||||
) -> Result<(String, SectionLabels)> {
|
||||
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 {
|
||||
PromptFormat::MinimalQwen => {
|
||||
let prompt = MinimalQwenPrompt {
|
||||
events: request.events.clone(),
|
||||
cursor_point: request.cursor_point,
|
||||
cursor_path: request.excerpt_path.clone(),
|
||||
included_files: request.included_files.clone(),
|
||||
};
|
||||
return Ok((prompt.render(), section_labels));
|
||||
return Ok((MinimalQwenPrompt.render(&prompt_data), section_labels));
|
||||
}
|
||||
PromptFormat::SeedCoder1120 => {
|
||||
return Ok((SeedCoder1120Prompt.render(&prompt_data), section_labels));
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
@@ -208,6 +211,7 @@ pub fn build_prompt(
|
||||
}
|
||||
PromptFormat::OnlySnippets => vec![],
|
||||
PromptFormat::MinimalQwen => unreachable!(),
|
||||
PromptFormat::SeedCoder1120 => unreachable!(),
|
||||
};
|
||||
|
||||
let mut prompt = match request.prompt_format {
|
||||
@@ -218,6 +222,7 @@ pub fn build_prompt(
|
||||
PromptFormat::OnlySnippets => String::new(),
|
||||
PromptFormat::Minimal => STUDENT_MODEL_INSTRUCTIONS.to_string(),
|
||||
PromptFormat::MinimalQwen => unreachable!(),
|
||||
PromptFormat::SeedCoder1120 => unreachable!(),
|
||||
};
|
||||
|
||||
if request.events.is_empty() {
|
||||
@@ -328,6 +333,13 @@ pub fn build_prompt(
|
||||
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>(
|
||||
path: &Path,
|
||||
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() {
|
||||
return;
|
||||
};
|
||||
@@ -786,6 +798,7 @@ impl<'a> SyntaxBasedPrompt<'a> {
|
||||
}
|
||||
}
|
||||
PromptFormat::MinimalQwen => unreachable!(),
|
||||
PromptFormat::SeedCoder1120 => unreachable!(),
|
||||
}
|
||||
|
||||
let push_full_snippet = |output: &mut String| {
|
||||
@@ -896,19 +909,34 @@ fn declaration_size(declaration: &ReferencedDeclaration, style: DeclarationStyle
|
||||
}
|
||||
}
|
||||
|
||||
struct MinimalQwenPrompt {
|
||||
events: Vec<Event>,
|
||||
struct PromptData {
|
||||
events: Vec<Arc<Event>>,
|
||||
cursor_point: Point,
|
||||
cursor_path: Arc<Path>, // TODO: make a common struct with cursor_point
|
||||
included_files: Vec<IncludedFile>,
|
||||
}
|
||||
|
||||
impl MinimalQwenPrompt {
|
||||
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";
|
||||
#[derive(Default)]
|
||||
pub struct GenerationParams {
|
||||
pub temperature: Option<f32>,
|
||||
pub top_p: Option<f32>,
|
||||
pub stop: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
fn render(&self) -> String {
|
||||
let edit_history = self.fmt_edit_history();
|
||||
let context = self.fmt_context();
|
||||
trait PromptFormatter {
|
||||
fn render(&self, data: &PromptData) -> String;
|
||||
|
||||
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!(
|
||||
"{instructions}\n\n{edit_history}\n\n{context}",
|
||||
@@ -917,13 +945,17 @@ impl MinimalQwenPrompt {
|
||||
context = context
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_edit_history(&self) -> String {
|
||||
if self.events.is_empty() {
|
||||
impl MinimalQwenPrompt {
|
||||
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()
|
||||
} else {
|
||||
let mut events_str = String::new();
|
||||
push_events(&mut events_str, &self.events);
|
||||
push_events(&mut events_str, &data.events);
|
||||
format!(
|
||||
"The following are the latest edits made by the user, from earlier to later.\n\n{}",
|
||||
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 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();
|
||||
|
||||
if related_file.path == self.cursor_path {
|
||||
if related_file.path == data.cursor_path {
|
||||
write!(context, "<|fim_prefix|>").unwrap();
|
||||
write_excerpts(
|
||||
&related_file.excerpts,
|
||||
&[(self.cursor_point, "<|fim_suffix|>")],
|
||||
&[(data.cursor_point, "<|fim_suffix|>")],
|
||||
related_file.max_row,
|
||||
include_line_numbers,
|
||||
&mut context,
|
||||
@@ -961,3 +993,83 @@ impl MinimalQwenPrompt {
|
||||
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()
|
||||
}
|
||||
|
||||
fn is_refreshing(&self) -> bool {
|
||||
fn is_refreshing(&self, _cx: &App) -> bool {
|
||||
self.pending_request.is_some()
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ scrypt = "0.11"
|
||||
# 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-macros = "=1.1.10"
|
||||
semantic_version.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -11,7 +11,7 @@ use axum::{
|
||||
};
|
||||
use collections::{BTreeSet, HashMap};
|
||||
use rpc::{ExtensionApiManifest, ExtensionProvides, GetExtensionsResponse};
|
||||
use semantic_version::SemanticVersion;
|
||||
use semver::Version as SemanticVersion;
|
||||
use serde::Deserialize;
|
||||
use std::str::FromStr;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
@@ -108,8 +108,8 @@ struct GetExtensionUpdatesParams {
|
||||
ids: String,
|
||||
min_schema_version: i32,
|
||||
max_schema_version: i32,
|
||||
min_wasm_api_version: SemanticVersion,
|
||||
max_wasm_api_version: SemanticVersion,
|
||||
min_wasm_api_version: semver::Version,
|
||||
max_wasm_api_version: semver::Version,
|
||||
}
|
||||
|
||||
async fn get_extension_updates(
|
||||
|
||||
@@ -22,7 +22,7 @@ use sea_orm::{
|
||||
entity::prelude::*,
|
||||
sea_query::{Alias, Expr, OnConflict},
|
||||
};
|
||||
use semantic_version::SemanticVersion;
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::RangeInclusive;
|
||||
use std::{
|
||||
@@ -671,7 +671,7 @@ pub struct NewExtensionVersion {
|
||||
|
||||
pub struct ExtensionVersionConstraints {
|
||||
pub schema_versions: RangeInclusive<i32>,
|
||||
pub wasm_api_versions: RangeInclusive<SemanticVersion>,
|
||||
pub wasm_api_versions: RangeInclusive<semver::Version>,
|
||||
}
|
||||
|
||||
impl LocalSettingsKind {
|
||||
|
||||
@@ -69,7 +69,7 @@ impl Database {
|
||||
extensions: &[extension::Model],
|
||||
constraints: Option<&ExtensionVersionConstraints>,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<HashMap<ExtensionId, (extension_version::Model, SemanticVersion)>> {
|
||||
) -> Result<HashMap<ExtensionId, (extension_version::Model, Version)>> {
|
||||
let mut versions = extension_version::Entity::find()
|
||||
.filter(
|
||||
extension_version::Column::ExtensionId
|
||||
@@ -79,11 +79,10 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
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 {
|
||||
let version = version?;
|
||||
let Some(extension_version) = SemanticVersion::from_str(&version.version).log_err()
|
||||
else {
|
||||
let Some(extension_version) = Version::from_str(&version.version).log_err() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -102,7 +101,7 @@ impl Database {
|
||||
}
|
||||
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ use rpc::{
|
||||
RequestMessage, ShareProject, UpdateChannelBufferCollaborators,
|
||||
},
|
||||
};
|
||||
use semantic_version::SemanticVersion;
|
||||
use semver::Version;
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::{
|
||||
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::StashDrop>)
|
||||
.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_read_only_project_request::<proto::GetRemotes>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitShow>)
|
||||
@@ -984,14 +985,14 @@ impl Server {
|
||||
|
||||
{
|
||||
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(
|
||||
connection_id,
|
||||
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?;
|
||||
}
|
||||
|
||||
@@ -1135,7 +1136,7 @@ impl Header for ProtocolVersion {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AppVersionHeader(SemanticVersion);
|
||||
pub struct AppVersionHeader(Version);
|
||||
impl Header for AppVersionHeader {
|
||||
fn name() -> &'static HeaderName {
|
||||
static ZED_APP_VERSION: OnceLock<HeaderName> = OnceLock::new();
|
||||
@@ -2833,8 +2834,8 @@ async fn remove_contact(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_auto_subscribe_to_channels(version: ZedVersion) -> bool {
|
||||
version.0.minor() < 139
|
||||
fn should_auto_subscribe_to_channels(version: &ZedVersion) -> bool {
|
||||
version.0.minor < 139
|
||||
}
|
||||
|
||||
async fn subscribe_to_channels(
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::db::{ChannelId, ChannelRole, UserId};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use rpc::ConnectionId;
|
||||
use semantic_version::SemanticVersion;
|
||||
use semver::Version;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use tracing::instrument;
|
||||
@@ -19,8 +19,8 @@ struct ConnectedPrincipal {
|
||||
connection_ids: HashSet<ConnectionId>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, PartialOrd, PartialEq, Eq, Ord)]
|
||||
pub struct ZedVersion(pub SemanticVersion);
|
||||
#[derive(Clone, Debug, Serialize, PartialOrd, PartialEq, Eq, Ord)]
|
||||
pub struct ZedVersion(pub Version);
|
||||
|
||||
impl fmt::Display for ZedVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
@@ -32,13 +32,13 @@ impl ZedVersion {
|
||||
pub fn can_collaborate(&self) -> bool {
|
||||
// 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.
|
||||
if self.0 < SemanticVersion::new(0, 204, 1) {
|
||||
if self.0 < Version::new(0, 204, 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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].
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@ use editor::{
|
||||
DocumentColorsRenderMode, Editor, FETCH_COLORS_DEBOUNCE_TIMEOUT, MultiBufferOffset, RowInfo,
|
||||
SelectionEffects,
|
||||
actions::{
|
||||
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
|
||||
ExpandMacroRecursively, MoveToEnd, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
|
||||
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, CopyFileLocation,
|
||||
CopyFileName, CopyFileNameWithoutExtension, ExpandMacroRecursively, MoveToEnd, Redo,
|
||||
Rename, SelectAll, ToggleCodeActions, Undo,
|
||||
},
|
||||
test::{
|
||||
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]
|
||||
fn tab_undo_assert(
|
||||
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.
|
||||
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.add_panel(panel, window, cx);
|
||||
workspace.toggle_panel_focus::<TestPanel>(window, cx);
|
||||
|
||||
@@ -7,10 +7,7 @@ use debugger_ui::debugger_panel::DebugPanel;
|
||||
use extension::ExtensionHostProxy;
|
||||
use fs::{FakeFs, Fs as _, RemoveOptions};
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{
|
||||
AppContext as _, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal as _,
|
||||
VisualContext,
|
||||
};
|
||||
use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as _, VisualContext};
|
||||
use http_client::BlockedHttpClient;
|
||||
use language::{
|
||||
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
|
||||
@@ -43,10 +40,10 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
) {
|
||||
let executor = cx_a.executor();
|
||||
cx_a.update(|cx| {
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
release_channel::init(semver::Version::new(0, 0, 0), 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 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");
|
||||
|
||||
cx_a.update(|cx| {
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
release_channel::init(semver::Version::new(0, 0, 0), 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;
|
||||
@@ -396,10 +393,10 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
server_cx.set_name("server");
|
||||
|
||||
cx_a.update(|cx| {
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
release_channel::init(semver::Version::new(0, 0, 0), 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;
|
||||
@@ -583,13 +580,13 @@ async fn test_remote_server_debugger(
|
||||
executor: BackgroundExecutor,
|
||||
) {
|
||||
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);
|
||||
zlog::init_test();
|
||||
dap_adapters::init(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);
|
||||
});
|
||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||
@@ -691,13 +688,13 @@ async fn test_slow_adapter_startup_retries(
|
||||
executor: BackgroundExecutor,
|
||||
) {
|
||||
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);
|
||||
zlog::init_test();
|
||||
dap_adapters::init(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);
|
||||
});
|
||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||
|
||||
@@ -31,7 +31,6 @@ use rpc::{
|
||||
RECEIVE_TIMEOUT,
|
||||
proto::{self, ChannelRole},
|
||||
};
|
||||
use semantic_version::SemanticVersion;
|
||||
use serde_json::json;
|
||||
use session::{AppSession, Session};
|
||||
use settings::SettingsStore;
|
||||
@@ -173,7 +172,7 @@ impl TestServer {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
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());
|
||||
@@ -295,7 +294,7 @@ impl TestServer {
|
||||
server_conn,
|
||||
client_name,
|
||||
Principal::User(user),
|
||||
ZedVersion(SemanticVersion::new(1, 0, 0)),
|
||||
ZedVersion(semver::Version::new(1, 0, 0)),
|
||||
Some("test".to_string()),
|
||||
None,
|
||||
None,
|
||||
|
||||
@@ -10,7 +10,7 @@ use gpui::{
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use ui::{Avatar, CheckboxWithLabel, ContextMenu, ListItem, ListItemSpacing, prelude::*};
|
||||
use ui::{Avatar, Checkbox, ContextMenu, ListItem, ListItemSpacing, prelude::*};
|
||||
use util::TryFutureExt;
|
||||
use workspace::{ModalView, notifications::DetachAndPromptErr};
|
||||
|
||||
@@ -165,16 +165,18 @@ impl Render for ChannelModal {
|
||||
.h(rems_from_px(22.))
|
||||
.justify_between()
|
||||
.line_height(rems(1.25))
|
||||
.child(CheckboxWithLabel::new(
|
||||
"is-public",
|
||||
Label::new("Public").size(LabelSize::Small),
|
||||
if visibility == ChannelVisibility::Public {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
},
|
||||
cx.listener(Self::set_channel_visibility),
|
||||
))
|
||||
.child(
|
||||
Checkbox::new(
|
||||
"is-public",
|
||||
if visibility == ChannelVisibility::Public {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
},
|
||||
)
|
||||
.label("Public")
|
||||
.on_click(cx.listener(Self::set_channel_visibility)),
|
||||
)
|
||||
.children(
|
||||
Some(
|
||||
Button::new("copy-link", "Copy Link")
|
||||
|
||||
@@ -353,6 +353,8 @@ pub enum ToolCallContent {
|
||||
pub struct FunctionContent {
|
||||
pub name: String,
|
||||
pub arguments: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub thought_signature: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -396,6 +398,7 @@ pub struct ToolCallChunk {
|
||||
pub struct FunctionChunk {
|
||||
pub name: Option<String>,
|
||||
pub arguments: Option<String>,
|
||||
pub thought_signature: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
@@ -68,7 +68,7 @@ impl EditPredictionProvider for CopilotCompletionProvider {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_refreshing(&self) -> bool {
|
||||
fn is_refreshing(&self, _cx: &App) -> bool {
|
||||
self.pending_refresh.is_some() && self.completions.is_empty()
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +127,8 @@ pub enum ResponseInputItem {
|
||||
arguments: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
status: Option<ItemStatus>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
thought_signature: Option<String>,
|
||||
},
|
||||
FunctionCallOutput {
|
||||
call_id: String,
|
||||
@@ -251,6 +253,8 @@ pub enum ResponseOutputItem {
|
||||
arguments: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
status: Option<ItemStatus>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
thought_signature: Option<String>,
|
||||
},
|
||||
Reasoning {
|
||||
id: String,
|
||||
|
||||
@@ -51,11 +51,13 @@ pub async fn init(crash_init: InitCrashHandler) {
|
||||
unsafe { env::set_var("RUST_BACKTRACE", "1") };
|
||||
old_hook(info);
|
||||
// prevent the macOS crash dialog from popping up
|
||||
std::process::exit(1);
|
||||
if cfg!(target_os = "macos") {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
(Some(true), _) | (None, _) => {
|
||||
_ => {
|
||||
panic::set_hook(Box::new(panic_hook));
|
||||
}
|
||||
}
|
||||
@@ -300,11 +302,18 @@ pub fn panic_hook(info: &PanicHookInfo) {
|
||||
.map(|loc| format!("{}:{}", loc.file(), loc.line()))
|
||||
.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
|
||||
// if it's still not there just write panic info and no minidump
|
||||
let retry_frequency = Duration::from_millis(100);
|
||||
for _ in 0..5 {
|
||||
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
|
||||
.send_message(
|
||||
2,
|
||||
|
||||
@@ -70,6 +70,7 @@ theme.workspace = true
|
||||
tree-sitter-json.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
unindent = { workspace = true, optional = true }
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -651,6 +651,17 @@ impl DebugPanel {
|
||||
.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(
|
||||
div.w_full()
|
||||
.py_1()
|
||||
@@ -658,7 +669,7 @@ impl DebugPanel {
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
.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(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
@@ -957,6 +968,7 @@ impl DebugPanel {
|
||||
.child(edit_debug_json_button())
|
||||
.child(documentation_button())
|
||||
.child(logs_button())
|
||||
.child(close_bottom_panel_button)
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -1692,7 +1704,7 @@ impl Render for DebugPanel {
|
||||
.child(
|
||||
Button::new("spawn-new-session-empty-state", "New Session")
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, window, cx| {
|
||||
@@ -1702,8 +1714,7 @@ impl Render for DebugPanel {
|
||||
.child(
|
||||
Button::new("edit-debug-settings", "Edit debug.json")
|
||||
.icon(IconName::Code)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, window, cx| {
|
||||
@@ -1716,8 +1727,7 @@ impl Render for DebugPanel {
|
||||
.child(
|
||||
Button::new("open-debugger-docs", "Debugger Docs")
|
||||
.icon(IconName::Book)
|
||||
.color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/debugger")),
|
||||
@@ -1728,8 +1738,7 @@ impl Render for DebugPanel {
|
||||
"Debugger Extensions",
|
||||
)
|
||||
.icon(IconName::Blocks)
|
||||
.color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.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()
|
||||
.group("base-breakpoint-list")
|
||||
.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(
|
||||
v_flex()
|
||||
|
||||
@@ -12,23 +12,22 @@ use tasks_ui::{TaskOverrides, TasksModal};
|
||||
use dap::{
|
||||
DapRegistry, DebugRequest, TelemetrySpawnLocation, adapters::DebugAdapterName, send_telemetry,
|
||||
};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use editor::Editor;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
KeyContext, Render, Subscription, Task, TextStyle, WeakEntity,
|
||||
KeyContext, Render, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools as _;
|
||||
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
|
||||
use project::{DebugScenarioContext, Project, TaskContexts, TaskSourceKind, task_store::TaskStore};
|
||||
use settings::Settings;
|
||||
use task::{DebugScenario, RevealTarget, VariableName, ZedDebugConfig};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
CheckboxWithLabel, ContextMenu, DropdownMenu, FluentBuilder, IconWithIndicator, Indicator,
|
||||
KeyBinding, ListItem, ListItemSpacing, ToggleButtonGroup, ToggleButtonSimple, ToggleState,
|
||||
Tooltip, prelude::*,
|
||||
ContextMenu, DropdownMenu, FluentBuilder, IconWithIndicator, Indicator, KeyBinding, ListItem,
|
||||
ListItemSpacing, Switch, SwitchLabelPosition, ToggleButtonGroup, ToggleButtonSimple,
|
||||
ToggleState, Tooltip, prelude::*,
|
||||
};
|
||||
use ui_input::InputField;
|
||||
use util::{ResultExt, debug_panic, rel_path::RelPath, shell::ShellKind};
|
||||
use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane};
|
||||
|
||||
@@ -448,7 +447,7 @@ impl NewProcessModal {
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ui::DropdownMenu {
|
||||
) -> DropdownMenu {
|
||||
let workspace = self.workspace.clone();
|
||||
let weak = cx.weak_entity();
|
||||
let active_buffer = self.task_contexts(cx).and_then(|tc| {
|
||||
@@ -508,6 +507,13 @@ impl NewProcessModal {
|
||||
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 {
|
||||
fn render(
|
||||
&mut self,
|
||||
@@ -788,22 +756,26 @@ impl RenderOnce for AttachMode {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct ConfigureMode {
|
||||
program: Entity<Editor>,
|
||||
cwd: Entity<Editor>,
|
||||
program: Entity<InputField>,
|
||||
cwd: Entity<InputField>,
|
||||
stop_on_entry: ToggleState,
|
||||
save_to_debug_json: ToggleState,
|
||||
}
|
||||
|
||||
impl ConfigureMode {
|
||||
pub(super) fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let program = cx.new(|cx| Editor::single_line(window, cx));
|
||||
program.update(cx, |this, cx| {
|
||||
this.set_placeholder_text("ENV=Zed ~/bin/program --option", window, cx);
|
||||
let program = cx.new(|cx| {
|
||||
InputField::new(window, cx, "ENV=Zed ~/bin/program --option")
|
||||
.label("Program")
|
||||
.tab_stop(true)
|
||||
.tab_index(1)
|
||||
});
|
||||
|
||||
let cwd = cx.new(|cx| Editor::single_line(window, cx));
|
||||
cwd.update(cx, |this, cx| {
|
||||
this.set_placeholder_text("Ex: $ZED_WORKTREE_ROOT", window, cx);
|
||||
let cwd = cx.new(|cx| {
|
||||
InputField::new(window, cx, "Ex: $ZED_WORKTREE_ROOT")
|
||||
.label("Working Directory")
|
||||
.tab_stop(true)
|
||||
.tab_index(2)
|
||||
});
|
||||
|
||||
cx.new(|_| Self {
|
||||
@@ -815,9 +787,9 @@ impl ConfigureMode {
|
||||
}
|
||||
|
||||
fn load(&mut self, cwd: PathBuf, window: &mut Window, cx: &mut App) {
|
||||
self.cwd.update(cx, |editor, cx| {
|
||||
if editor.is_empty(cx) {
|
||||
editor.set_text(cwd.to_string_lossy(), window, cx);
|
||||
self.cwd.update(cx, |input_field, cx| {
|
||||
if input_field.is_empty(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(
|
||||
&mut self,
|
||||
adapter_menu: DropdownMenu,
|
||||
window: &mut Window,
|
||||
_: &mut Window,
|
||||
cx: &mut ui::Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
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()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.track_focus(&self.program.focus_handle(cx))
|
||||
.gap_3()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new("Debugger")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.gap_1()
|
||||
.child(Label::new("Debugger:").color(Color::Muted))
|
||||
.child(adapter_menu),
|
||||
)
|
||||
.child(self.program.clone())
|
||||
.child(self.cwd.clone())
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new("Program")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.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,
|
||||
{
|
||||
Switch::new("debugger-stop-on-entry", self.stop_on_entry)
|
||||
.tab_index(3_isize)
|
||||
.label("Stop on Entry")
|
||||
.label_position(SwitchLabelPosition::Start)
|
||||
.label_size(LabelSize::Default)
|
||||
.color(ui::SwitchColor::Accent)
|
||||
.on_click({
|
||||
let this = cx.weak_entity();
|
||||
move |state, _, cx| {
|
||||
this.update(cx, |this, _| {
|
||||
@@ -924,9 +890,7 @@ impl ConfigureMode {
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
)
|
||||
.checkbox_position(ui::IconPosition::End),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1407,7 +1407,6 @@ impl RenderOnce for BreakpointOptionsStrip {
|
||||
|
||||
h_flex()
|
||||
.gap_px()
|
||||
.mr_3() // Space to avoid overlapping with the scrollbar
|
||||
.justify_end()
|
||||
.when(has_logs || self.is_selected, |this| {
|
||||
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