Compare commits
65 Commits
disable-so
...
vim-scroll
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b81c19fa1 | ||
|
|
83dfdb0cfe | ||
|
|
566c5f91a7 | ||
|
|
21057e3af7 | ||
|
|
f68a475eca | ||
|
|
c62210b178 | ||
|
|
ad14dcc57b | ||
|
|
b9432dbe42 | ||
|
|
41c373eff1 | ||
|
|
6a95ec6a64 | ||
|
|
8d7b021f92 | ||
|
|
798a34bfc2 | ||
|
|
a4a9f6bd07 | ||
|
|
bfe4c40f73 | ||
|
|
daa16bcf42 | ||
|
|
22ad7b17c5 | ||
|
|
728a5eb388 | ||
|
|
8d8e5d3635 | ||
|
|
a05a480ed9 | ||
|
|
d141fa027e | ||
|
|
8e0e291bd5 | ||
|
|
e3c0f56a96 | ||
|
|
3935e8343a | ||
|
|
0c84170071 | ||
|
|
a38687d278 | ||
|
|
b75b308459 | ||
|
|
dffa725c7d | ||
|
|
22f1429f97 | ||
|
|
6bdd2cf7db | ||
|
|
a7f3b22051 | ||
|
|
f3703fa8be | ||
|
|
a0be6c8cb2 | ||
|
|
b5a7fb13c3 | ||
|
|
2183fc674d | ||
|
|
0ad5979f19 | ||
|
|
ed1938dd9a | ||
|
|
f7927d3fa4 | ||
|
|
8361c32a34 | ||
|
|
2edadd9352 | ||
|
|
85384fb9c6 | ||
|
|
00359271d1 | ||
|
|
18fcdf1d2c | ||
|
|
55c927b039 | ||
|
|
1be3f81920 | ||
|
|
2eb4d6b7eb | ||
|
|
25f407baab | ||
|
|
79874872cb | ||
|
|
95208a6576 | ||
|
|
1034d1a6b5 | ||
|
|
d4eab557b2 | ||
|
|
b75964a636 | ||
|
|
87cdb68cca | ||
|
|
b0b65420f6 | ||
|
|
8ec0309645 | ||
|
|
6767e98e00 | ||
|
|
8cf5af1a84 | ||
|
|
247ee880d2 | ||
|
|
2e217759c0 | ||
|
|
0a0c163692 | ||
|
|
e80df25386 | ||
|
|
d9590f3f0e | ||
|
|
4ecd1b5174 | ||
|
|
70c973f6c3 | ||
|
|
e842b4eade | ||
|
|
606aa7a78c |
51
.github/ISSUE_TEMPLATE/0_git_beta_bug_report.yml
vendored
51
.github/ISSUE_TEMPLATE/0_git_beta_bug_report.yml
vendored
@@ -1,51 +0,0 @@
|
||||
name: Git Beta
|
||||
description: There is a bug related to new Git features in Zed
|
||||
type: "Bug"
|
||||
labels: [git]
|
||||
title: "Git Beta: <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 -->
|
||||
|
||||
<!-- 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 `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
description: |
|
||||
macOS: `~/Library/Logs/Zed/Zed.log`
|
||||
Linux: `~/.local/share/zed/logs/Zed.log` or $XDG_DATA_HOME
|
||||
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
|
||||
value: |
|
||||
<details><summary>Zed.log</summary>
|
||||
|
||||
<!-- Click below this line and paste or drag-and-drop your log-->
|
||||
```
|
||||
|
||||
```
|
||||
<!-- Click above this line and paste or drag-and-drop your log--></details>
|
||||
validations:
|
||||
required: false
|
||||
47
.github/workflows/ci.yml
vendored
47
.github/workflows/ci.yml
vendored
@@ -28,6 +28,7 @@ jobs:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
outputs:
|
||||
run_tests: ${{ steps.filter.outputs.run_tests }}
|
||||
run_license: ${{ steps.filter.outputs.run_license }}
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
@@ -47,7 +48,12 @@ jobs:
|
||||
git fetch origin "$GITHUB_BASE_REF" --depth=350
|
||||
COMPARE_REV=$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)
|
||||
fi
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -v "^docs/") ]]; then
|
||||
# Specify anything which should skip full CI in this regex:
|
||||
# - docs/
|
||||
# - .github/ISSUE_TEMPLATE/
|
||||
# - .github/workflows/ (except .github/workflows/ci.yml)
|
||||
SKIP_REGEX='^(docs/|\.github/(ISSUE_TEMPLATE|workflows/(?!ci)))'
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -vP "$SKIP_REGEX") ]]; then
|
||||
echo "run_tests=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "run_tests=false" >> $GITHUB_OUTPUT
|
||||
@@ -366,7 +372,8 @@ jobs:
|
||||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
needs.job_spec.outputs.run_tests == 'true'
|
||||
runs-on: windows-2025-64
|
||||
# Use bigger runners for PRs (speed); smaller for async (cost)
|
||||
runs-on: ${{ github.event_name == 'pull_request' && 'windows-2025-32' || 'windows-2025-16' }}
|
||||
steps:
|
||||
# more info here:- https://github.com/rust-lang/cargo/issues/13020
|
||||
- name: Enable longer pathnames for git
|
||||
@@ -431,22 +438,28 @@ jobs:
|
||||
- macos_tests
|
||||
- windows_clippy
|
||||
- windows_tests
|
||||
if: |
|
||||
always() && (
|
||||
needs.style.result == 'success'
|
||||
&& (
|
||||
needs.job_spec.outputs.run_tests == 'false'
|
||||
|| (needs.macos_tests.result == 'success'
|
||||
&& needs.linux_tests.result == 'success'
|
||||
&& needs.windows_tests.result == 'success'
|
||||
&& needs.windows_clippy.result == 'success'
|
||||
&& needs.build_remote_server.result == 'success'
|
||||
&& needs.migration_checks.result == 'success')
|
||||
)
|
||||
)
|
||||
if: always()
|
||||
steps:
|
||||
- name: All tests passed
|
||||
run: echo "All tests passed successfully!"
|
||||
- name: Check all tests passed
|
||||
run: |
|
||||
# Check dependent jobs...
|
||||
RET_CODE=0
|
||||
# Always check style
|
||||
[[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }
|
||||
|
||||
# Only check test jobs if they were supposed to run
|
||||
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
|
||||
[[ "${{ needs.macos_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "macOS tests failed"; }
|
||||
[[ "${{ needs.linux_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Linux tests failed"; }
|
||||
[[ "${{ needs.windows_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows tests failed"; }
|
||||
[[ "${{ needs.windows_clippy.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows clippy failed"; }
|
||||
[[ "${{ needs.migration_checks.result }}" != 'success' ]] && { RET_CODE=1; echo "Migration checks failed"; }
|
||||
[[ "${{ needs.build_remote_server.result }}" != 'success' ]] && { RET_CODE=1; echo "Remote server build failed"; }
|
||||
fi
|
||||
if [[ "$RET_CODE" -eq 0 ]]; then
|
||||
echo "All tests passed successfully!"
|
||||
fi
|
||||
exit $RET_CODE
|
||||
|
||||
bundle-mac:
|
||||
timeout-minutes: 120
|
||||
|
||||
451
Cargo.lock
generated
451
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
4
assets/icons/expand_down.svg
Normal file
4
assets/icons/expand_down.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.5 8.5L7.5 11.5M7.5 11.5L4.5 8.5M7.5 11.5L7.5 5.5" stroke="black" stroke-linecap="square"/>
|
||||
<path d="M5 3.5L10 3.5" stroke="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 248 B |
4
assets/icons/expand_up.svg
Normal file
4
assets/icons/expand_up.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.5 6.5L7.5 3.5M7.5 3.5L10.5 6.5M7.5 3.5V9.5" stroke="black" stroke-linecap="square"/>
|
||||
<path d="M5 11.5H10" stroke="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 238 B |
@@ -1,6 +1,6 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 4H8" stroke="black" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 10L11 10" stroke="black" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="4" cy="10" r="1.875" stroke="black" stroke-width="1.75"/>
|
||||
<circle cx="10" cy="4" r="1.875" stroke="black" stroke-width="1.75"/>
|
||||
<path d="M3 4H8" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 10L11 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="4" cy="10" r="1.875" stroke="black" stroke-width="1.5"/>
|
||||
<circle cx="10" cy="4" r="1.875" stroke="black" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 446 B |
@@ -362,6 +362,7 @@
|
||||
"ctrl-k ctrl-0": "editor::FoldAll",
|
||||
"ctrl-k ctrl-j": "editor::UnfoldAll",
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
"ctrl-shift-space": "editor::ShowWordCompletions",
|
||||
"ctrl-.": "editor::ToggleCodeActions",
|
||||
"ctrl-k r": "editor::RevealInFileManager",
|
||||
"ctrl-k p": "editor::CopyPath",
|
||||
|
||||
@@ -466,6 +466,7 @@
|
||||
// Using `ctrl-space` in Zed requires disabling the macOS global shortcut.
|
||||
// System Preferences->Keyboard->Keyboard Shortcuts->Input Sources->Select the previous input source (uncheck)
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
"ctrl-shift-space": "editor::ShowWordCompletions",
|
||||
"cmd-.": "editor::ToggleCodeActions",
|
||||
"cmd-k r": "editor::RevealInFileManager",
|
||||
"cmd-k p": "editor::CopyPath",
|
||||
|
||||
@@ -155,6 +155,7 @@
|
||||
"z +": ["workspace::SendKeystrokes", "shift-l j z t ^"],
|
||||
"z t": "editor::ScrollCursorTop",
|
||||
"z z": "editor::ScrollCursorCenter",
|
||||
"z l": "vim::ScrollLeftHalfWay",
|
||||
"z .": ["workspace::SendKeystrokes", "z z ^"],
|
||||
"z b": "editor::ScrollCursorBottom",
|
||||
"z a": "editor::ToggleFold",
|
||||
|
||||
@@ -11,8 +11,8 @@ You should only perform actions that modify the user’s system if explicitly re
|
||||
|
||||
Be concise and direct in your responses.
|
||||
|
||||
The user has opened a project that contains the following top-level directories/files:
|
||||
The user has opened a project that contains the following root directories/files:
|
||||
|
||||
{{#each worktree_root_names}}
|
||||
- {{this}}
|
||||
{{#each worktrees}}
|
||||
- {{root_name}} (absolute path: {{abs_path}})
|
||||
{{/each}}
|
||||
|
||||
@@ -547,7 +547,7 @@
|
||||
"git_panel": {
|
||||
// Whether to show the git panel button in the status bar.
|
||||
"button": true,
|
||||
// Where to the git panel. Can be 'left' or 'right'.
|
||||
// Where to show the git panel. Can be 'left' or 'right'.
|
||||
"dock": "left",
|
||||
// Default width of the git panel.
|
||||
"default_width": 360,
|
||||
@@ -1092,11 +1092,12 @@
|
||||
//
|
||||
// May take 3 values:
|
||||
// 1. "enabled"
|
||||
// Always fetch document's words for completions.
|
||||
// Always fetch document's words for completions along with LSP completions.
|
||||
// 2. "fallback"
|
||||
// Only if LSP response errors/times out/is empty, use document's words to show completions.
|
||||
// Only if LSP response errors or times out, use document's words to show completions.
|
||||
// 3. "disabled"
|
||||
// Never fetch or complete document's words for completions.
|
||||
// (Word-based completions can still be queried via a separate action)
|
||||
//
|
||||
// Default: fallback
|
||||
"words": "fallback",
|
||||
@@ -1107,8 +1108,8 @@
|
||||
// When fetching LSP completions, determines how long to wait for a response of a particular server.
|
||||
// When set to 0, waits indefinitely.
|
||||
//
|
||||
// Default: 500
|
||||
"lsp_fetch_timeout_ms": 500
|
||||
// Default: 0
|
||||
"lsp_fetch_timeout_ms": 0
|
||||
},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
|
||||
@@ -6,15 +6,7 @@
|
||||
{
|
||||
"name": "Gruvbox Dark",
|
||||
"appearance": "dark",
|
||||
"accents": [
|
||||
"#cc241dff",
|
||||
"#98971aff",
|
||||
"#d79921ff",
|
||||
"#458588ff",
|
||||
"#b16286ff",
|
||||
"#689d6aff",
|
||||
"#d65d0eff"
|
||||
],
|
||||
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
|
||||
"style": {
|
||||
"border": "#5b534dff",
|
||||
"border.variant": "#494340ff",
|
||||
@@ -105,9 +97,9 @@
|
||||
"terminal.ansi.bright_white": "#fbf1c7ff",
|
||||
"terminal.ansi.dim_white": "#b0a189ff",
|
||||
"link_text.hover": "#83a598ff",
|
||||
"version_control_added": "#b7bb26ff",
|
||||
"version_control_modified": "#f9bd2fff",
|
||||
"version_control_deleted": "#fb4a35ff",
|
||||
"version_control.added": "#b7bb26ff",
|
||||
"version_control.modified": "#f9bd2fff",
|
||||
"version_control.deleted": "#fb4a35ff",
|
||||
"conflict": "#f9bd2fff",
|
||||
"conflict.background": "#572e10ff",
|
||||
"conflict.border": "#754916ff",
|
||||
@@ -399,15 +391,7 @@
|
||||
{
|
||||
"name": "Gruvbox Dark Hard",
|
||||
"appearance": "dark",
|
||||
"accents": [
|
||||
"#cc241dff",
|
||||
"#98971aff",
|
||||
"#d79921ff",
|
||||
"#458588ff",
|
||||
"#b16286ff",
|
||||
"#689d6aff",
|
||||
"#d65d0eff"
|
||||
],
|
||||
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
|
||||
"style": {
|
||||
"border": "#5b534dff",
|
||||
"border.variant": "#494340ff",
|
||||
@@ -498,9 +482,9 @@
|
||||
"terminal.ansi.bright_white": "#fbf1c7ff",
|
||||
"terminal.ansi.dim_white": "#b0a189ff",
|
||||
"link_text.hover": "#83a598ff",
|
||||
"version_control_added": "#b7bb26ff",
|
||||
"version_control_modified": "#f9bd2fff",
|
||||
"version_control_deleted": "#fb4a35ff",
|
||||
"version_control.added": "#b7bb26ff",
|
||||
"version_control.modified": "#f9bd2fff",
|
||||
"version_control.deleted": "#fb4a35ff",
|
||||
"conflict": "#f9bd2fff",
|
||||
"conflict.background": "#572e10ff",
|
||||
"conflict.border": "#754916ff",
|
||||
@@ -792,15 +776,7 @@
|
||||
{
|
||||
"name": "Gruvbox Dark Soft",
|
||||
"appearance": "dark",
|
||||
"accents": [
|
||||
"#cc241dff",
|
||||
"#98971aff",
|
||||
"#d79921ff",
|
||||
"#458588ff",
|
||||
"#b16286ff",
|
||||
"#689d6aff",
|
||||
"#d65d0eff"
|
||||
],
|
||||
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
|
||||
"style": {
|
||||
"border": "#5b534dff",
|
||||
"border.variant": "#494340ff",
|
||||
@@ -891,9 +867,9 @@
|
||||
"terminal.ansi.bright_white": "#fbf1c7ff",
|
||||
"terminal.ansi.dim_white": "#b0a189ff",
|
||||
"link_text.hover": "#83a598ff",
|
||||
"version_control_added": "#b7bb26ff",
|
||||
"version_control_modified": "#f9bd2fff",
|
||||
"version_control_deleted": "#fb4a35ff",
|
||||
"version_control.added": "#b7bb26ff",
|
||||
"version_control.modified": "#f9bd2fff",
|
||||
"version_control.deleted": "#fb4a35ff",
|
||||
"conflict": "#f9bd2fff",
|
||||
"conflict.background": "#572e10ff",
|
||||
"conflict.border": "#754916ff",
|
||||
@@ -1185,15 +1161,7 @@
|
||||
{
|
||||
"name": "Gruvbox Light",
|
||||
"appearance": "light",
|
||||
"accents": [
|
||||
"#cc241dff",
|
||||
"#98971aff",
|
||||
"#d79921ff",
|
||||
"#458588ff",
|
||||
"#b16286ff",
|
||||
"#689d6aff",
|
||||
"#d65d0eff"
|
||||
],
|
||||
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
|
||||
"style": {
|
||||
"border": "#c8b899ff",
|
||||
"border.variant": "#ddcca7ff",
|
||||
@@ -1284,9 +1252,9 @@
|
||||
"terminal.ansi.bright_white": "#282828ff",
|
||||
"terminal.ansi.dim_white": "#73675eff",
|
||||
"link_text.hover": "#0b6678ff",
|
||||
"version_control_added": "#797410ff",
|
||||
"version_control_modified": "#b57615ff",
|
||||
"version_control_deleted": "#9d0308ff",
|
||||
"version_control.added": "#797410ff",
|
||||
"version_control.modified": "#b57615ff",
|
||||
"version_control.deleted": "#9d0308ff",
|
||||
"conflict": "#b57615ff",
|
||||
"conflict.background": "#f5e2d0ff",
|
||||
"conflict.border": "#ebccabff",
|
||||
@@ -1578,15 +1546,7 @@
|
||||
{
|
||||
"name": "Gruvbox Light Hard",
|
||||
"appearance": "light",
|
||||
"accents": [
|
||||
"#cc241dff",
|
||||
"#98971aff",
|
||||
"#d79921ff",
|
||||
"#458588ff",
|
||||
"#b16286ff",
|
||||
"#689d6aff",
|
||||
"#d65d0eff"
|
||||
],
|
||||
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
|
||||
"style": {
|
||||
"border": "#c8b899ff",
|
||||
"border.variant": "#ddcca7ff",
|
||||
@@ -1677,9 +1637,9 @@
|
||||
"terminal.ansi.bright_white": "#282828ff",
|
||||
"terminal.ansi.dim_white": "#73675eff",
|
||||
"link_text.hover": "#0b6678ff",
|
||||
"version_control_added": "#797410ff",
|
||||
"version_control_modified": "#b57615ff",
|
||||
"version_control_deleted": "#9d0308ff",
|
||||
"version_control.added": "#797410ff",
|
||||
"version_control.modified": "#b57615ff",
|
||||
"version_control.deleted": "#9d0308ff",
|
||||
"conflict": "#b57615ff",
|
||||
"conflict.background": "#f5e2d0ff",
|
||||
"conflict.border": "#ebccabff",
|
||||
@@ -1971,15 +1931,7 @@
|
||||
{
|
||||
"name": "Gruvbox Light Soft",
|
||||
"appearance": "light",
|
||||
"accents": [
|
||||
"#cc241dff",
|
||||
"#98971aff",
|
||||
"#d79921ff",
|
||||
"#458588ff",
|
||||
"#b16286ff",
|
||||
"#689d6aff",
|
||||
"#d65d0eff"
|
||||
],
|
||||
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
|
||||
"style": {
|
||||
"border": "#c8b899ff",
|
||||
"border.variant": "#ddcca7ff",
|
||||
@@ -2070,9 +2022,9 @@
|
||||
"terminal.ansi.bright_white": "#282828ff",
|
||||
"terminal.ansi.dim_white": "#73675eff",
|
||||
"link_text.hover": "#0b6678ff",
|
||||
"version_control_added": "#797410ff",
|
||||
"version_control_modified": "#b57615ff",
|
||||
"version_control_deleted": "#9d0308ff",
|
||||
"version_control.added": "#797410ff",
|
||||
"version_control.modified": "#b57615ff",
|
||||
"version_control.deleted": "#9d0308ff",
|
||||
"conflict": "#b57615ff",
|
||||
"conflict.background": "#f5e2d0ff",
|
||||
"conflict.border": "#ebccabff",
|
||||
|
||||
@@ -553,7 +553,7 @@ pub struct Metadata {
|
||||
pub user_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct Usage {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub input_tokens: Option<u32>,
|
||||
|
||||
@@ -1246,7 +1246,7 @@ impl InlineAssistant {
|
||||
});
|
||||
|
||||
enum DeletedLines {}
|
||||
let mut editor = Editor::for_multibuffer(multi_buffer, None, true, window, cx);
|
||||
let mut editor = Editor::for_multibuffer(multi_buffer, None, window, cx);
|
||||
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
@@ -1693,7 +1693,6 @@ impl PromptEditor {
|
||||
},
|
||||
prompt_buffer,
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -720,7 +720,6 @@ impl PromptEditor {
|
||||
},
|
||||
prompt_buffer,
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -38,6 +38,7 @@ file_icons.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
heed.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
@@ -65,6 +66,7 @@ serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
streaming_diff.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
terminal.workspace = true
|
||||
terminal_view.workspace = true
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::tool_use::{ToolUse, ToolUseStatus};
|
||||
use crate::ui::ContextPill;
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
list, AbsoluteLength, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
|
||||
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, StyleRefinement, Subscription,
|
||||
Task, TextStyleRefinement, UnderlineStyle,
|
||||
list, percentage, AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent,
|
||||
DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Length, ListAlignment, ListOffset,
|
||||
ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Transformation,
|
||||
UnderlineStyle,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use scripting_tool::{ScriptingTool, ScriptingToolInput};
|
||||
use settings::Settings as _;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
use ui::Color;
|
||||
use ui::{prelude::*, Disclosure, KeyBinding};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::tool_use::{ToolUse, ToolUseStatus};
|
||||
use crate::ui::ContextPill;
|
||||
|
||||
pub struct ActiveThread {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
@@ -394,7 +395,6 @@ impl ActiveThread {
|
||||
editor::EditorMode::AutoHeight { max_lines: 8 },
|
||||
buffer,
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -497,7 +497,7 @@ impl ActiveThread {
|
||||
};
|
||||
|
||||
let thread = self.thread.read(cx);
|
||||
|
||||
// Get all the data we need from thread before we start using it in closures
|
||||
let context = thread.context_for_message(message_id);
|
||||
let tool_uses = thread.tool_uses_for_message(message_id);
|
||||
let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id);
|
||||
@@ -652,28 +652,27 @@ impl ActiveThread {
|
||||
)
|
||||
.child(message_content),
|
||||
),
|
||||
Role::Assistant => v_flex()
|
||||
.id(("message-container", ix))
|
||||
.child(message_content)
|
||||
.map(|parent| {
|
||||
if tool_uses.is_empty() && scripting_tool_uses.is_empty() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent.child(
|
||||
v_flex()
|
||||
.children(
|
||||
tool_uses
|
||||
.into_iter()
|
||||
.map(|tool_use| self.render_tool_use(tool_use, cx)),
|
||||
Role::Assistant => {
|
||||
v_flex()
|
||||
.id(("message-container", ix))
|
||||
.child(message_content)
|
||||
.when(
|
||||
!tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
|
||||
|parent| {
|
||||
parent.child(
|
||||
v_flex()
|
||||
.children(
|
||||
tool_uses
|
||||
.into_iter()
|
||||
.map(|tool_use| self.render_tool_use(tool_use, cx)),
|
||||
)
|
||||
.children(scripting_tool_uses.into_iter().map(|tool_use| {
|
||||
self.render_scripting_tool_use(tool_use, cx)
|
||||
})),
|
||||
)
|
||||
.children(
|
||||
scripting_tool_uses
|
||||
.into_iter()
|
||||
.map(|tool_use| self.render_scripting_tool_use(tool_use, cx)),
|
||||
),
|
||||
},
|
||||
)
|
||||
}),
|
||||
}
|
||||
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
|
||||
v_flex()
|
||||
.bg(colors.editor_background)
|
||||
@@ -692,27 +691,28 @@ impl ActiveThread {
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
|
||||
let lighter_border = cx.theme().colors().border.opacity(0.5);
|
||||
|
||||
div().px_2p5().child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_color(lighter_border)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.py_0p5()
|
||||
.py_1()
|
||||
.pl_1()
|
||||
.pr_2()
|
||||
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
|
||||
.bg(cx.theme().colors().editor_foreground.opacity(0.025))
|
||||
.map(|element| {
|
||||
if is_open {
|
||||
element.border_b_1().rounded_t(px(6.))
|
||||
element.border_b_1().rounded_t_md()
|
||||
} else {
|
||||
element.rounded_md()
|
||||
}
|
||||
})
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_color(lighter_border)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -729,57 +729,129 @@ impl ActiveThread {
|
||||
}
|
||||
}),
|
||||
))
|
||||
.child(Label::new(tool_use.name)),
|
||||
.child(
|
||||
Label::new(tool_use.name)
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new(match tool_use.status {
|
||||
ToolUseStatus::Pending => "Pending",
|
||||
ToolUseStatus::Running => "Running",
|
||||
ToolUseStatus::Finished(_) => "Finished",
|
||||
ToolUseStatus::Error(_) => "Error",
|
||||
})
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
.child({
|
||||
let (icon_name, color, animated) = match &tool_use.status {
|
||||
ToolUseStatus::Pending => {
|
||||
(IconName::Warning, Color::Warning, false)
|
||||
}
|
||||
ToolUseStatus::Running => {
|
||||
(IconName::ArrowCircle, Color::Accent, true)
|
||||
}
|
||||
ToolUseStatus::Finished(_) => {
|
||||
(IconName::Check, Color::Success, false)
|
||||
}
|
||||
ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
|
||||
};
|
||||
|
||||
let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
|
||||
|
||||
if animated {
|
||||
icon.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(delta)))
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
icon.into_any_element()
|
||||
}
|
||||
}),
|
||||
)
|
||||
.map(|parent| {
|
||||
if !is_open {
|
||||
return parent;
|
||||
}
|
||||
|
||||
let content_container = || v_flex().py_1().gap_0p5().px_2p5();
|
||||
|
||||
parent.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_b_lg()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
content_container()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Label::new("Input:"))
|
||||
.child(Label::new(
|
||||
serde_json::to_string_pretty(&tool_use.input)
|
||||
.unwrap_or_default(),
|
||||
)),
|
||||
.border_color(lighter_border)
|
||||
.child(
|
||||
Label::new("Input")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
serde_json::to_string_pretty(&tool_use.input)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
)
|
||||
.map(|parent| match tool_use.status {
|
||||
ToolUseStatus::Finished(output) => parent.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.child(Label::new("Result:"))
|
||||
.child(Label::new(output)),
|
||||
.map(|container| match tool_use.status {
|
||||
ToolUseStatus::Finished(output) => container.child(
|
||||
content_container()
|
||||
.child(
|
||||
Label::new("Result")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(
|
||||
Label::new(output)
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
),
|
||||
ToolUseStatus::Error(err) => parent.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.child(Label::new("Error:"))
|
||||
.child(Label::new(err)),
|
||||
ToolUseStatus::Running => container.child(
|
||||
content_container().child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.pb_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(
|
||||
percentage(delta),
|
||||
))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Running…")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
),
|
||||
),
|
||||
ToolUseStatus::Pending | ToolUseStatus::Running => parent,
|
||||
ToolUseStatus::Error(err) => container.child(
|
||||
content_container()
|
||||
.child(
|
||||
Label::new("Error")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(
|
||||
Label::new(err).size(LabelSize::Small).buffer_font(cx),
|
||||
),
|
||||
),
|
||||
ToolUseStatus::Pending => container,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
@@ -812,7 +884,7 @@ impl ActiveThread {
|
||||
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
|
||||
.map(|element| {
|
||||
if is_open {
|
||||
element.border_b_1().rounded_t(px(6.))
|
||||
element.border_b_1().rounded_t_md()
|
||||
} else {
|
||||
element.rounded_md()
|
||||
}
|
||||
|
||||
@@ -53,7 +53,8 @@ actions!(
|
||||
FocusLeft,
|
||||
FocusRight,
|
||||
RemoveFocusedContext,
|
||||
AcceptSuggestedContext
|
||||
AcceptSuggestedContext,
|
||||
OpenActiveThreadAsMarkdown
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
|
||||
use client::zed_urls;
|
||||
use editor::Editor;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
prelude::*, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter,
|
||||
@@ -38,7 +38,10 @@ use crate::message_editor::MessageEditor;
|
||||
use crate::thread::{Thread, ThreadError, ThreadId};
|
||||
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::{InlineAssistant, NewPromptEditor, NewThread, OpenConfiguration, OpenHistory};
|
||||
use crate::{
|
||||
InlineAssistant, NewPromptEditor, NewThread, OpenActiveThreadAsMarkdown, OpenConfiguration,
|
||||
OpenHistory,
|
||||
};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(
|
||||
@@ -411,6 +414,65 @@ impl AssistantPanel {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn open_active_thread_as_markdown(
|
||||
&mut self,
|
||||
_: &OpenActiveThreadAsMarkdown,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(workspace) = self
|
||||
.workspace
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("workspace dropped"))
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let markdown_language_task = workspace
|
||||
.read(cx)
|
||||
.app_state()
|
||||
.languages
|
||||
.language_for_name("Markdown");
|
||||
let thread = self.active_thread(cx);
|
||||
cx.spawn_in(window, |_this, mut cx| async move {
|
||||
let markdown_language = markdown_language_task.await?;
|
||||
|
||||
workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
let thread = thread.read(cx);
|
||||
let markdown = thread.to_markdown()?;
|
||||
let thread_summary = thread
|
||||
.summary()
|
||||
.map(|summary| summary.to_string())
|
||||
.unwrap_or_else(|| "Thread".to_string());
|
||||
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.create_local_buffer(&markdown, Some(markdown_language), cx)
|
||||
});
|
||||
let buffer = cx.new(|cx| {
|
||||
MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
|
||||
});
|
||||
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(cx.new(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
|
||||
editor.set_breadcrumb_header(thread_summary);
|
||||
editor
|
||||
})),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_assistant_configuration_event(
|
||||
&mut self,
|
||||
_entity: &Entity<AssistantConfiguration>,
|
||||
@@ -1011,6 +1073,7 @@ impl Render for AssistantPanel {
|
||||
.on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
|
||||
this.open_history(window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(Self::open_active_thread_as_markdown))
|
||||
.on_action(cx.listener(Self::deploy_prompt_library))
|
||||
.child(self.render_toolbar(cx))
|
||||
.map(|parent| match self.active_view {
|
||||
|
||||
@@ -223,13 +223,18 @@ pub fn render_thread_context_entry(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
Icon::new(IconName::MessageCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.max_w_72()
|
||||
.child(
|
||||
Icon::new(IconName::MessageCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new(thread.summary.clone()).truncate()),
|
||||
)
|
||||
.child(Label::new(thread.summary.clone()))
|
||||
.child(div().w_full())
|
||||
.when(added, |el| {
|
||||
el.child(
|
||||
h_flex()
|
||||
|
||||
@@ -2,10 +2,10 @@ use assistant_context_editor::SavedContextMetadata;
|
||||
use chrono::{DateTime, Utc};
|
||||
use gpui::{prelude::*, Entity};
|
||||
|
||||
use crate::thread_store::{SavedThreadMetadata, ThreadStore};
|
||||
use crate::thread_store::{SerializedThreadMetadata, ThreadStore};
|
||||
|
||||
pub enum HistoryEntry {
|
||||
Thread(SavedThreadMetadata),
|
||||
Thread(SerializedThreadMetadata),
|
||||
Context(SavedContextMetadata),
|
||||
}
|
||||
|
||||
|
||||
@@ -1341,7 +1341,7 @@ impl InlineAssistant {
|
||||
});
|
||||
|
||||
enum DeletedLines {}
|
||||
let mut editor = Editor::for_multibuffer(multi_buffer, None, true, window, cx);
|
||||
let mut editor = Editor::for_multibuffer(multi_buffer, None, window, cx);
|
||||
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
|
||||
@@ -843,7 +843,6 @@ impl PromptEditor<BufferCodegen> {
|
||||
},
|
||||
prompt_buffer,
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -1001,7 +1000,6 @@ impl PromptEditor<TerminalCodegen> {
|
||||
},
|
||||
prompt_buffer,
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -20,7 +20,8 @@ use ui::{
|
||||
Tooltip,
|
||||
};
|
||||
use vim_mode_setting::VimModeSetting;
|
||||
use workspace::Workspace;
|
||||
use workspace::notifications::{NotificationId, NotifyTaskExt};
|
||||
use workspace::{Toast, Workspace};
|
||||
|
||||
use crate::assistant_model_selector::AssistantModelSelector;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
@@ -34,6 +35,7 @@ use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker};
|
||||
pub struct MessageEditor {
|
||||
thread: Entity<Thread>,
|
||||
editor: Entity<Editor>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: Entity<ContextStore>,
|
||||
context_strip: Entity<ContextStrip>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
@@ -106,6 +108,7 @@ impl MessageEditor {
|
||||
Self {
|
||||
thread,
|
||||
editor: editor.clone(),
|
||||
workspace,
|
||||
context_store,
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
@@ -150,6 +153,14 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.is_editor_empty(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.thread.read(cx).is_streaming() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.send_to_model(RequestKind::Chat, window, cx);
|
||||
}
|
||||
|
||||
@@ -272,6 +283,34 @@ impl MessageEditor {
|
||||
self.context_strip.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_feedback_click(
|
||||
&mut self,
|
||||
is_positive: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let workspace = self.workspace.clone();
|
||||
let report = self
|
||||
.thread
|
||||
.update(cx, |thread, cx| thread.report_feedback(is_positive, cx));
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
report.await?;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let message = if is_positive {
|
||||
"Positive feedback recorded. Thank you!"
|
||||
} else {
|
||||
"Negative feedback recorded. Thank you for helping us improve!"
|
||||
};
|
||||
|
||||
struct ThreadFeedback;
|
||||
let id = NotificationId::unique::<ThreadFeedback>();
|
||||
workspace.show_toast(Toast::new(id, message).autohide(), cx)
|
||||
})
|
||||
})
|
||||
.detach_and_notify_err(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for MessageEditor {
|
||||
@@ -489,7 +528,45 @@ impl Render for MessageEditor {
|
||||
.bg(bg_color)
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(self.context_strip.clone())
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(self.context_strip.clone())
|
||||
.when(!self.thread.read(cx).is_empty(), |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
IconButton::new(
|
||||
"feedback-thumbs-up",
|
||||
IconName::ThumbsUp,
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Helpful"))
|
||||
.on_click(
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.handle_feedback_click(true, window, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(
|
||||
"feedback-thumbs-down",
|
||||
IconName::ThumbsDown,
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Not Helpful"))
|
||||
.on_click(
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.handle_feedback_click(false, window, cx);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_5()
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use futures::StreamExt as _;
|
||||
use futures::future::Shared;
|
||||
use futures::{FutureExt, StreamExt as _};
|
||||
use git;
|
||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
|
||||
Role, StopReason,
|
||||
Role, StopReason, TokenUsage,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_store::PromptBuilder;
|
||||
use prompt_store::{AssistantSystemPromptWorktree, PromptBuilder};
|
||||
use scripting_tool::{ScriptingSession, ScriptingTool};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::{post_inc, ResultExt, TryFutureExt as _};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::context::{attach_context_to_message, ContextId, ContextSnapshot};
|
||||
use crate::thread_store::SavedThread;
|
||||
use crate::thread_store::{
|
||||
SerializedMessage, SerializedThread, SerializedToolResult, SerializedToolUse,
|
||||
};
|
||||
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -62,6 +67,27 @@ pub struct Message {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProjectSnapshot {
|
||||
pub worktree_snapshots: Vec<WorktreeSnapshot>,
|
||||
pub unsaved_buffer_paths: Vec<String>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorktreeSnapshot {
|
||||
pub worktree_path: String,
|
||||
pub git_state: Option<GitState>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GitState {
|
||||
pub remote_url: Option<String>,
|
||||
pub head_sha: Option<String>,
|
||||
pub current_branch: Option<String>,
|
||||
pub diff: Option<String>,
|
||||
}
|
||||
|
||||
/// A thread of conversation with the LLM.
|
||||
pub struct Thread {
|
||||
id: ThreadId,
|
||||
@@ -80,6 +106,8 @@ pub struct Thread {
|
||||
tool_use: ToolUseState,
|
||||
scripting_session: Entity<ScriptingSession>,
|
||||
scripting_tool_use: ToolUseState,
|
||||
initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
|
||||
cumulative_token_usage: TokenUsage,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
@@ -89,8 +117,6 @@ impl Thread {
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
|
||||
|
||||
Self {
|
||||
id: ThreadId::new(),
|
||||
updated_at: Utc::now(),
|
||||
@@ -102,42 +128,52 @@ impl Thread {
|
||||
context_by_message: HashMap::default(),
|
||||
completion_count: 0,
|
||||
pending_completions: Vec::new(),
|
||||
project,
|
||||
project: project.clone(),
|
||||
prompt_builder,
|
||||
tools,
|
||||
tool_use: ToolUseState::new(),
|
||||
scripting_session,
|
||||
scripting_session: cx.new(|cx| ScriptingSession::new(project.clone(), cx)),
|
||||
scripting_tool_use: ToolUseState::new(),
|
||||
initial_project_snapshot: {
|
||||
let project_snapshot = Self::project_snapshot(project, cx);
|
||||
cx.foreground_executor()
|
||||
.spawn(async move { Some(project_snapshot.await) })
|
||||
.shared()
|
||||
},
|
||||
cumulative_token_usage: TokenUsage::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_saved(
|
||||
pub fn deserialize(
|
||||
id: ThreadId,
|
||||
saved: SavedThread,
|
||||
serialized: SerializedThread,
|
||||
project: Entity<Project>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let next_message_id = MessageId(
|
||||
saved
|
||||
serialized
|
||||
.messages
|
||||
.last()
|
||||
.map(|message| message.id.0 + 1)
|
||||
.unwrap_or(0),
|
||||
);
|
||||
let tool_use =
|
||||
ToolUseState::from_saved_messages(&saved.messages, |name| name != ScriptingTool::NAME);
|
||||
let tool_use = ToolUseState::from_serialized_messages(&serialized.messages, |name| {
|
||||
name != ScriptingTool::NAME
|
||||
});
|
||||
let scripting_tool_use =
|
||||
ToolUseState::from_saved_messages(&saved.messages, |name| name == ScriptingTool::NAME);
|
||||
ToolUseState::from_serialized_messages(&serialized.messages, |name| {
|
||||
name == ScriptingTool::NAME
|
||||
});
|
||||
let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
|
||||
|
||||
Self {
|
||||
id,
|
||||
updated_at: saved.updated_at,
|
||||
summary: Some(saved.summary),
|
||||
updated_at: serialized.updated_at,
|
||||
summary: Some(serialized.summary),
|
||||
pending_summary: Task::ready(None),
|
||||
messages: saved
|
||||
messages: serialized
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|message| Message {
|
||||
@@ -157,6 +193,9 @@ impl Thread {
|
||||
tool_use,
|
||||
scripting_session,
|
||||
scripting_tool_use,
|
||||
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
|
||||
// TODO: persist token usage?
|
||||
cumulative_token_usage: TokenUsage::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +238,7 @@ impl Thread {
|
||||
}
|
||||
|
||||
pub fn is_streaming(&self) -> bool {
|
||||
!self.pending_completions.is_empty()
|
||||
!self.pending_completions.is_empty() || !self.all_tools_finished()
|
||||
}
|
||||
|
||||
pub fn tools(&self) -> &Arc<ToolWorkingSet> {
|
||||
@@ -344,6 +383,47 @@ impl Thread {
|
||||
text
|
||||
}
|
||||
|
||||
/// Serializes this thread into a format for storage or telemetry.
|
||||
pub fn serialize(&self, cx: &mut Context<Self>) -> Task<Result<SerializedThread>> {
|
||||
let initial_project_snapshot = self.initial_project_snapshot.clone();
|
||||
cx.spawn(|this, cx| async move {
|
||||
let initial_project_snapshot = initial_project_snapshot.await;
|
||||
this.read_with(&cx, |this, _| SerializedThread {
|
||||
summary: this.summary_or_default(),
|
||||
updated_at: this.updated_at(),
|
||||
messages: this
|
||||
.messages()
|
||||
.map(|message| SerializedMessage {
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
text: message.text.clone(),
|
||||
tool_uses: this
|
||||
.tool_uses_for_message(message.id)
|
||||
.into_iter()
|
||||
.chain(this.scripting_tool_uses_for_message(message.id))
|
||||
.map(|tool_use| SerializedToolUse {
|
||||
id: tool_use.id,
|
||||
name: tool_use.name,
|
||||
input: tool_use.input,
|
||||
})
|
||||
.collect(),
|
||||
tool_results: this
|
||||
.tool_results_for_message(message.id)
|
||||
.into_iter()
|
||||
.chain(this.scripting_tool_results_for_message(message.id))
|
||||
.map(|tool_result| SerializedToolResult {
|
||||
tool_use_id: tool_result.tool_use_id.clone(),
|
||||
is_error: tool_result.is_error,
|
||||
content: tool_result.content.clone(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
initial_project_snapshot,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_to_model(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
@@ -384,8 +464,14 @@ impl Thread {
|
||||
let worktree_root_names = self
|
||||
.project
|
||||
.read(cx)
|
||||
.worktree_root_names(cx)
|
||||
.map(ToString::to_string)
|
||||
.visible_worktrees(cx)
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
AssistantSystemPromptWorktree {
|
||||
root_name: worktree.root_name().into(),
|
||||
abs_path: worktree.abs_path(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let system_prompt = self
|
||||
.prompt_builder
|
||||
@@ -483,6 +569,7 @@ impl Thread {
|
||||
let stream_completion = async {
|
||||
let mut events = stream.await?;
|
||||
let mut stop_reason = StopReason::EndTurn;
|
||||
let mut current_token_usage = TokenUsage::default();
|
||||
|
||||
while let Some(event) = events.next().await {
|
||||
let event = event?;
|
||||
@@ -495,6 +582,12 @@ impl Thread {
|
||||
LanguageModelCompletionEvent::Stop(reason) => {
|
||||
stop_reason = reason;
|
||||
}
|
||||
LanguageModelCompletionEvent::UsageUpdate(token_usage) => {
|
||||
thread.cumulative_token_usage =
|
||||
thread.cumulative_token_usage.clone() + token_usage.clone()
|
||||
- current_token_usage.clone();
|
||||
current_token_usage = token_usage;
|
||||
}
|
||||
LanguageModelCompletionEvent::Text(chunk) => {
|
||||
if let Some(last_message) = thread.messages.last_mut() {
|
||||
if last_message.role == Role::Assistant {
|
||||
@@ -788,6 +881,185 @@ impl Thread {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Reports feedback about the thread and stores it in our telemetry backend.
|
||||
pub fn report_feedback(&self, is_positive: bool, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
let final_project_snapshot = Self::project_snapshot(self.project.clone(), cx);
|
||||
let serialized_thread = self.serialize(cx);
|
||||
let thread_id = self.id().clone();
|
||||
let client = self.project.read(cx).client();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let final_project_snapshot = final_project_snapshot.await;
|
||||
let serialized_thread = serialized_thread.await?;
|
||||
let thread_data =
|
||||
serde_json::to_value(serialized_thread).unwrap_or_else(|_| serde_json::Value::Null);
|
||||
|
||||
let rating = if is_positive { "positive" } else { "negative" };
|
||||
telemetry::event!(
|
||||
"Assistant Thread Rated",
|
||||
rating,
|
||||
thread_id,
|
||||
thread_data,
|
||||
final_project_snapshot
|
||||
);
|
||||
client.telemetry().flush_events();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a snapshot of the current project state including git information and unsaved buffers.
|
||||
fn project_snapshot(
|
||||
project: Entity<Project>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Arc<ProjectSnapshot>> {
|
||||
let worktree_snapshots: Vec<_> = project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.map(|worktree| Self::worktree_snapshot(worktree, cx))
|
||||
.collect();
|
||||
|
||||
cx.spawn(move |_, cx| async move {
|
||||
let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
|
||||
|
||||
let mut unsaved_buffers = Vec::new();
|
||||
cx.update(|app_cx| {
|
||||
let buffer_store = project.read(app_cx).buffer_store();
|
||||
for buffer_handle in buffer_store.read(app_cx).buffers() {
|
||||
let buffer = buffer_handle.read(app_cx);
|
||||
if buffer.is_dirty() {
|
||||
if let Some(file) = buffer.file() {
|
||||
let path = file.path().to_string_lossy().to_string();
|
||||
unsaved_buffers.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
Arc::new(ProjectSnapshot {
|
||||
worktree_snapshots,
|
||||
unsaved_buffer_paths: unsaved_buffers,
|
||||
timestamp: Utc::now(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn worktree_snapshot(worktree: Entity<project::Worktree>, cx: &App) -> Task<WorktreeSnapshot> {
|
||||
cx.spawn(move |cx| async move {
|
||||
// Get worktree path and snapshot
|
||||
let worktree_info = cx.update(|app_cx| {
|
||||
let worktree = worktree.read(app_cx);
|
||||
let path = worktree.abs_path().to_string_lossy().to_string();
|
||||
let snapshot = worktree.snapshot();
|
||||
(path, snapshot)
|
||||
});
|
||||
|
||||
let Ok((worktree_path, snapshot)) = worktree_info else {
|
||||
return WorktreeSnapshot {
|
||||
worktree_path: String::new(),
|
||||
git_state: None,
|
||||
};
|
||||
};
|
||||
|
||||
// Extract git information
|
||||
let git_state = match snapshot.repositories().first() {
|
||||
None => None,
|
||||
Some(repo_entry) => {
|
||||
// Get branch information
|
||||
let current_branch = repo_entry.branch().map(|branch| branch.name.to_string());
|
||||
|
||||
// Get repository info
|
||||
let repo_result = worktree.read_with(&cx, |worktree, _cx| {
|
||||
if let project::Worktree::Local(local_worktree) = &worktree {
|
||||
local_worktree.get_local_repo(repo_entry).map(|local_repo| {
|
||||
let repo = local_repo.repo();
|
||||
(repo.remote_url("origin"), repo.head_sha(), repo.clone())
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
match repo_result {
|
||||
Ok(Some((remote_url, head_sha, repository))) => {
|
||||
// Get diff asynchronously
|
||||
let diff = repository
|
||||
.diff(git::repository::DiffType::HeadToWorktree, cx)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
Some(GitState {
|
||||
remote_url,
|
||||
head_sha,
|
||||
current_branch,
|
||||
diff,
|
||||
})
|
||||
}
|
||||
Err(_) | Ok(None) => None,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
WorktreeSnapshot {
|
||||
worktree_path,
|
||||
git_state,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_markdown(&self) -> Result<String> {
|
||||
let mut markdown = Vec::new();
|
||||
|
||||
if let Some(summary) = self.summary() {
|
||||
writeln!(markdown, "# {summary}\n")?;
|
||||
};
|
||||
|
||||
for message in self.messages() {
|
||||
writeln!(
|
||||
markdown,
|
||||
"## {role}\n",
|
||||
role = match message.role {
|
||||
Role::User => "User",
|
||||
Role::Assistant => "Assistant",
|
||||
Role::System => "System",
|
||||
}
|
||||
)?;
|
||||
writeln!(markdown, "{}\n", message.text)?;
|
||||
|
||||
for tool_use in self.tool_uses_for_message(message.id) {
|
||||
writeln!(
|
||||
markdown,
|
||||
"**Use Tool: {} ({})**",
|
||||
tool_use.name, tool_use.id
|
||||
)?;
|
||||
writeln!(markdown, "```json")?;
|
||||
writeln!(
|
||||
markdown,
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&tool_use.input)?
|
||||
)?;
|
||||
writeln!(markdown, "```")?;
|
||||
}
|
||||
|
||||
for tool_result in self.tool_results_for_message(message.id) {
|
||||
write!(markdown, "**Tool Results: {}", tool_result.tool_use_id)?;
|
||||
if tool_result.is_error {
|
||||
write!(markdown, " (Error)")?;
|
||||
}
|
||||
|
||||
writeln!(markdown, "**\n")?;
|
||||
writeln!(markdown, "{}", tool_result.content)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&markdown).to_string())
|
||||
}
|
||||
|
||||
pub fn cumulative_token_usage(&self) -> TokenUsage {
|
||||
self.cumulative_token_usage.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -7,7 +7,7 @@ use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
|
||||
|
||||
use crate::history_store::{HistoryEntry, HistoryStore};
|
||||
use crate::thread_store::SavedThreadMetadata;
|
||||
use crate::thread_store::SerializedThreadMetadata;
|
||||
use crate::{AssistantPanel, RemoveSelectedThread};
|
||||
|
||||
pub struct ThreadHistory {
|
||||
@@ -221,14 +221,14 @@ impl Render for ThreadHistory {
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct PastThread {
|
||||
thread: SavedThreadMetadata,
|
||||
thread: SerializedThreadMetadata,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl PastThread {
|
||||
pub fn new(
|
||||
thread: SavedThreadMetadata,
|
||||
thread: SerializedThreadMetadata,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
selected: bool,
|
||||
) -> Self {
|
||||
|
||||
@@ -20,7 +20,7 @@ use prompt_store::PromptBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::thread::{MessageId, Thread, ThreadId};
|
||||
use crate::thread::{MessageId, ProjectSnapshot, Thread, ThreadId};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
ThreadsDatabase::init(cx);
|
||||
@@ -32,7 +32,7 @@ pub struct ThreadStore {
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
context_server_manager: Entity<ContextServerManager>,
|
||||
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
|
||||
threads: Vec<SavedThreadMetadata>,
|
||||
threads: Vec<SerializedThreadMetadata>,
|
||||
}
|
||||
|
||||
impl ThreadStore {
|
||||
@@ -70,13 +70,13 @@ impl ThreadStore {
|
||||
self.threads.len()
|
||||
}
|
||||
|
||||
pub fn threads(&self) -> Vec<SavedThreadMetadata> {
|
||||
pub fn threads(&self) -> Vec<SerializedThreadMetadata> {
|
||||
let mut threads = self.threads.iter().cloned().collect::<Vec<_>>();
|
||||
threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.updated_at));
|
||||
threads
|
||||
}
|
||||
|
||||
pub fn recent_threads(&self, limit: usize) -> Vec<SavedThreadMetadata> {
|
||||
pub fn recent_threads(&self, limit: usize) -> Vec<SerializedThreadMetadata> {
|
||||
self.threads().into_iter().take(limit).collect()
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ impl ThreadStore {
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
cx.new(|cx| {
|
||||
Thread::from_saved(
|
||||
Thread::deserialize(
|
||||
id.clone(),
|
||||
thread,
|
||||
this.project.clone(),
|
||||
@@ -121,53 +121,14 @@ impl ThreadStore {
|
||||
}
|
||||
|
||||
pub fn save_thread(&self, thread: &Entity<Thread>, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
let (metadata, thread) = thread.update(cx, |thread, _cx| {
|
||||
let id = thread.id().clone();
|
||||
let thread = SavedThread {
|
||||
summary: thread.summary_or_default(),
|
||||
updated_at: thread.updated_at(),
|
||||
messages: thread
|
||||
.messages()
|
||||
.map(|message| {
|
||||
let all_tool_uses = thread
|
||||
.tool_uses_for_message(message.id)
|
||||
.into_iter()
|
||||
.chain(thread.scripting_tool_uses_for_message(message.id))
|
||||
.map(|tool_use| SavedToolUse {
|
||||
id: tool_use.id,
|
||||
name: tool_use.name,
|
||||
input: tool_use.input,
|
||||
})
|
||||
.collect();
|
||||
let all_tool_results = thread
|
||||
.tool_results_for_message(message.id)
|
||||
.into_iter()
|
||||
.chain(thread.scripting_tool_results_for_message(message.id))
|
||||
.map(|tool_result| SavedToolResult {
|
||||
tool_use_id: tool_result.tool_use_id.clone(),
|
||||
is_error: tool_result.is_error,
|
||||
content: tool_result.content.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
SavedMessage {
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
text: message.text.clone(),
|
||||
tool_uses: all_tool_uses,
|
||||
tool_results: all_tool_results,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
(id, thread)
|
||||
});
|
||||
let (metadata, serialized_thread) =
|
||||
thread.update(cx, |thread, cx| (thread.id().clone(), thread.serialize(cx)));
|
||||
|
||||
let database_future = ThreadsDatabase::global_future(cx);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let serialized_thread = serialized_thread.await?;
|
||||
let database = database_future.await.map_err(|err| anyhow!(err))?;
|
||||
database.save_thread(metadata, thread).await?;
|
||||
database.save_thread(metadata, serialized_thread).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?.await
|
||||
})
|
||||
@@ -270,39 +231,41 @@ impl ThreadStore {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SavedThreadMetadata {
|
||||
pub struct SerializedThreadMetadata {
|
||||
pub id: ThreadId,
|
||||
pub summary: SharedString,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedThread {
|
||||
pub struct SerializedThread {
|
||||
pub summary: SharedString,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub messages: Vec<SerializedMessage>,
|
||||
#[serde(default)]
|
||||
pub initial_project_snapshot: Option<Arc<ProjectSnapshot>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
pub struct SerializedMessage {
|
||||
pub id: MessageId,
|
||||
pub role: Role,
|
||||
pub text: String,
|
||||
#[serde(default)]
|
||||
pub tool_uses: Vec<SavedToolUse>,
|
||||
pub tool_uses: Vec<SerializedToolUse>,
|
||||
#[serde(default)]
|
||||
pub tool_results: Vec<SavedToolResult>,
|
||||
pub tool_results: Vec<SerializedToolResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SavedToolUse {
|
||||
pub struct SerializedToolUse {
|
||||
pub id: LanguageModelToolUseId,
|
||||
pub name: SharedString,
|
||||
pub input: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SavedToolResult {
|
||||
pub struct SerializedToolResult {
|
||||
pub tool_use_id: LanguageModelToolUseId,
|
||||
pub is_error: bool,
|
||||
pub content: Arc<str>,
|
||||
@@ -317,7 +280,7 @@ impl Global for GlobalThreadsDatabase {}
|
||||
pub(crate) struct ThreadsDatabase {
|
||||
executor: BackgroundExecutor,
|
||||
env: heed::Env,
|
||||
threads: Database<SerdeBincode<ThreadId>, SerdeJson<SavedThread>>,
|
||||
threads: Database<SerdeBincode<ThreadId>, SerdeJson<SerializedThread>>,
|
||||
}
|
||||
|
||||
impl ThreadsDatabase {
|
||||
@@ -364,7 +327,7 @@ impl ThreadsDatabase {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_threads(&self) -> Task<Result<Vec<SavedThreadMetadata>>> {
|
||||
pub fn list_threads(&self) -> Task<Result<Vec<SerializedThreadMetadata>>> {
|
||||
let env = self.env.clone();
|
||||
let threads = self.threads;
|
||||
|
||||
@@ -373,7 +336,7 @@ impl ThreadsDatabase {
|
||||
let mut iter = threads.iter(&txn)?;
|
||||
let mut threads = Vec::new();
|
||||
while let Some((key, value)) = iter.next().transpose()? {
|
||||
threads.push(SavedThreadMetadata {
|
||||
threads.push(SerializedThreadMetadata {
|
||||
id: key,
|
||||
summary: value.summary,
|
||||
updated_at: value.updated_at,
|
||||
@@ -384,7 +347,7 @@ impl ThreadsDatabase {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn try_find_thread(&self, id: ThreadId) -> Task<Result<Option<SavedThread>>> {
|
||||
pub fn try_find_thread(&self, id: ThreadId) -> Task<Result<Option<SerializedThread>>> {
|
||||
let env = self.env.clone();
|
||||
let threads = self.threads;
|
||||
|
||||
@@ -395,7 +358,7 @@ impl ThreadsDatabase {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save_thread(&self, id: ThreadId, thread: SavedThread) -> Task<Result<()>> {
|
||||
pub fn save_thread(&self, id: ThreadId, thread: SerializedThread) -> Task<Result<()>> {
|
||||
let env = self.env.clone();
|
||||
let threads = self.threads;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use gpui::Entity;
|
||||
use scripting_tool::ScriptingTool;
|
||||
use ui::{prelude::*, ContextMenu, IconButtonShape, PopoverMenu, Tooltip};
|
||||
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
|
||||
|
||||
pub struct ToolSelector {
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
@@ -24,22 +24,16 @@ impl ToolSelector {
|
||||
let tools_by_source = self.tools.tools_by_source(cx);
|
||||
|
||||
let all_tools_enabled = self.tools.are_all_tools_enabled();
|
||||
menu = menu.header("Tools").toggleable_entry(
|
||||
"All Tools",
|
||||
all_tools_enabled,
|
||||
icon_position,
|
||||
None,
|
||||
{
|
||||
let tools = self.tools.clone();
|
||||
move |_window, cx| {
|
||||
if all_tools_enabled {
|
||||
tools.disable_all_tools(cx);
|
||||
} else {
|
||||
tools.enable_all_tools();
|
||||
}
|
||||
menu = menu.toggleable_entry("All Tools", all_tools_enabled, icon_position, None, {
|
||||
let tools = self.tools.clone();
|
||||
move |_window, cx| {
|
||||
if all_tools_enabled {
|
||||
tools.disable_all_tools(cx);
|
||||
} else {
|
||||
tools.enable_all_tools();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
for (source, tools) in tools_by_source {
|
||||
let mut tools = tools
|
||||
@@ -63,7 +57,7 @@ impl ToolSelector {
|
||||
}
|
||||
|
||||
menu = match &source {
|
||||
ToolSource::Native => menu.header("Zed"),
|
||||
ToolSource::Native => menu.separator().header("Zed Tools"),
|
||||
ToolSource::ContextServer { id } => {
|
||||
let all_tools_from_source_enabled =
|
||||
self.tools.are_all_tools_from_source_enabled(&source);
|
||||
@@ -124,7 +118,6 @@ impl Render for ToolSelector {
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("tool-selector-button", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted),
|
||||
Tooltip::text("Customize Tools"),
|
||||
|
||||
@@ -11,7 +11,7 @@ use language_model::{
|
||||
};
|
||||
|
||||
use crate::thread::MessageId;
|
||||
use crate::thread_store::SavedMessage;
|
||||
use crate::thread_store::SerializedMessage;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ToolUse {
|
||||
@@ -46,11 +46,11 @@ impl ToolUseState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a [`ToolUseState`] from the given list of [`SavedMessage`]s.
|
||||
/// Constructs a [`ToolUseState`] from the given list of [`SerializedMessage`]s.
|
||||
///
|
||||
/// Accepts a function to filter the tools that should be used to populate the state.
|
||||
pub fn from_saved_messages(
|
||||
messages: &[SavedMessage],
|
||||
pub fn from_serialized_messages(
|
||||
messages: &[SerializedMessage],
|
||||
mut filter_by_tool_name: impl FnMut(&str) -> bool,
|
||||
) -> Self {
|
||||
let mut this = Self::new();
|
||||
|
||||
@@ -126,7 +126,13 @@ impl RenderOnce for ContextPill {
|
||||
h_flex()
|
||||
.id("context-data")
|
||||
.gap_1()
|
||||
.child(Label::new(context.name.clone()).size(LabelSize::Small))
|
||||
.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(
|
||||
@@ -174,21 +180,22 @@ impl RenderOnce for ContextPill {
|
||||
})
|
||||
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
|
||||
.child(
|
||||
Label::new(name.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
div().px_0p5().max_w_64().child(
|
||||
Label::new(name.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().px_0p5().child(
|
||||
Label::new(match kind {
|
||||
ContextKind::File => "Active Tab",
|
||||
ContextKind::Thread
|
||||
| ContextKind::Directory
|
||||
| ContextKind::FetchedUrl => "Active",
|
||||
})
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
Label::new(match kind {
|
||||
ContextKind::File => "Active Tab",
|
||||
ContextKind::Thread | ContextKind::Directory | ContextKind::FetchedUrl => {
|
||||
"Active"
|
||||
}
|
||||
})
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::Plus)
|
||||
|
||||
@@ -2254,6 +2254,7 @@ impl AssistantContext {
|
||||
);
|
||||
}
|
||||
LanguageModelCompletionEvent::ToolUse(_) => {}
|
||||
LanguageModelCompletionEvent::UsageUpdate(_) => {}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ impl SlashCommandCompletionProvider {
|
||||
name_range: Range<Anchor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
) -> Task<Result<Option<Vec<project::Completion>>>> {
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let candidates = slash_commands
|
||||
.command_names(cx)
|
||||
@@ -71,65 +71,67 @@ impl SlashCommandCompletionProvider {
|
||||
.await;
|
||||
|
||||
cx.update(|_, cx| {
|
||||
matches
|
||||
.into_iter()
|
||||
.filter_map(|mat| {
|
||||
let command = slash_commands.command(&mat.string, cx)?;
|
||||
let mut new_text = mat.string.clone();
|
||||
let requires_argument = command.requires_argument();
|
||||
let accepts_arguments = command.accepts_arguments();
|
||||
if requires_argument || accepts_arguments {
|
||||
new_text.push(' ');
|
||||
}
|
||||
Some(
|
||||
matches
|
||||
.into_iter()
|
||||
.filter_map(|mat| {
|
||||
let command = slash_commands.command(&mat.string, cx)?;
|
||||
let mut new_text = mat.string.clone();
|
||||
let requires_argument = command.requires_argument();
|
||||
let accepts_arguments = command.accepts_arguments();
|
||||
if requires_argument || accepts_arguments {
|
||||
new_text.push(' ');
|
||||
}
|
||||
|
||||
let confirm =
|
||||
editor
|
||||
.clone()
|
||||
.zip(workspace.clone())
|
||||
.map(|(editor, workspace)| {
|
||||
let command_name = mat.string.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
Arc::new(
|
||||
move |intent: CompletionIntent,
|
||||
window: &mut Window,
|
||||
cx: &mut App| {
|
||||
if !requires_argument
|
||||
&& (!accepts_arguments || intent.is_complete())
|
||||
{
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
&[],
|
||||
true,
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
false
|
||||
} else {
|
||||
requires_argument || accepts_arguments
|
||||
}
|
||||
},
|
||||
) as Arc<_>
|
||||
});
|
||||
Some(project::Completion {
|
||||
old_range: name_range.clone(),
|
||||
documentation: Some(CompletionDocumentation::SingleLine(
|
||||
command.description().into(),
|
||||
)),
|
||||
new_text,
|
||||
label: command.label(cx),
|
||||
confirm,
|
||||
source: CompletionSource::Custom,
|
||||
let confirm =
|
||||
editor
|
||||
.clone()
|
||||
.zip(workspace.clone())
|
||||
.map(|(editor, workspace)| {
|
||||
let command_name = mat.string.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
Arc::new(
|
||||
move |intent: CompletionIntent,
|
||||
window: &mut Window,
|
||||
cx: &mut App| {
|
||||
if !requires_argument
|
||||
&& (!accepts_arguments || intent.is_complete())
|
||||
{
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
&[],
|
||||
true,
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
false
|
||||
} else {
|
||||
requires_argument || accepts_arguments
|
||||
}
|
||||
},
|
||||
) as Arc<_>
|
||||
});
|
||||
Some(project::Completion {
|
||||
old_range: name_range.clone(),
|
||||
documentation: Some(CompletionDocumentation::SingleLine(
|
||||
command.description().into(),
|
||||
)),
|
||||
new_text,
|
||||
label: command.label(cx),
|
||||
confirm,
|
||||
source: CompletionSource::Custom,
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -143,7 +145,7 @@ impl SlashCommandCompletionProvider {
|
||||
last_argument_range: Range<Anchor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
) -> Task<Result<Option<Vec<project::Completion>>>> {
|
||||
let new_cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let mut flag = self.cancel_flag.lock();
|
||||
flag.store(true, SeqCst);
|
||||
@@ -161,27 +163,28 @@ impl SlashCommandCompletionProvider {
|
||||
let workspace = self.workspace.clone();
|
||||
let arguments = arguments.to_vec();
|
||||
cx.background_spawn(async move {
|
||||
Ok(completions
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|new_argument| {
|
||||
let confirm =
|
||||
editor
|
||||
.clone()
|
||||
.zip(workspace.clone())
|
||||
.map(|(editor, workspace)| {
|
||||
Arc::new({
|
||||
let mut completed_arguments = arguments.clone();
|
||||
if new_argument.replace_previous_arguments {
|
||||
completed_arguments.clear();
|
||||
} else {
|
||||
completed_arguments.pop();
|
||||
}
|
||||
completed_arguments.push(new_argument.new_text.clone());
|
||||
Ok(Some(
|
||||
completions
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|new_argument| {
|
||||
let confirm =
|
||||
editor
|
||||
.clone()
|
||||
.zip(workspace.clone())
|
||||
.map(|(editor, workspace)| {
|
||||
Arc::new({
|
||||
let mut completed_arguments = arguments.clone();
|
||||
if new_argument.replace_previous_arguments {
|
||||
completed_arguments.clear();
|
||||
} else {
|
||||
completed_arguments.pop();
|
||||
}
|
||||
completed_arguments.push(new_argument.new_text.clone());
|
||||
|
||||
let command_range = command_range.clone();
|
||||
let command_name = command_name.clone();
|
||||
move |intent: CompletionIntent,
|
||||
let command_range = command_range.clone();
|
||||
let command_name = command_name.clone();
|
||||
move |intent: CompletionIntent,
|
||||
window: &mut Window,
|
||||
cx: &mut App| {
|
||||
if new_argument.after_completion.run()
|
||||
@@ -205,31 +208,32 @@ impl SlashCommandCompletionProvider {
|
||||
!new_argument.after_completion.run()
|
||||
}
|
||||
}
|
||||
}) as Arc<_>
|
||||
});
|
||||
}) as Arc<_>
|
||||
});
|
||||
|
||||
let mut new_text = new_argument.new_text.clone();
|
||||
if new_argument.after_completion == AfterCompletion::Continue {
|
||||
new_text.push(' ');
|
||||
}
|
||||
let mut new_text = new_argument.new_text.clone();
|
||||
if new_argument.after_completion == AfterCompletion::Continue {
|
||||
new_text.push(' ');
|
||||
}
|
||||
|
||||
project::Completion {
|
||||
old_range: if new_argument.replace_previous_arguments {
|
||||
argument_range.clone()
|
||||
} else {
|
||||
last_argument_range.clone()
|
||||
},
|
||||
label: new_argument.label,
|
||||
new_text,
|
||||
documentation: None,
|
||||
confirm,
|
||||
source: CompletionSource::Custom,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
project::Completion {
|
||||
old_range: if new_argument.replace_previous_arguments {
|
||||
argument_range.clone()
|
||||
} else {
|
||||
last_argument_range.clone()
|
||||
},
|
||||
label: new_argument.label,
|
||||
new_text,
|
||||
documentation: None,
|
||||
confirm,
|
||||
source: CompletionSource::Custom,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
})
|
||||
} else {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
Task::ready(Ok(Some(Vec::new())))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,7 +246,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
_: editor::CompletionContext,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
) -> Task<Result<Option<Vec<project::Completion>>>> {
|
||||
let Some((name, arguments, command_range, last_argument_range)) =
|
||||
buffer.update(cx, |buffer, _cx| {
|
||||
let position = buffer_position.to_point(buffer);
|
||||
@@ -286,7 +290,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
Some((name, arguments, command_range, last_argument_range))
|
||||
})
|
||||
else {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
return Task::ready(Ok(Some(Vec::new())));
|
||||
};
|
||||
|
||||
if let Some((arguments, argument_range)) = arguments {
|
||||
|
||||
@@ -16,15 +16,21 @@ anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
settings.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rand.workspace = true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod bash_tool;
|
||||
mod delete_path_tool;
|
||||
mod diagnostics_tool;
|
||||
mod edit_files_tool;
|
||||
mod list_directory_tool;
|
||||
mod now_tool;
|
||||
@@ -12,6 +13,7 @@ use gpui::App;
|
||||
|
||||
use crate::bash_tool::BashTool;
|
||||
use crate::delete_path_tool::DeletePathTool;
|
||||
use crate::diagnostics_tool::DiagnosticsTool;
|
||||
use crate::edit_files_tool::EditFilesTool;
|
||||
use crate::list_directory_tool::ListDirectoryTool;
|
||||
use crate::now_tool::NowTool;
|
||||
@@ -21,14 +23,16 @@ use crate::regex_search::RegexSearchTool;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
assistant_tool::init(cx);
|
||||
crate::edit_files_tool::log::init(cx);
|
||||
|
||||
let registry = ToolRegistry::global(cx);
|
||||
registry.register_tool(NowTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(EditFilesTool);
|
||||
registry.register_tool(PathSearchTool);
|
||||
registry.register_tool(RegexSearchTool);
|
||||
registry.register_tool(DeletePathTool);
|
||||
registry.register_tool(BashTool);
|
||||
registry.register_tool(DeletePathTool);
|
||||
registry.register_tool(DiagnosticsTool);
|
||||
registry.register_tool(EditFilesTool);
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(NowTool);
|
||||
registry.register_tool(PathSearchTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
registry.register_tool(RegexSearchTool);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
@@ -6,11 +6,14 @@ use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use util::command::new_smol_command;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct BashToolInput {
|
||||
/// The bash command to execute as a one-liner.
|
||||
command: String,
|
||||
/// Working directory for the command. This must be one of the root directories of the project.
|
||||
cd: String,
|
||||
}
|
||||
|
||||
pub struct BashTool;
|
||||
@@ -33,7 +36,7 @@ impl Tool for BashTool {
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input: BashToolInput = match serde_json::from_value(input) {
|
||||
@@ -41,23 +44,31 @@ impl Tool for BashTool {
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
cx.spawn(|_| async move {
|
||||
// Add 2>&1 to merge stderr into stdout for proper interleaving
|
||||
let command = format!("{} 2>&1", input.command);
|
||||
let Some(worktree) = project.read(cx).worktree_for_root_name(&input.cd, cx) else {
|
||||
return Task::ready(Err(anyhow!("Working directory not found in the project")));
|
||||
};
|
||||
let working_directory = worktree.read(cx).abs_path();
|
||||
|
||||
// Spawn a blocking task to execute the command
|
||||
let output = futures::executor::block_on(async {
|
||||
std::process::Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg(&command)
|
||||
.output()
|
||||
.map_err(|err| anyhow!("Failed to execute bash command: {}", err))
|
||||
})?;
|
||||
cx.spawn(|_| async move {
|
||||
// Add 2>&1 to merge stderr into stdout for proper interleaving.
|
||||
let command = format!("({}) 2>&1", input.command);
|
||||
|
||||
let output = new_smol_command("bash")
|
||||
.arg("-c")
|
||||
.arg(&command)
|
||||
.current_dir(working_directory)
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to execute bash command")?;
|
||||
|
||||
let output_string = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
|
||||
if output.status.success() {
|
||||
Ok(output_string)
|
||||
if output_string.is_empty() {
|
||||
Ok("Command executed successfully.".to_string())
|
||||
} else {
|
||||
Ok(output_string)
|
||||
}
|
||||
} else {
|
||||
Ok(format!(
|
||||
"Command failed with exit code {}\n{}",
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
Executes a bash one-liner and returns the combined output. This tool spawns a bash process, combines stdout and stderr into one interleaved stream as they are produced (preserving the order of writes), and captures that stream into a string which is returned. Use this tool when you need to run shell commands to get information about the system or process files.
|
||||
Executes a bash one-liner and returns the combined output.
|
||||
|
||||
This tool spawns a bash process, combines stdout and stderr into one interleaved stream as they are produced (preserving the order of writes), and captures that stream into a string which is returned.
|
||||
|
||||
Make sure you use the `cd` parameter to navigate to one of the root directories of the project. NEVER do it as part of the `command` itself, otherwise it will error.
|
||||
|
||||
Remember that each invocation of this tool will spawn a new bash process, so you can't rely on any state from previous invocations.
|
||||
|
||||
127
crates/assistant_tools/src/diagnostics_tool.rs
Normal file
127
crates/assistant_tools/src/diagnostics_tool.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::{DiagnosticSeverity, OffsetRangeExt};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DiagnosticsToolInput {
|
||||
/// The path to get diagnostics for. If not provided, returns a project-wide summary.
|
||||
///
|
||||
/// This path should never be absolute, and the first component
|
||||
/// of the path should always be a root directory in a project.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - lorem
|
||||
/// - ipsum
|
||||
///
|
||||
/// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`.
|
||||
/// </example>
|
||||
pub path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub struct DiagnosticsTool;
|
||||
|
||||
impl Tool for DiagnosticsTool {
|
||||
fn name(&self) -> String {
|
||||
"diagnostics".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./diagnostics_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(DiagnosticsToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input = match serde_json::from_value::<DiagnosticsToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
if let Some(path) = input.path {
|
||||
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Could not find path in project")));
|
||||
};
|
||||
let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
let mut output = String::new();
|
||||
let buffer = buffer.await?;
|
||||
let snapshot = buffer.read_with(&cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||
let entry = &group.entries[group.primary_ix];
|
||||
let range = entry.range.to_point(&snapshot);
|
||||
let severity = match entry.diagnostic.severity {
|
||||
DiagnosticSeverity::ERROR => "error",
|
||||
DiagnosticSeverity::WARNING => "warning",
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
"{} at line {}: {}",
|
||||
severity,
|
||||
range.start.row + 1,
|
||||
entry.diagnostic.message
|
||||
)?;
|
||||
}
|
||||
|
||||
if output.is_empty() {
|
||||
Ok("File doesn't have errors or warnings!".to_string())
|
||||
} else {
|
||||
Ok(output)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let project = project.read(cx);
|
||||
let mut output = String::new();
|
||||
let mut has_diagnostics = false;
|
||||
|
||||
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
|
||||
if summary.error_count > 0 || summary.warning_count > 0 {
|
||||
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
has_diagnostics = true;
|
||||
output.push_str(&format!(
|
||||
"{}: {} error(s), {} warning(s)\n",
|
||||
Path::new(worktree.read(cx).root_name())
|
||||
.join(project_path.path)
|
||||
.display(),
|
||||
summary.error_count,
|
||||
summary.warning_count
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if has_diagnostics {
|
||||
Task::ready(Ok(output))
|
||||
} else {
|
||||
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
crates/assistant_tools/src/diagnostics_tool/description.md
Normal file
16
crates/assistant_tools/src/diagnostics_tool/description.md
Normal file
@@ -0,0 +1,16 @@
|
||||
Get errors and warnings for the project or a specific file.
|
||||
|
||||
This tool can be invoked after a series of edits to determine if further edits are necessary, or if the user asks to fix errors or warnings in their codebase.
|
||||
|
||||
When a path is provided, shows all diagnostics for that specific file.
|
||||
When no path is provided, shows a summary of error and warning counts for all files in the project.
|
||||
|
||||
<example>
|
||||
To get diagnostics for a specific file:
|
||||
{
|
||||
"path": "src/main.rs"
|
||||
}
|
||||
|
||||
To get a project-wide diagnostic summary:
|
||||
{}
|
||||
</example>
|
||||
@@ -1,33 +1,60 @@
|
||||
mod edit_action;
|
||||
pub mod log;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use assistant_tool::Tool;
|
||||
use collections::HashSet;
|
||||
use edit_action::{EditAction, EditActionParser};
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, Entity, Task};
|
||||
use gpui::{App, AsyncApp, Entity, Task};
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use project::{Project, ProjectPath};
|
||||
use log::{EditToolLog, EditToolRequestId};
|
||||
use project::{search::SearchQuery, Project};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
use util::paths::PathMatcher;
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EditFilesToolInput {
|
||||
/// High-level edit instructions. These will be interpreted by a smaller model,
|
||||
/// so explain the edits you want that model to make and to which files need changing.
|
||||
/// The description should be concise and clear. We will show this description to the user
|
||||
/// as well.
|
||||
/// High-level edit instructions. These will be interpreted by a smaller
|
||||
/// model, so explain the changes you want that model to make and which
|
||||
/// file paths need changing.
|
||||
///
|
||||
/// The description should be concise and clear. We will show this
|
||||
/// description to the user as well.
|
||||
///
|
||||
/// WARNING: When specifying which file paths need changing, you MUST
|
||||
/// start each path with one of the project's root directories.
|
||||
///
|
||||
/// WARNING: NEVER include code blocks or snippets in edit instructions.
|
||||
/// Only provide natural language descriptions of the changes needed! The tool will
|
||||
/// reject any instructions that contain code blocks or snippets.
|
||||
///
|
||||
/// The following examples assume we have two root directories in the project:
|
||||
/// - root-1
|
||||
/// - root-2
|
||||
///
|
||||
/// <example>
|
||||
/// If you want to rename a function you can say "Rename the function 'foo' to 'bar'".
|
||||
/// If you want to introduce a new quit function to kill the process, your
|
||||
/// instructions should be: "Add a new `quit` function to
|
||||
/// `root-1/src/main.rs` to kill the process".
|
||||
///
|
||||
/// Notice how the file path starts with root-1. Without that, the path
|
||||
/// would be ambiguous and the call would fail!
|
||||
/// </example>
|
||||
///
|
||||
/// <example>
|
||||
/// If you want to add a new function you can say "Add a new method to the `User` struct that prints the age".
|
||||
/// If you want to change documentation to always start with a capital
|
||||
/// letter, your instructions should be: "In `root-2/db.js`,
|
||||
/// `root-2/inMemory.js` and `root-2/sql.js`, change all the documentation
|
||||
/// to start with a capital letter".
|
||||
///
|
||||
/// Notice how we never specify code snippets in the instructions!
|
||||
/// </example>
|
||||
pub edit_instructions: String,
|
||||
}
|
||||
@@ -60,6 +87,65 @@ impl Tool for EditFilesTool {
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
match EditToolLog::try_global(cx) {
|
||||
Some(log) => {
|
||||
let req_id = log.update(cx, |log, cx| {
|
||||
log.new_request(input.edit_instructions.clone(), cx)
|
||||
});
|
||||
|
||||
let task =
|
||||
EditToolRequest::new(input, messages, project, Some((log.clone(), req_id)), cx);
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let result = task.await;
|
||||
|
||||
let str_result = match &result {
|
||||
Ok(out) => Ok(out.clone()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
};
|
||||
|
||||
log.update(&mut cx, |log, cx| {
|
||||
log.set_tool_output(req_id, str_result, cx)
|
||||
})
|
||||
.log_err();
|
||||
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
None => EditToolRequest::new(input, messages, project, None, cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EditToolRequest {
|
||||
parser: EditActionParser,
|
||||
changed_buffers: HashSet<Entity<language::Buffer>>,
|
||||
bad_searches: Vec<BadSearch>,
|
||||
project: Entity<Project>,
|
||||
log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DiffResult {
|
||||
BadSearch(BadSearch),
|
||||
Diff(language::Diff),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BadSearch {
|
||||
file_path: String,
|
||||
search: String,
|
||||
}
|
||||
|
||||
impl EditToolRequest {
|
||||
fn new(
|
||||
input: EditFilesToolInput,
|
||||
messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let Some(model) = model_registry.editor_model() else {
|
||||
return Task::ready(Err(anyhow!("No editor model configured")));
|
||||
@@ -82,110 +168,208 @@ impl Tool for EditFilesTool {
|
||||
});
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let request = LanguageModelRequest {
|
||||
let llm_request = LanguageModelRequest {
|
||||
messages,
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
temperature: Some(0.0),
|
||||
};
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
|
||||
let stream = model.stream_completion_text(request, &cx);
|
||||
let stream = model.stream_completion_text(llm_request, &cx);
|
||||
let mut chunks = stream.await?;
|
||||
|
||||
let mut changed_buffers = HashSet::default();
|
||||
let mut applied_edits = 0;
|
||||
|
||||
while let Some(chunk) = chunks.stream.next().await {
|
||||
for action in parser.parse_chunk(&chunk?) {
|
||||
let project_path = project.read_with(&cx, |project, cx| {
|
||||
let worktree_root_name = action
|
||||
.file_path()
|
||||
.components()
|
||||
.next()
|
||||
.context("Invalid path")?;
|
||||
let worktree = project
|
||||
.worktree_for_root_name(
|
||||
&worktree_root_name.as_os_str().to_string_lossy(),
|
||||
cx,
|
||||
)
|
||||
.context("Directory not found in project")?;
|
||||
anyhow::Ok(ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: Arc::from(
|
||||
action.file_path().strip_prefix(worktree_root_name).unwrap(),
|
||||
),
|
||||
})
|
||||
})??;
|
||||
|
||||
let buffer = project
|
||||
.update(&mut cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.await?;
|
||||
|
||||
let diff = buffer
|
||||
.read_with(&cx, |buffer, cx| {
|
||||
let new_text = match action {
|
||||
EditAction::Replace { old, new, .. } => {
|
||||
// TODO: Replace in background?
|
||||
buffer.text().replace(&old, &new)
|
||||
}
|
||||
EditAction::Write { content, .. } => content,
|
||||
};
|
||||
|
||||
buffer.diff(new_text, cx)
|
||||
})?
|
||||
.await;
|
||||
|
||||
let _clock =
|
||||
buffer.update(&mut cx, |buffer, cx| buffer.apply_diff(diff, cx))?;
|
||||
|
||||
changed_buffers.insert(buffer);
|
||||
|
||||
applied_edits += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let mut answer = match changed_buffers.len() {
|
||||
0 => "No files were edited.".to_string(),
|
||||
1 => "Successfully edited ".to_string(),
|
||||
_ => "Successfully edited these files:\n\n".to_string(),
|
||||
let mut request = Self {
|
||||
parser: EditActionParser::new(),
|
||||
changed_buffers: HashSet::default(),
|
||||
bad_searches: Vec::new(),
|
||||
project,
|
||||
log,
|
||||
};
|
||||
|
||||
// Save each buffer once at the end
|
||||
for buffer in changed_buffers {
|
||||
project
|
||||
.update(&mut cx, |project, cx| {
|
||||
if let Some(file) = buffer.read(&cx).file() {
|
||||
let _ = write!(&mut answer, "{}\n\n", &file.path().display());
|
||||
}
|
||||
|
||||
project.save_buffer(buffer, cx)
|
||||
})?
|
||||
.await?;
|
||||
while let Some(chunk) = chunks.stream.next().await {
|
||||
request.process_response_chunk(&chunk?, &mut cx).await?;
|
||||
}
|
||||
|
||||
let errors = parser.errors();
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(answer.trim_end().to_string())
|
||||
} else {
|
||||
let error_message = errors
|
||||
.iter()
|
||||
.map(|e| e.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
if applied_edits > 0 {
|
||||
Err(anyhow!(
|
||||
"Applied {} edit(s), but some blocks failed to parse:\n{}",
|
||||
applied_edits,
|
||||
error_message
|
||||
))
|
||||
} else {
|
||||
Err(anyhow!(error_message))
|
||||
}
|
||||
}
|
||||
request.finalize(&mut cx).await
|
||||
})
|
||||
}
|
||||
|
||||
async fn process_response_chunk(&mut self, chunk: &str, cx: &mut AsyncApp) -> Result<()> {
|
||||
let new_actions = self.parser.parse_chunk(chunk);
|
||||
|
||||
if let Some((ref log, req_id)) = self.log {
|
||||
log.update(cx, |log, cx| {
|
||||
log.push_editor_response_chunk(req_id, chunk, &new_actions, cx)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
for action in new_actions {
|
||||
self.apply_action(action, cx).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn apply_action(&mut self, action: EditAction, cx: &mut AsyncApp) -> Result<()> {
|
||||
let project_path = self.project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.find_project_path(action.file_path(), cx)
|
||||
.context("Path not found in project")
|
||||
})??;
|
||||
|
||||
let buffer = self
|
||||
.project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.await?;
|
||||
|
||||
let result = match action {
|
||||
EditAction::Replace {
|
||||
old,
|
||||
new,
|
||||
file_path,
|
||||
} => {
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(Self::replace_diff(old, new, file_path, snapshot))
|
||||
.await
|
||||
}
|
||||
EditAction::Write { content, .. } => Ok(DiffResult::Diff(
|
||||
buffer
|
||||
.read_with(cx, |buffer, cx| buffer.diff(content, cx))?
|
||||
.await,
|
||||
)),
|
||||
}?;
|
||||
|
||||
match result {
|
||||
DiffResult::BadSearch(invalid_replace) => {
|
||||
self.bad_searches.push(invalid_replace);
|
||||
}
|
||||
DiffResult::Diff(diff) => {
|
||||
let _clock = buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx))?;
|
||||
|
||||
self.changed_buffers.insert(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn replace_diff(
|
||||
old: String,
|
||||
new: String,
|
||||
file_path: std::path::PathBuf,
|
||||
snapshot: language::BufferSnapshot,
|
||||
) -> Result<DiffResult> {
|
||||
let query = SearchQuery::text(
|
||||
old.clone(),
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
PathMatcher::new(&[])?,
|
||||
PathMatcher::new(&[])?,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let matches = query.search(&snapshot, None).await;
|
||||
|
||||
if matches.is_empty() {
|
||||
return Ok(DiffResult::BadSearch(BadSearch {
|
||||
search: new.clone(),
|
||||
file_path: file_path.display().to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
let edit_range = matches[0].clone();
|
||||
let diff = language::text_diff(&old, &new);
|
||||
|
||||
let edits = diff
|
||||
.into_iter()
|
||||
.map(|(old_range, text)| {
|
||||
let start = edit_range.start + old_range.start;
|
||||
let end = edit_range.start + old_range.end;
|
||||
(start..end, text)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let diff = language::Diff {
|
||||
base_version: snapshot.version().clone(),
|
||||
line_ending: snapshot.line_ending(),
|
||||
edits,
|
||||
};
|
||||
|
||||
anyhow::Ok(DiffResult::Diff(diff))
|
||||
}
|
||||
|
||||
async fn finalize(self, cx: &mut AsyncApp) -> Result<String> {
|
||||
let mut answer = match self.changed_buffers.len() {
|
||||
0 => "No files were edited.".to_string(),
|
||||
1 => "Successfully edited ".to_string(),
|
||||
_ => "Successfully edited these files:\n\n".to_string(),
|
||||
};
|
||||
|
||||
// Save each buffer once at the end
|
||||
for buffer in self.changed_buffers {
|
||||
let (path, save_task) = self.project.update(cx, |project, cx| {
|
||||
let path = buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map(|file| file.path().display().to_string());
|
||||
|
||||
let task = project.save_buffer(buffer.clone(), cx);
|
||||
|
||||
(path, task)
|
||||
})?;
|
||||
|
||||
save_task.await?;
|
||||
|
||||
if let Some(path) = path {
|
||||
writeln!(&mut answer, "{}", path)?;
|
||||
}
|
||||
}
|
||||
|
||||
let errors = self.parser.errors();
|
||||
|
||||
if errors.is_empty() && self.bad_searches.is_empty() {
|
||||
Ok(answer.trim_end().to_string())
|
||||
} else {
|
||||
if !self.bad_searches.is_empty() {
|
||||
writeln!(
|
||||
&mut answer,
|
||||
"\nThese searches failed because they didn't match any strings:"
|
||||
)?;
|
||||
|
||||
for replace in self.bad_searches {
|
||||
writeln!(
|
||||
&mut answer,
|
||||
"- '{}' does not appear in `{}`",
|
||||
replace.search.replace("\r", "\\r").replace("\n", "\\n"),
|
||||
replace.file_path
|
||||
)?;
|
||||
}
|
||||
|
||||
writeln!(&mut answer, "Make sure to use exact searches.")?;
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
writeln!(
|
||||
&mut answer,
|
||||
"\nThese SEARCH/REPLACE blocks failed to parse:"
|
||||
)?;
|
||||
|
||||
for error in errors {
|
||||
writeln!(&mut answer, "- {}", error)?;
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(
|
||||
&mut answer,
|
||||
"\nYou can fix errors by running the tool again. You can include instructions,\
|
||||
but errors are part of the conversation so you don't need to repeat them."
|
||||
)?;
|
||||
|
||||
Err(anyhow!(answer))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Edit files in the current project.
|
||||
Edit files in the current project by specifying instructions in natural language.
|
||||
|
||||
When using this tool, you should suggest one coherent edit that can be made to the codebase.
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ Every *SEARCH/REPLACE block* must use this format:
|
||||
7. The end of the replace block: >>>>>>> REPLACE
|
||||
8. The closing fence: ```
|
||||
|
||||
Use the *FULL* file path, as shown to you by the user.
|
||||
Use the *FULL* file path, as shown to you by the user. Make sure to include the project's root directory name at the start of the path. *NEVER* specify the absolute path of the file!
|
||||
|
||||
Every *SEARCH* section must *EXACTLY MATCH* the existing file content, character for character, including all comments, docstrings, etc.
|
||||
If the file contains code or other data wrapped/escaped in json/xml/quotes or other containers, you need to propose edits to the literal contents of the file, including the container markup.
|
||||
|
||||
415
crates/assistant_tools/src/edit_files_tool/log.rs
Normal file
415
crates/assistant_tools/src/edit_files_tool/log.rs
Normal file
@@ -0,0 +1,415 @@
|
||||
use std::path::Path;
|
||||
|
||||
use collections::HashSet;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use gpui::{
|
||||
actions, list, prelude::*, App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global,
|
||||
ListAlignment, ListState, SharedString, Subscription, Window,
|
||||
};
|
||||
use release_channel::ReleaseChannel;
|
||||
use settings::Settings;
|
||||
use ui::prelude::*;
|
||||
use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId};
|
||||
|
||||
use super::edit_action::EditAction;
|
||||
|
||||
actions!(debug, [EditTool]);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
if cx.is_staff() || ReleaseChannel::global(cx) == ReleaseChannel::Dev {
|
||||
// Track events even before opening the log
|
||||
EditToolLog::global(cx);
|
||||
}
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||
workspace.register_action(|workspace, _: &EditTool, window, cx| {
|
||||
let viewer = cx.new(EditToolLogViewer::new);
|
||||
workspace.add_item_to_active_pane(Box::new(viewer), None, true, window, cx)
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub struct GlobalEditToolLog(Entity<EditToolLog>);
|
||||
|
||||
impl Global for GlobalEditToolLog {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EditToolLog {
|
||||
requests: Vec<EditToolRequest>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
|
||||
pub struct EditToolRequestId(u32);
|
||||
|
||||
impl EditToolLog {
|
||||
pub fn global(cx: &mut App) -> Entity<Self> {
|
||||
match Self::try_global(cx) {
|
||||
Some(entity) => entity,
|
||||
None => {
|
||||
let entity = cx.new(|_cx| Self::default());
|
||||
cx.set_global(GlobalEditToolLog(entity.clone()));
|
||||
entity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
|
||||
cx.try_global::<GlobalEditToolLog>()
|
||||
.map(|log| log.0.clone())
|
||||
}
|
||||
|
||||
pub fn new_request(
|
||||
&mut self,
|
||||
instructions: String,
|
||||
cx: &mut Context<Self>,
|
||||
) -> EditToolRequestId {
|
||||
let id = EditToolRequestId(self.requests.len() as u32);
|
||||
self.requests.push(EditToolRequest {
|
||||
id,
|
||||
instructions,
|
||||
editor_response: None,
|
||||
tool_output: None,
|
||||
parsed_edits: Vec::new(),
|
||||
});
|
||||
cx.emit(EditToolLogEvent::Inserted);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn push_editor_response_chunk(
|
||||
&mut self,
|
||||
id: EditToolRequestId,
|
||||
chunk: &str,
|
||||
new_actions: &[EditAction],
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(request) = self.requests.get_mut(id.0 as usize) {
|
||||
match &mut request.editor_response {
|
||||
None => {
|
||||
request.editor_response = Some(chunk.to_string());
|
||||
}
|
||||
Some(response) => {
|
||||
response.push_str(chunk);
|
||||
}
|
||||
}
|
||||
request.parsed_edits.extend(new_actions.iter().cloned());
|
||||
|
||||
cx.emit(EditToolLogEvent::Updated);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_tool_output(
|
||||
&mut self,
|
||||
id: EditToolRequestId,
|
||||
tool_output: Result<String, String>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(request) = self.requests.get_mut(id.0 as usize) {
|
||||
request.tool_output = Some(tool_output);
|
||||
cx.emit(EditToolLogEvent::Updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum EditToolLogEvent {
|
||||
Inserted,
|
||||
Updated,
|
||||
}
|
||||
|
||||
impl EventEmitter<EditToolLogEvent> for EditToolLog {}
|
||||
|
||||
pub struct EditToolRequest {
|
||||
id: EditToolRequestId,
|
||||
instructions: String,
|
||||
// we don't use a result here because the error might have occurred after we got a response
|
||||
editor_response: Option<String>,
|
||||
parsed_edits: Vec<EditAction>,
|
||||
tool_output: Option<Result<String, String>>,
|
||||
}
|
||||
|
||||
pub struct EditToolLogViewer {
|
||||
focus_handle: FocusHandle,
|
||||
log: Entity<EditToolLog>,
|
||||
list_state: ListState,
|
||||
expanded_edits: HashSet<(EditToolRequestId, usize)>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl EditToolLogViewer {
|
||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||
let log = EditToolLog::global(cx);
|
||||
|
||||
let subscription = cx.subscribe(&log, Self::handle_log_event);
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
log: log.clone(),
|
||||
list_state: ListState::new(
|
||||
log.read(cx).requests.len(),
|
||||
ListAlignment::Bottom,
|
||||
px(1024.),
|
||||
{
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
this.update(cx, |this, cx| this.render_request(ix, window, cx))
|
||||
.unwrap()
|
||||
}
|
||||
},
|
||||
),
|
||||
expanded_edits: HashSet::default(),
|
||||
_subscription: subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_log_event(
|
||||
&mut self,
|
||||
_: Entity<EditToolLog>,
|
||||
event: &EditToolLogEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditToolLogEvent::Inserted => {
|
||||
let count = self.list_state.item_count();
|
||||
self.list_state.splice(count..count, 1);
|
||||
}
|
||||
EditToolLogEvent::Updated => {}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_request(
|
||||
&self,
|
||||
index: usize,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let requests = &self.log.read(cx).requests;
|
||||
let request = &requests[index];
|
||||
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.child(Self::render_section(IconName::ArrowRight, "Tool Input"))
|
||||
.child(request.instructions.clone())
|
||||
.py_5()
|
||||
.when(index + 1 < requests.len(), |element| {
|
||||
element
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
})
|
||||
.map(|parent| match &request.editor_response {
|
||||
None => {
|
||||
if request.tool_output.is_none() {
|
||||
parent.child("...")
|
||||
} else {
|
||||
parent
|
||||
}
|
||||
}
|
||||
Some(response) => parent
|
||||
.child(Self::render_section(
|
||||
IconName::ZedAssistant,
|
||||
"Editor Response",
|
||||
))
|
||||
.child(Label::new(response.clone()).buffer_font(cx)),
|
||||
})
|
||||
.when(!request.parsed_edits.is_empty(), |parent| {
|
||||
parent
|
||||
.child(Self::render_section(IconName::Microscope, "Parsed Edits"))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.children(request.parsed_edits.iter().enumerate().map(
|
||||
|(index, edit)| {
|
||||
self.render_edit_action(edit, request.id, index, cx)
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
.when_some(request.tool_output.as_ref(), |parent, output| {
|
||||
parent
|
||||
.child(Self::render_section(IconName::ArrowLeft, "Tool Output"))
|
||||
.child(match output {
|
||||
Ok(output) => Label::new(output.clone()).color(Color::Success),
|
||||
Err(error) => Label::new(error.clone()).color(Color::Error),
|
||||
})
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_section(icon: IconName, title: &'static str) -> AnyElement {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(icon).color(Color::Muted))
|
||||
.child(Label::new(title).size(LabelSize::Small).color(Color::Muted))
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_edit_action(
|
||||
&self,
|
||||
edit_action: &EditAction,
|
||||
request_id: EditToolRequestId,
|
||||
index: usize,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let expanded_id = (request_id, index);
|
||||
|
||||
match edit_action {
|
||||
EditAction::Replace {
|
||||
file_path,
|
||||
old,
|
||||
new,
|
||||
} => self
|
||||
.render_edit_action_container(
|
||||
expanded_id,
|
||||
&file_path,
|
||||
[
|
||||
Self::render_block(IconName::MagnifyingGlass, "Search", old.clone(), cx)
|
||||
.border_r_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.into_any(),
|
||||
Self::render_block(IconName::Replace, "Replace", new.clone(), cx)
|
||||
.into_any(),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.into_any(),
|
||||
EditAction::Write { file_path, content } => self
|
||||
.render_edit_action_container(
|
||||
expanded_id,
|
||||
&file_path,
|
||||
[
|
||||
Self::render_block(IconName::Pencil, "Write", content.clone(), cx)
|
||||
.into_any(),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.into_any(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_edit_action_container(
|
||||
&self,
|
||||
expanded_id: (EditToolRequestId, usize),
|
||||
file_path: &Path,
|
||||
content: impl IntoIterator<Item = AnyElement>,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let is_expanded = self.expanded_edits.contains(&expanded_id);
|
||||
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_t_md()
|
||||
.when(!is_expanded, |el| el.rounded_b_md())
|
||||
.py_1()
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.child(
|
||||
ui::Disclosure::new(ElementId::Integer(expanded_id.1), is_expanded)
|
||||
.on_click(cx.listener(move |this, _ev, _window, cx| {
|
||||
if is_expanded {
|
||||
this.expanded_edits.remove(&expanded_id);
|
||||
} else {
|
||||
this.expanded_edits.insert(expanded_id);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(Label::new(file_path.display().to_string()).size(LabelSize::Small)),
|
||||
)
|
||||
.child(if is_expanded {
|
||||
h_flex()
|
||||
.border_1()
|
||||
.border_t_0()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_b_md()
|
||||
.children(content)
|
||||
.into_any()
|
||||
} else {
|
||||
Empty.into_any()
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_block(icon: IconName, title: &'static str, content: String, cx: &App) -> Div {
|
||||
v_flex()
|
||||
.p_1()
|
||||
.gap_1()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(icon).color(Color::Muted))
|
||||
.child(Label::new(title).size(LabelSize::Small).color(Color::Muted)),
|
||||
)
|
||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.text_sm()
|
||||
.child(content)
|
||||
.child(div().flex_1())
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for EditToolLogViewer {}
|
||||
|
||||
impl Focusable for EditToolLogViewer {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for EditToolLogViewer {
|
||||
type Event = ();
|
||||
|
||||
fn to_item_events(_: &Self::Event, _: impl FnMut(ItemEvent)) {}
|
||||
|
||||
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
|
||||
Some("Edit Tool Log".into())
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(cx.new(Self::new))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for EditToolLogViewer {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self.list_state.item_count() == 0 {
|
||||
return v_flex()
|
||||
.justify_center()
|
||||
.size_full()
|
||||
.gap_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.text_center()
|
||||
.text_lg()
|
||||
.child("No requests yet")
|
||||
.child(
|
||||
div()
|
||||
.text_ui(cx)
|
||||
.child("Go ask the assistant to perform some edits"),
|
||||
);
|
||||
}
|
||||
|
||||
v_flex()
|
||||
.p_4()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.size_full()
|
||||
.child(list(self.list_state.clone()).flex_grow())
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,10 @@ pub struct ListDirectoryToolInput {
|
||||
/// The relative path of the directory to list.
|
||||
///
|
||||
/// This path should never be absolute, and the first component
|
||||
/// of the path should always be a top-level directory in a project.
|
||||
/// of the path should always be a root directory in a project.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following top-level directories:
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - directory1
|
||||
/// - directory2
|
||||
@@ -24,7 +24,7 @@ pub struct ListDirectoryToolInput {
|
||||
/// </example>
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following top-level directories:
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - foo
|
||||
/// - bar
|
||||
@@ -62,27 +62,37 @@ impl Tool for ListDirectoryTool {
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
let Some(worktree_root_name) = input.path.components().next() else {
|
||||
return Task::ready(Err(anyhow!("Invalid path")));
|
||||
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Path not found in project")));
|
||||
};
|
||||
let Some(worktree) = project
|
||||
.read(cx)
|
||||
.worktree_for_root_name(&worktree_root_name.as_os_str().to_string_lossy(), cx)
|
||||
.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("Directory not found in the project")));
|
||||
return Task::ready(Err(anyhow!("Worktree not found")));
|
||||
};
|
||||
let path = input.path.strip_prefix(worktree_root_name).unwrap();
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
let Some(entry) = worktree.entry_for_path(&project_path.path) else {
|
||||
return Task::ready(Err(anyhow!("Path not found: {}", input.path.display())));
|
||||
};
|
||||
|
||||
if !entry.is_dir() {
|
||||
return Task::ready(Err(anyhow!("{} is a file.", input.path.display())));
|
||||
}
|
||||
|
||||
let mut output = String::new();
|
||||
for entry in worktree.read(cx).child_entries(path) {
|
||||
for entry in worktree.child_entries(&project_path.path) {
|
||||
writeln!(
|
||||
output,
|
||||
"{}",
|
||||
Path::new(worktree_root_name.as_os_str())
|
||||
.join(&entry.path)
|
||||
.display(),
|
||||
Path::new(worktree.root_name()).join(&entry.path).display(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
if output.is_empty() {
|
||||
return Task::ready(Ok(format!("{} is empty.", input.path.display())));
|
||||
}
|
||||
Task::ready(Ok(output))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ pub struct PathSearchToolInput {
|
||||
/// The glob to search all project paths for.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following top-level directories:
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - directory1/a/something.txt
|
||||
/// - directory2/a/things.txt
|
||||
|
||||
@@ -5,7 +5,7 @@ use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::{Project, ProjectPath};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -14,10 +14,10 @@ pub struct ReadFileToolInput {
|
||||
/// The relative path of the file to read.
|
||||
///
|
||||
/// This path should never be absolute, and the first component
|
||||
/// of the path should always be a top-level directory in a project.
|
||||
/// of the path should always be a root directory in a project.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following top-level directories:
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - directory1
|
||||
/// - directory2
|
||||
@@ -56,18 +56,8 @@ impl Tool for ReadFileTool {
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
let Some(worktree_root_name) = input.path.components().next() else {
|
||||
return Task::ready(Err(anyhow!("Invalid path")));
|
||||
};
|
||||
let Some(worktree) = project
|
||||
.read(cx)
|
||||
.worktree_for_root_name(&worktree_root_name.as_os_str().to_string_lossy(), cx)
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("Directory not found in the project")));
|
||||
};
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: Arc::from(input.path.strip_prefix(worktree_root_name).unwrap()),
|
||||
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Path not found in project")));
|
||||
};
|
||||
cx.spawn(|cx| async move {
|
||||
let buffer = cx
|
||||
|
||||
@@ -93,7 +93,7 @@ fn view_release_notes_locally(
|
||||
|
||||
let tab_description = SharedString::from(body.title.to_string());
|
||||
let editor = cx.new(|cx| {
|
||||
Editor::for_multibuffer(buffer, Some(project), true, window, cx)
|
||||
Editor::for_multibuffer(buffer, Some(project), window, cx)
|
||||
});
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let markdown_preview: Entity<MarkdownPreviewView> =
|
||||
|
||||
@@ -22,6 +22,7 @@ git2.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
rope.workspace = true
|
||||
sum_tree.workspace = true
|
||||
text.workspace = true
|
||||
@@ -31,7 +32,6 @@ util.workspace = true
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
rand.workspace = true
|
||||
serde_json.workspace = true
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -6,9 +6,9 @@ use rope::Rope;
|
||||
use std::cmp::Ordering;
|
||||
use std::mem;
|
||||
use std::{future::Future, iter, ops::Range, sync::Arc};
|
||||
use sum_tree::{SumTree, TreeMap};
|
||||
use text::ToOffset as _;
|
||||
use sum_tree::SumTree;
|
||||
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};
|
||||
use text::{AnchorRangeExt, ToOffset as _};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct BufferDiff {
|
||||
@@ -26,7 +26,7 @@ pub struct BufferDiffSnapshot {
|
||||
#[derive(Clone)]
|
||||
struct BufferDiffInner {
|
||||
hunks: SumTree<InternalDiffHunk>,
|
||||
pending_hunks: TreeMap<usize, PendingHunk>,
|
||||
pending_hunks: SumTree<PendingHunk>,
|
||||
base_text: language::BufferSnapshot,
|
||||
base_text_exists: bool,
|
||||
}
|
||||
@@ -48,7 +48,7 @@ pub enum DiffHunkStatusKind {
|
||||
pub enum DiffHunkSecondaryStatus {
|
||||
HasSecondaryHunk,
|
||||
OverlapsWithSecondaryHunk,
|
||||
None,
|
||||
NoSecondaryHunk,
|
||||
SecondaryHunkAdditionPending,
|
||||
SecondaryHunkRemovalPending,
|
||||
}
|
||||
@@ -74,6 +74,8 @@ struct InternalDiffHunk {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct PendingHunk {
|
||||
buffer_range: Range<Anchor>,
|
||||
diff_base_byte_range: Range<usize>,
|
||||
buffer_version: clock::Global,
|
||||
new_status: DiffHunkSecondaryStatus,
|
||||
}
|
||||
@@ -93,6 +95,16 @@ impl sum_tree::Item for InternalDiffHunk {
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for PendingHunk {
|
||||
type Summary = DiffHunkSummary;
|
||||
|
||||
fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
|
||||
DiffHunkSummary {
|
||||
buffer_range: self.buffer_range.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for DiffHunkSummary {
|
||||
type Context = text::BufferSnapshot;
|
||||
|
||||
@@ -176,6 +188,7 @@ impl BufferDiffSnapshot {
|
||||
}
|
||||
|
||||
impl BufferDiffInner {
|
||||
/// Returns the new index text and new pending hunks.
|
||||
fn stage_or_unstage_hunks(
|
||||
&mut self,
|
||||
unstaged_diff: &Self,
|
||||
@@ -183,7 +196,7 @@ impl BufferDiffInner {
|
||||
hunks: &[DiffHunk],
|
||||
buffer: &text::BufferSnapshot,
|
||||
file_exists: bool,
|
||||
) -> (Option<Rope>, Vec<(usize, PendingHunk)>) {
|
||||
) -> (Option<Rope>, SumTree<PendingHunk>) {
|
||||
let head_text = self
|
||||
.base_text_exists
|
||||
.then(|| self.base_text.as_rope().clone());
|
||||
@@ -195,41 +208,41 @@ impl BufferDiffInner {
|
||||
// entire file must be either created or deleted in the index.
|
||||
let (index_text, head_text) = match (index_text, head_text) {
|
||||
(Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
|
||||
(_, head_text @ _) => {
|
||||
if stage {
|
||||
(index_text, head_text) => {
|
||||
let (rope, new_status) = if stage {
|
||||
log::debug!("stage all");
|
||||
return (
|
||||
(
|
||||
file_exists.then(|| buffer.as_rope().clone()),
|
||||
vec![(
|
||||
0,
|
||||
PendingHunk {
|
||||
buffer_version: buffer.version().clone(),
|
||||
new_status: DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
|
||||
},
|
||||
)],
|
||||
);
|
||||
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
|
||||
)
|
||||
} else {
|
||||
log::debug!("unstage all");
|
||||
return (
|
||||
(
|
||||
head_text,
|
||||
vec![(
|
||||
0,
|
||||
PendingHunk {
|
||||
buffer_version: buffer.version().clone(),
|
||||
new_status: DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
|
||||
},
|
||||
)],
|
||||
);
|
||||
}
|
||||
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
|
||||
)
|
||||
};
|
||||
|
||||
let hunk = PendingHunk {
|
||||
buffer_range: Anchor::MIN..Anchor::MAX,
|
||||
diff_base_byte_range: 0..index_text.map_or(0, |rope| rope.len()),
|
||||
buffer_version: buffer.version().clone(),
|
||||
new_status,
|
||||
};
|
||||
let tree = SumTree::from_item(hunk, buffer);
|
||||
return (rope, tree);
|
||||
}
|
||||
};
|
||||
|
||||
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
|
||||
unstaged_hunk_cursor.next(buffer);
|
||||
let mut edits = Vec::new();
|
||||
let mut pending_hunks = Vec::new();
|
||||
let mut prev_unstaged_hunk_buffer_offset = 0;
|
||||
let mut prev_unstaged_hunk_base_text_offset = 0;
|
||||
|
||||
let mut pending_hunks = SumTree::new(buffer);
|
||||
let mut old_pending_hunks = unstaged_diff
|
||||
.pending_hunks
|
||||
.cursor::<DiffHunkSummary>(buffer);
|
||||
|
||||
// first, merge new hunks into pending_hunks
|
||||
for DiffHunk {
|
||||
buffer_range,
|
||||
diff_base_byte_range,
|
||||
@@ -237,12 +250,58 @@ impl BufferDiffInner {
|
||||
..
|
||||
} in hunks.iter().cloned()
|
||||
{
|
||||
if (stage && secondary_status == DiffHunkSecondaryStatus::None)
|
||||
let preceding_pending_hunks =
|
||||
old_pending_hunks.slice(&buffer_range.start, Bias::Left, buffer);
|
||||
|
||||
pending_hunks.append(preceding_pending_hunks, buffer);
|
||||
|
||||
// skip all overlapping old pending hunks
|
||||
while old_pending_hunks
|
||||
.item()
|
||||
.is_some_and(|preceding_pending_hunk_item| {
|
||||
preceding_pending_hunk_item
|
||||
.buffer_range
|
||||
.overlaps(&buffer_range, buffer)
|
||||
})
|
||||
{
|
||||
old_pending_hunks.next(buffer);
|
||||
}
|
||||
|
||||
// merge into pending hunks
|
||||
if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
|
||||
|| (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
pending_hunks.push(
|
||||
PendingHunk {
|
||||
buffer_range,
|
||||
diff_base_byte_range,
|
||||
buffer_version: buffer.version().clone(),
|
||||
new_status: if stage {
|
||||
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
|
||||
} else {
|
||||
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
|
||||
},
|
||||
},
|
||||
buffer,
|
||||
);
|
||||
}
|
||||
// append the remainder
|
||||
pending_hunks.append(old_pending_hunks.suffix(buffer), buffer);
|
||||
|
||||
let mut prev_unstaged_hunk_buffer_offset = 0;
|
||||
let mut prev_unstaged_hunk_base_text_offset = 0;
|
||||
let mut edits = Vec::<(Range<usize>, String)>::new();
|
||||
|
||||
// then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits
|
||||
for PendingHunk {
|
||||
buffer_range,
|
||||
diff_base_byte_range,
|
||||
..
|
||||
} in pending_hunks.iter().cloned()
|
||||
{
|
||||
let skipped_hunks = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left, buffer);
|
||||
|
||||
if let Some(secondary_hunk) = skipped_hunks.last() {
|
||||
@@ -294,22 +353,15 @@ impl BufferDiffInner {
|
||||
.chunks_in_range(diff_base_byte_range.clone())
|
||||
.collect::<String>()
|
||||
};
|
||||
pending_hunks.push((
|
||||
diff_base_byte_range.start,
|
||||
PendingHunk {
|
||||
buffer_version: buffer.version().clone(),
|
||||
new_status: if stage {
|
||||
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
|
||||
} else {
|
||||
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
|
||||
},
|
||||
},
|
||||
));
|
||||
|
||||
edits.push((index_range, replacement_text));
|
||||
}
|
||||
|
||||
debug_assert!(edits.iter().is_sorted_by_key(|(range, _)| range.start));
|
||||
|
||||
let mut new_index_text = Rope::new();
|
||||
let mut index_cursor = index_text.cursor(0);
|
||||
|
||||
for (old_range, replacement_text) in edits {
|
||||
new_index_text.append(index_cursor.slice(old_range.start));
|
||||
index_cursor.seek_forward(old_range.end);
|
||||
@@ -354,12 +406,14 @@ impl BufferDiffInner {
|
||||
});
|
||||
|
||||
let mut secondary_cursor = None;
|
||||
let mut pending_hunks = TreeMap::default();
|
||||
let mut pending_hunks_cursor = None;
|
||||
if let Some(secondary) = secondary.as_ref() {
|
||||
let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
|
||||
cursor.next(buffer);
|
||||
secondary_cursor = Some(cursor);
|
||||
pending_hunks = secondary.pending_hunks.clone();
|
||||
let mut cursor = secondary.pending_hunks.cursor::<DiffHunkSummary>(buffer);
|
||||
cursor.next(buffer);
|
||||
pending_hunks_cursor = Some(cursor);
|
||||
}
|
||||
|
||||
let max_point = buffer.max_point();
|
||||
@@ -378,16 +432,33 @@ impl BufferDiffInner {
|
||||
end_anchor = buffer.anchor_before(end_point);
|
||||
}
|
||||
|
||||
let mut secondary_status = DiffHunkSecondaryStatus::None;
|
||||
let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
|
||||
|
||||
let mut has_pending = false;
|
||||
if let Some(pending_hunk) = pending_hunks.get(&start_base) {
|
||||
if !buffer.has_edits_since_in_range(
|
||||
&pending_hunk.buffer_version,
|
||||
start_anchor..end_anchor,
|
||||
) {
|
||||
has_pending = true;
|
||||
secondary_status = pending_hunk.new_status;
|
||||
if let Some(pending_cursor) = pending_hunks_cursor.as_mut() {
|
||||
if start_anchor
|
||||
.cmp(&pending_cursor.start().buffer_range.start, buffer)
|
||||
.is_gt()
|
||||
{
|
||||
pending_cursor.seek_forward(&start_anchor, Bias::Left, buffer);
|
||||
}
|
||||
|
||||
if let Some(pending_hunk) = pending_cursor.item() {
|
||||
let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
|
||||
if pending_range.end.column > 0 {
|
||||
pending_range.end.row += 1;
|
||||
pending_range.end.column = 0;
|
||||
}
|
||||
|
||||
if pending_range == (start_point..end_point) {
|
||||
if !buffer.has_edits_since_in_range(
|
||||
&pending_hunk.buffer_version,
|
||||
start_anchor..end_anchor,
|
||||
) {
|
||||
has_pending = true;
|
||||
secondary_status = pending_hunk.new_status;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,7 +520,7 @@ impl BufferDiffInner {
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||
buffer_range: hunk.buffer_range.clone(),
|
||||
// The secondary status is not used by callers of this method.
|
||||
secondary_status: DiffHunkSecondaryStatus::None,
|
||||
secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -724,7 +795,7 @@ impl BufferDiff {
|
||||
base_text,
|
||||
hunks,
|
||||
base_text_exists,
|
||||
pending_hunks: TreeMap::default(),
|
||||
pending_hunks: SumTree::new(&buffer),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -740,8 +811,8 @@ impl BufferDiff {
|
||||
cx.background_spawn(async move {
|
||||
BufferDiffInner {
|
||||
base_text: base_text_snapshot,
|
||||
pending_hunks: SumTree::new(&buffer),
|
||||
hunks: compute_hunks(base_text_pair, buffer),
|
||||
pending_hunks: TreeMap::default(),
|
||||
base_text_exists,
|
||||
}
|
||||
})
|
||||
@@ -751,7 +822,7 @@ impl BufferDiff {
|
||||
BufferDiffInner {
|
||||
base_text: language::Buffer::build_empty_snapshot(cx),
|
||||
hunks: SumTree::new(buffer),
|
||||
pending_hunks: TreeMap::default(),
|
||||
pending_hunks: SumTree::new(buffer),
|
||||
base_text_exists: false,
|
||||
}
|
||||
}
|
||||
@@ -767,7 +838,7 @@ impl BufferDiff {
|
||||
pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(secondary_diff) = &self.secondary_diff {
|
||||
secondary_diff.update(cx, |diff, _| {
|
||||
diff.inner.pending_hunks.clear();
|
||||
diff.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary::default());
|
||||
});
|
||||
cx.emit(BufferDiffEvent::DiffChanged {
|
||||
changed_range: Some(Anchor::MIN..Anchor::MAX),
|
||||
@@ -783,18 +854,17 @@ impl BufferDiff {
|
||||
file_exists: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Rope> {
|
||||
let (new_index_text, pending_hunks) = self.inner.stage_or_unstage_hunks(
|
||||
let (new_index_text, new_pending_hunks) = self.inner.stage_or_unstage_hunks(
|
||||
&self.secondary_diff.as_ref()?.read(cx).inner,
|
||||
stage,
|
||||
&hunks,
|
||||
buffer,
|
||||
file_exists,
|
||||
);
|
||||
|
||||
if let Some(unstaged_diff) = &self.secondary_diff {
|
||||
unstaged_diff.update(cx, |diff, _| {
|
||||
for (offset, pending_hunk) in pending_hunks {
|
||||
diff.inner.pending_hunks.insert(offset, pending_hunk);
|
||||
}
|
||||
diff.inner.pending_hunks = new_pending_hunks;
|
||||
});
|
||||
}
|
||||
cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
|
||||
@@ -916,7 +986,9 @@ impl BufferDiff {
|
||||
}
|
||||
_ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)),
|
||||
};
|
||||
let pending_hunks = mem::take(&mut self.inner.pending_hunks);
|
||||
|
||||
let pending_hunks = mem::replace(&mut self.inner.pending_hunks, SumTree::new(buffer));
|
||||
|
||||
self.inner = new_state;
|
||||
if !base_text_changed {
|
||||
self.inner.pending_hunks = pending_hunks;
|
||||
@@ -1149,21 +1221,21 @@ impl DiffHunkStatus {
|
||||
pub fn deleted_none() -> Self {
|
||||
Self {
|
||||
kind: DiffHunkStatusKind::Deleted,
|
||||
secondary: DiffHunkSecondaryStatus::None,
|
||||
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn added_none() -> Self {
|
||||
Self {
|
||||
kind: DiffHunkStatusKind::Added,
|
||||
secondary: DiffHunkSecondaryStatus::None,
|
||||
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modified_none() -> Self {
|
||||
Self {
|
||||
kind: DiffHunkStatusKind::Modified,
|
||||
secondary: DiffHunkSecondaryStatus::None,
|
||||
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1171,13 +1243,14 @@ impl DiffHunkStatus {
|
||||
/// Range (crossing new lines), old, new
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[track_caller]
|
||||
pub fn assert_hunks<Iter>(
|
||||
diff_hunks: Iter,
|
||||
pub fn assert_hunks<ExpectedText, HunkIter>(
|
||||
diff_hunks: HunkIter,
|
||||
buffer: &text::BufferSnapshot,
|
||||
diff_base: &str,
|
||||
expected_hunks: &[(Range<u32>, &str, &str, DiffHunkStatus)],
|
||||
expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
|
||||
) where
|
||||
Iter: Iterator<Item = DiffHunk>,
|
||||
HunkIter: Iterator<Item = DiffHunk>,
|
||||
ExpectedText: AsRef<str>,
|
||||
{
|
||||
let actual_hunks = diff_hunks
|
||||
.map(|hunk| {
|
||||
@@ -1197,14 +1270,14 @@ pub fn assert_hunks<Iter>(
|
||||
.map(|(r, old_text, new_text, status)| {
|
||||
(
|
||||
Point::new(r.start, 0)..Point::new(r.end, 0),
|
||||
*old_text,
|
||||
new_text.to_string(),
|
||||
old_text.as_ref(),
|
||||
new_text.as_ref().to_string(),
|
||||
*status,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(actual_hunks, expected_hunks);
|
||||
pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1263,7 +1336,7 @@ mod tests {
|
||||
);
|
||||
|
||||
diff = cx.update(|cx| BufferDiff::build_empty(&buffer, cx));
|
||||
assert_hunks(
|
||||
assert_hunks::<&str, _>(
|
||||
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
@@ -1601,7 +1674,10 @@ mod tests {
|
||||
.hunks_intersecting_range(hunk_range.clone(), &buffer, &cx)
|
||||
.collect::<Vec<_>>();
|
||||
for hunk in &hunks {
|
||||
assert_ne!(hunk.secondary_status, DiffHunkSecondaryStatus::None)
|
||||
assert_ne!(
|
||||
hunk.secondary_status,
|
||||
DiffHunkSecondaryStatus::NoSecondaryHunk
|
||||
)
|
||||
}
|
||||
|
||||
let new_index_text = diff
|
||||
@@ -1880,10 +1956,10 @@ mod tests {
|
||||
let hunk_to_change = hunk.clone();
|
||||
let stage = match hunk.secondary_status {
|
||||
DiffHunkSecondaryStatus::HasSecondaryHunk => {
|
||||
hunk.secondary_status = DiffHunkSecondaryStatus::None;
|
||||
hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
|
||||
true
|
||||
}
|
||||
DiffHunkSecondaryStatus::None => {
|
||||
DiffHunkSecondaryStatus::NoSecondaryHunk => {
|
||||
hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
|
||||
false
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@ fn main() -> Result<()> {
|
||||
let mut paths = vec![];
|
||||
let mut urls = vec![];
|
||||
let mut stdin_tmp_file: Option<fs::File> = None;
|
||||
let mut anonymous_fd_tmp_files = vec![];
|
||||
|
||||
for path in args.paths_with_position.iter() {
|
||||
if path.starts_with("zed://")
|
||||
|| path.starts_with("http://")
|
||||
@@ -211,6 +213,11 @@ fn main() -> Result<()> {
|
||||
paths.push(file.path().to_string_lossy().to_string());
|
||||
let (file, _) = file.keep()?;
|
||||
stdin_tmp_file = Some(file);
|
||||
} else if let Some(file) = anonymous_fd(path) {
|
||||
let tmp_file = NamedTempFile::new()?;
|
||||
paths.push(tmp_file.path().to_string_lossy().to_string());
|
||||
let (tmp_file, _) = tmp_file.keep()?;
|
||||
anonymous_fd_tmp_files.push((file, tmp_file));
|
||||
} else {
|
||||
paths.push(parse_path_with_position(path)?)
|
||||
}
|
||||
@@ -252,31 +259,33 @@ fn main() -> Result<()> {
|
||||
}
|
||||
});
|
||||
|
||||
let pipe_handle: JoinHandle<anyhow::Result<()>> = thread::spawn(move || {
|
||||
if let Some(mut tmp_file) = stdin_tmp_file {
|
||||
let mut stdin = std::io::stdin().lock();
|
||||
if io::IsTerminal::is_terminal(&stdin) {
|
||||
return Ok(());
|
||||
}
|
||||
let mut buffer = [0; 8 * 1024];
|
||||
loop {
|
||||
let bytes_read = io::Read::read(&mut stdin, &mut buffer)?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
let stdin_pipe_handle: Option<JoinHandle<anyhow::Result<()>>> =
|
||||
stdin_tmp_file.map(|tmp_file| {
|
||||
thread::spawn(move || {
|
||||
let stdin = std::io::stdin().lock();
|
||||
if io::IsTerminal::is_terminal(&stdin) {
|
||||
return Ok(());
|
||||
}
|
||||
io::Write::write(&mut tmp_file, &buffer[..bytes_read])?;
|
||||
}
|
||||
io::Write::flush(&mut tmp_file)?;
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
return pipe_to_tmp(stdin, tmp_file);
|
||||
})
|
||||
});
|
||||
|
||||
let anonymous_fd_pipe_handles: Vec<JoinHandle<anyhow::Result<()>>> = anonymous_fd_tmp_files
|
||||
.into_iter()
|
||||
.map(|(file, tmp_file)| thread::spawn(move || pipe_to_tmp(file, tmp_file)))
|
||||
.collect();
|
||||
|
||||
if args.foreground {
|
||||
app.run_foreground(url)?;
|
||||
} else {
|
||||
app.launch(url)?;
|
||||
sender.join().unwrap()?;
|
||||
pipe_handle.join().unwrap()?;
|
||||
if let Some(handle) = stdin_pipe_handle {
|
||||
handle.join().unwrap()?;
|
||||
}
|
||||
for handle in anonymous_fd_pipe_handles {
|
||||
handle.join().unwrap()?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(exit_status) = exit_status.lock().take() {
|
||||
@@ -285,6 +294,64 @@ fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pipe_to_tmp(mut src: impl io::Read, mut dest: fs::File) -> Result<()> {
|
||||
let mut buffer = [0; 8 * 1024];
|
||||
loop {
|
||||
let bytes_read = match src.read(&mut buffer) {
|
||||
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
|
||||
res => res?,
|
||||
};
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
io::Write::write_all(&mut dest, &buffer[..bytes_read])?;
|
||||
}
|
||||
io::Write::flush(&mut dest)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn anonymous_fd(path: &str) -> Option<fs::File> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::os::fd::{self, FromRawFd};
|
||||
|
||||
let fd_str = path.strip_prefix("/proc/self/fd/")?;
|
||||
|
||||
let link = fs::read_link(path).ok()?;
|
||||
if !link.starts_with("memfd:") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let fd: fd::RawFd = fd_str.parse().ok()?;
|
||||
let file = unsafe { fs::File::from_raw_fd(fd) };
|
||||
return Some(file);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::os::{
|
||||
fd::{self, FromRawFd},
|
||||
unix::fs::FileTypeExt,
|
||||
};
|
||||
|
||||
let fd_str = path.strip_prefix("/dev/fd/")?;
|
||||
|
||||
let metadata = fs::metadata(path).ok()?;
|
||||
let file_type = metadata.file_type();
|
||||
if !file_type.is_fifo() && !file_type.is_socket() {
|
||||
return None;
|
||||
}
|
||||
let fd: fd::RawFd = fd_str.parse().ok()?;
|
||||
let file = unsafe { fs::File::from_raw_fd(fd) };
|
||||
return Some(file);
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
{
|
||||
_ = path;
|
||||
// not implemented for bsd, windows. Could be, but isn't yet
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
mod linux {
|
||||
use std::{
|
||||
|
||||
@@ -660,6 +660,10 @@ fn for_snowflake(
|
||||
e.event_type.clone(),
|
||||
serde_json::to_value(&e.event_properties).unwrap(),
|
||||
),
|
||||
Event::AssistantThreadFeedback(e) => (
|
||||
"Assistant Feedback".to_string(),
|
||||
serde_json::to_value(&e).unwrap(),
|
||||
),
|
||||
};
|
||||
|
||||
if let serde_json::Value::Object(ref mut map) = event_properties {
|
||||
|
||||
@@ -313,9 +313,8 @@ async fn test_basic_following(
|
||||
result
|
||||
});
|
||||
let multibuffer_editor_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
let editor = cx.new(|cx| {
|
||||
Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, window, cx)
|
||||
});
|
||||
let editor = cx
|
||||
.new(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), window, cx));
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
@@ -61,9 +61,9 @@ impl CompletionProvider for MessageEditorCompletionProvider {
|
||||
_: editor::CompletionContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<anyhow::Result<Vec<Completion>>> {
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
let Some(handle) = self.0.upgrade() else {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
return Task::ready(Ok(None));
|
||||
};
|
||||
handle.update(cx, |message_editor, cx| {
|
||||
message_editor.completions(buffer, buffer_position, cx)
|
||||
@@ -246,20 +246,22 @@ impl MessageEditor {
|
||||
buffer: &Entity<Buffer>,
|
||||
end_anchor: Anchor,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<Completion>>> {
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
if let Some((start_anchor, query, candidates)) =
|
||||
self.collect_mention_candidates(buffer, end_anchor, cx)
|
||||
{
|
||||
if !candidates.is_empty() {
|
||||
return cx.spawn(|_, cx| async move {
|
||||
Ok(Self::resolve_completions_for_candidates(
|
||||
&cx,
|
||||
query.as_str(),
|
||||
&candidates,
|
||||
start_anchor..end_anchor,
|
||||
Self::completion_for_mention,
|
||||
)
|
||||
.await)
|
||||
Ok(Some(
|
||||
Self::resolve_completions_for_candidates(
|
||||
&cx,
|
||||
query.as_str(),
|
||||
&candidates,
|
||||
start_anchor..end_anchor,
|
||||
Self::completion_for_mention,
|
||||
)
|
||||
.await,
|
||||
))
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -269,19 +271,21 @@ impl MessageEditor {
|
||||
{
|
||||
if !candidates.is_empty() {
|
||||
return cx.spawn(|_, cx| async move {
|
||||
Ok(Self::resolve_completions_for_candidates(
|
||||
&cx,
|
||||
query.as_str(),
|
||||
candidates,
|
||||
start_anchor..end_anchor,
|
||||
Self::completion_for_emoji,
|
||||
)
|
||||
.await)
|
||||
Ok(Some(
|
||||
Self::resolve_completions_for_candidates(
|
||||
&cx,
|
||||
query.as_str(),
|
||||
candidates,
|
||||
start_anchor..end_anchor,
|
||||
Self::completion_for_emoji,
|
||||
)
|
||||
.await,
|
||||
))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Task::ready(Ok(vec![]))
|
||||
Task::ready(Ok(Some(Vec::new())))
|
||||
}
|
||||
|
||||
async fn resolve_completions_for_candidates(
|
||||
|
||||
@@ -170,7 +170,9 @@ enum SignInStatus {
|
||||
prompt: Option<request::PromptUserDeviceFlow>,
|
||||
task: Shared<Task<Result<(), Arc<anyhow::Error>>>>,
|
||||
},
|
||||
SignedOut,
|
||||
SignedOut {
|
||||
awaiting_signing_in: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -180,7 +182,9 @@ pub enum Status {
|
||||
},
|
||||
Error(Arc<str>),
|
||||
Disabled,
|
||||
SignedOut,
|
||||
SignedOut {
|
||||
awaiting_signing_in: bool,
|
||||
},
|
||||
SigningIn {
|
||||
prompt: Option<request::PromptUserDeviceFlow>,
|
||||
},
|
||||
@@ -345,8 +349,8 @@ impl Copilot {
|
||||
buffers: Default::default(),
|
||||
_subscription: cx.on_app_quit(Self::shutdown_language_server),
|
||||
};
|
||||
this.enable_or_disable_copilot(cx);
|
||||
cx.observe_global::<SettingsStore>(move |this, cx| this.enable_or_disable_copilot(cx))
|
||||
this.start_copilot(true, false, cx);
|
||||
cx.observe_global::<SettingsStore>(move |this, cx| this.start_copilot(true, false, cx))
|
||||
.detach();
|
||||
this
|
||||
}
|
||||
@@ -364,26 +368,40 @@ impl Copilot {
|
||||
}
|
||||
}
|
||||
|
||||
fn enable_or_disable_copilot(&mut self, cx: &mut Context<Self>) {
|
||||
fn start_copilot(
|
||||
&mut self,
|
||||
check_edit_prediction_provider: bool,
|
||||
awaiting_sign_in_after_start: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if !matches!(self.server, CopilotServer::Disabled) {
|
||||
return;
|
||||
}
|
||||
let language_settings = all_language_settings(None, cx);
|
||||
if check_edit_prediction_provider
|
||||
&& language_settings.edit_predictions.provider != EditPredictionProvider::Copilot
|
||||
{
|
||||
return;
|
||||
}
|
||||
let server_id = self.server_id;
|
||||
let http = self.http.clone();
|
||||
let node_runtime = self.node_runtime.clone();
|
||||
let language_settings = all_language_settings(None, cx);
|
||||
if language_settings.edit_predictions.provider == EditPredictionProvider::Copilot {
|
||||
if matches!(self.server, CopilotServer::Disabled) {
|
||||
let env = self.build_env(&language_settings.edit_predictions.copilot);
|
||||
let start_task = cx
|
||||
.spawn(move |this, cx| {
|
||||
Self::start_language_server(server_id, http, node_runtime, env, this, cx)
|
||||
})
|
||||
.shared();
|
||||
self.server = CopilotServer::Starting { task: start_task };
|
||||
cx.notify();
|
||||
}
|
||||
} else {
|
||||
self.server = CopilotServer::Disabled;
|
||||
cx.notify();
|
||||
}
|
||||
let env = self.build_env(&language_settings.edit_predictions.copilot);
|
||||
let start_task = cx
|
||||
.spawn(move |this, cx| {
|
||||
Self::start_language_server(
|
||||
server_id,
|
||||
http,
|
||||
node_runtime,
|
||||
env,
|
||||
this,
|
||||
awaiting_sign_in_after_start,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.shared();
|
||||
self.server = CopilotServer::Starting { task: start_task };
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn build_env(&self, copilot_settings: &CopilotSettings) -> Option<HashMap<String, String>> {
|
||||
@@ -449,6 +467,7 @@ impl Copilot {
|
||||
node_runtime: NodeRuntime,
|
||||
env: Option<HashMap<String, String>>,
|
||||
this: WeakEntity<Self>,
|
||||
awaiting_sign_in_after_start: bool,
|
||||
mut cx: AsyncApp,
|
||||
) {
|
||||
let start_language_server = async {
|
||||
@@ -522,7 +541,9 @@ impl Copilot {
|
||||
Ok((server, status)) => {
|
||||
this.server = CopilotServer::Running(RunningCopilotServer {
|
||||
lsp: server,
|
||||
sign_in_status: SignInStatus::SignedOut,
|
||||
sign_in_status: SignInStatus::SignedOut {
|
||||
awaiting_signing_in: awaiting_sign_in_after_start,
|
||||
},
|
||||
registered_buffers: Default::default(),
|
||||
});
|
||||
cx.emit(Event::CopilotLanguageServerStarted);
|
||||
@@ -545,7 +566,7 @@ impl Copilot {
|
||||
cx.notify();
|
||||
task.clone()
|
||||
}
|
||||
SignInStatus::SignedOut | SignInStatus::Unauthorized { .. } => {
|
||||
SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized { .. } => {
|
||||
let lsp = server.lsp.clone();
|
||||
let task = cx
|
||||
.spawn(|this, mut cx| async move {
|
||||
@@ -633,7 +654,7 @@ impl Copilot {
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
CopilotServer::Disabled => cx.background_spawn(async move {
|
||||
CopilotServer::Disabled => cx.background_spawn(async {
|
||||
clear_copilot_config_dir().await;
|
||||
anyhow::Ok(())
|
||||
}),
|
||||
@@ -651,7 +672,8 @@ impl Copilot {
|
||||
let server_id = self.server_id;
|
||||
move |this, cx| async move {
|
||||
clear_copilot_dir().await;
|
||||
Self::start_language_server(server_id, http, node_runtime, env, this, cx).await
|
||||
Self::start_language_server(server_id, http, node_runtime, env, this, false, cx)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
@@ -961,7 +983,11 @@ impl Copilot {
|
||||
SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
|
||||
prompt: prompt.clone(),
|
||||
},
|
||||
SignInStatus::SignedOut => Status::SignedOut,
|
||||
SignInStatus::SignedOut {
|
||||
awaiting_signing_in,
|
||||
} => Status::SignedOut {
|
||||
awaiting_signing_in: *awaiting_signing_in,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -990,7 +1016,11 @@ impl Copilot {
|
||||
}
|
||||
}
|
||||
request::SignInStatus::Ok { user: None } | request::SignInStatus::NotSignedIn => {
|
||||
server.sign_in_status = SignInStatus::SignedOut;
|
||||
if !matches!(server.sign_in_status, SignInStatus::SignedOut { .. }) {
|
||||
server.sign_in_status = SignInStatus::SignedOut {
|
||||
awaiting_signing_in: false,
|
||||
};
|
||||
}
|
||||
cx.emit(Event::CopilotAuthSignedOut);
|
||||
for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
|
||||
self.unregister_buffer(&buffer);
|
||||
|
||||
@@ -745,8 +745,8 @@ mod tests {
|
||||
);
|
||||
multibuffer
|
||||
});
|
||||
let editor = cx
|
||||
.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, true, window, cx));
|
||||
let editor =
|
||||
cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
|
||||
editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
use gpui::Focusable;
|
||||
@@ -781,7 +781,7 @@ mod tests {
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
"\n\n\na = 1\nb = 2 + a\n\n\n\n\n\nc = 3\nd = 4\n\n"
|
||||
"\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
|
||||
);
|
||||
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
|
||||
});
|
||||
@@ -803,7 +803,7 @@ mod tests {
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4\n\n"
|
||||
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
|
||||
);
|
||||
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
|
||||
|
||||
@@ -812,7 +812,7 @@ mod tests {
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 \n\n"
|
||||
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
|
||||
);
|
||||
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
|
||||
});
|
||||
@@ -823,7 +823,7 @@ mod tests {
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 + c\n\n"
|
||||
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
|
||||
);
|
||||
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
|
||||
});
|
||||
@@ -997,8 +997,8 @@ mod tests {
|
||||
);
|
||||
multibuffer
|
||||
});
|
||||
let editor = cx
|
||||
.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, true, window, cx));
|
||||
let editor =
|
||||
cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
|
||||
editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
use gpui::Focusable;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::{request::PromptUserDeviceFlow, Copilot, Status};
|
||||
use gpui::{
|
||||
div, App, ClipboardItem, Context, DismissEvent, Element, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, InteractiveElement, IntoElement, MouseDownEvent, ParentElement, Render, Styled,
|
||||
Subscription, Window,
|
||||
div, percentage, svg, Animation, AnimationExt, App, ClipboardItem, Context, DismissEvent,
|
||||
Element, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement,
|
||||
MouseDownEvent, ParentElement, Render, Styled, Subscription, Transformation, Window,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use ui::{prelude::*, Button, Label, Vector, VectorName};
|
||||
use util::ResultExt as _;
|
||||
use workspace::notifications::NotificationId;
|
||||
@@ -17,11 +18,13 @@ pub fn initiate_sign_in(window: &mut Window, cx: &mut App) {
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return;
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
let Some(workspace) = window.root::<Workspace>().flatten() else {
|
||||
return;
|
||||
};
|
||||
match status {
|
||||
if matches!(copilot.read(cx).status(), Status::Disabled) {
|
||||
copilot.update(cx, |this, cx| this.start_copilot(false, true, cx));
|
||||
}
|
||||
match copilot.read(cx).status() {
|
||||
Status::Starting { task } => {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
@@ -54,6 +57,15 @@ pub fn initiate_sign_in(window: &mut Window, cx: &mut App) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
if let Some(window_handle) = cx.active_window() {
|
||||
window_handle
|
||||
.update(cx, |_, window, cx| {
|
||||
workspace.toggle_modal(window, cx, |_, cx| {
|
||||
CopilotCodeVerification::new(&copilot, cx)
|
||||
});
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
@@ -76,6 +88,7 @@ pub struct CopilotCodeVerification {
|
||||
status: Status,
|
||||
connect_clicked: bool,
|
||||
focus_handle: FocusHandle,
|
||||
copilot: Entity<Copilot>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
@@ -86,7 +99,20 @@ impl Focusable for CopilotCodeVerification {
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for CopilotCodeVerification {}
|
||||
impl ModalView for CopilotCodeVerification {}
|
||||
impl ModalView for CopilotCodeVerification {
|
||||
fn on_before_dismiss(
|
||||
&mut self,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> workspace::DismissDecision {
|
||||
self.copilot.update(cx, |copilot, cx| {
|
||||
if matches!(copilot.status(), Status::SigningIn { .. }) {
|
||||
copilot.sign_out(cx).detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
workspace::DismissDecision::Dismiss(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl CopilotCodeVerification {
|
||||
pub fn new(copilot: &Entity<Copilot>, cx: &mut Context<Self>) -> Self {
|
||||
@@ -95,6 +121,7 @@ impl CopilotCodeVerification {
|
||||
status,
|
||||
connect_clicked: false,
|
||||
focus_handle: cx.focus_handle(),
|
||||
copilot: copilot.clone(),
|
||||
_subscription: cx.observe(copilot, |this, copilot, cx| {
|
||||
let status = copilot.read(cx).status();
|
||||
match status {
|
||||
@@ -180,9 +207,12 @@ impl CopilotCodeVerification {
|
||||
.child(
|
||||
Button::new("copilot-enable-cancel-button", "Cancel")
|
||||
.full_width()
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_enabled_modal(cx: &mut Context<Self>) -> impl Element {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
@@ -216,16 +246,27 @@ impl CopilotCodeVerification {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_disabled_modal() -> impl Element {
|
||||
v_flex()
|
||||
.child(Headline::new("Copilot is disabled").size(HeadlineSize::Large))
|
||||
.child(Label::new("You can enable Copilot in your settings."))
|
||||
fn render_loading(window: &mut Window, _: &mut Context<Self>) -> impl Element {
|
||||
let loading_icon = svg()
|
||||
.size_8()
|
||||
.path(IconName::ArrowCircle.path())
|
||||
.text_color(window.text_style().color)
|
||||
.with_animation(
|
||||
"icon_circle_arrow",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|svg, delta| svg.with_transformation(Transformation::rotate(percentage(delta))),
|
||||
);
|
||||
|
||||
h_flex().justify_center().child(loading_icon)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CopilotCodeVerification {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let prompt = match &self.status {
|
||||
Status::SigningIn { prompt: None } => {
|
||||
Self::render_loading(window, cx).into_any_element()
|
||||
}
|
||||
Status::SigningIn {
|
||||
prompt: Some(prompt),
|
||||
} => Self::render_prompting_modal(self.connect_clicked, prompt, cx).into_any_element(),
|
||||
@@ -237,10 +278,6 @@ impl Render for CopilotCodeVerification {
|
||||
self.connect_clicked = false;
|
||||
Self::render_enabled_modal(cx).into_any_element()
|
||||
}
|
||||
Status::Disabled => {
|
||||
self.connect_clicked = false;
|
||||
Self::render_disabled_modal().into_any_element()
|
||||
}
|
||||
_ => div().into_any_element(),
|
||||
};
|
||||
|
||||
|
||||
@@ -198,13 +198,8 @@ impl ProjectDiagnosticsEditor {
|
||||
|
||||
let excerpts = cx.new(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::for_multibuffer(
|
||||
excerpts.clone(),
|
||||
Some(project_handle.clone()),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), window, cx);
|
||||
editor.set_vertical_scroll_margin(5, cx);
|
||||
editor.disable_inline_diagnostics();
|
||||
editor
|
||||
|
||||
@@ -169,10 +169,10 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
editor_blocks(&editor, cx),
|
||||
[
|
||||
(DisplayRow(0), FILE_HEADER.into()),
|
||||
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(16), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(18), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(27), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(15), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(16), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(25), EXCERPT_HEADER.into()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -186,7 +186,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"\n", // expand
|
||||
" let x = vec![];\n",
|
||||
" let y = vec![];\n",
|
||||
"\n", // supporting diagnostic
|
||||
@@ -198,7 +197,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
" c(y);\n",
|
||||
"\n", // supporting diagnostic
|
||||
" d(x);\n",
|
||||
"\n", // expand
|
||||
"\n", // context ellipsis
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
@@ -210,13 +208,11 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
" a(x);\n",
|
||||
"\n", // supporting diagnostic
|
||||
" b(y);\n",
|
||||
"\n", // expand
|
||||
"\n", // context ellipsis
|
||||
" c(y);\n",
|
||||
" d(x);\n",
|
||||
"\n", // supporting diagnostic
|
||||
"}",
|
||||
"\n", // expand
|
||||
)
|
||||
);
|
||||
|
||||
@@ -224,7 +220,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
[DisplayPoint::new(DisplayRow(13), 6)..DisplayPoint::new(DisplayRow(13), 6)]
|
||||
[DisplayPoint::new(DisplayRow(12), 6)..DisplayPoint::new(DisplayRow(12), 6)]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -260,12 +256,12 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
editor_blocks(&editor, cx),
|
||||
[
|
||||
(DisplayRow(0), FILE_HEADER.into()),
|
||||
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(8), FILE_HEADER.into()),
|
||||
(DisplayRow(12), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(25), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(27), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(36), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(7), FILE_HEADER.into()),
|
||||
(DisplayRow(9), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(22), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(23), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(32), EXCERPT_HEADER.into()),
|
||||
]
|
||||
);
|
||||
|
||||
@@ -280,7 +276,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"\n", // expand
|
||||
"const a: i32 = 'a';\n",
|
||||
"\n", // supporting diagnostic
|
||||
"const b: i32 = c;\n",
|
||||
@@ -292,8 +287,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"\n", // expand
|
||||
"\n", // expand
|
||||
" let x = vec![];\n",
|
||||
" let y = vec![];\n",
|
||||
"\n", // supporting diagnostic
|
||||
@@ -309,7 +302,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
"\n", // filename
|
||||
"\n", // expand
|
||||
"fn main() {\n",
|
||||
" let x = vec![];\n",
|
||||
"\n", // supporting diagnostic
|
||||
@@ -317,13 +309,11 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
" a(x);\n",
|
||||
"\n", // supporting diagnostic
|
||||
" b(y);\n",
|
||||
"\n", // expand
|
||||
"\n", // context ellipsis
|
||||
" c(y);\n",
|
||||
" d(x);\n",
|
||||
"\n", // supporting diagnostic
|
||||
"}",
|
||||
"\n", // expand
|
||||
)
|
||||
);
|
||||
|
||||
@@ -331,7 +321,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
[DisplayPoint::new(DisplayRow(22), 6)..DisplayPoint::new(DisplayRow(22), 6)]
|
||||
[DisplayPoint::new(DisplayRow(19), 6)..DisplayPoint::new(DisplayRow(19), 6)]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -380,14 +370,14 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
editor_blocks(&editor, cx),
|
||||
[
|
||||
(DisplayRow(0), FILE_HEADER.into()),
|
||||
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(8), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(10), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(15), FILE_HEADER.into()),
|
||||
(DisplayRow(19), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(32), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(34), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(43), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(7), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(8), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(13), FILE_HEADER.into()),
|
||||
(DisplayRow(15), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(28), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(29), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(38), EXCERPT_HEADER.into()),
|
||||
]
|
||||
);
|
||||
|
||||
@@ -402,7 +392,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"\n", // expand
|
||||
"const a: i32 = 'a';\n",
|
||||
"\n", // supporting diagnostic
|
||||
"const b: i32 = c;\n",
|
||||
@@ -410,7 +399,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"\n", // expand
|
||||
"const a: i32 = 'a';\n",
|
||||
"const b: i32 = c;\n",
|
||||
"\n", // supporting diagnostic
|
||||
@@ -422,8 +410,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"\n", // expand
|
||||
"\n", // expand
|
||||
" let x = vec![];\n",
|
||||
" let y = vec![];\n",
|
||||
"\n", // supporting diagnostic
|
||||
@@ -439,7 +425,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
"\n", // filename
|
||||
"\n", // expand
|
||||
"fn main() {\n",
|
||||
" let x = vec![];\n",
|
||||
"\n", // supporting diagnostic
|
||||
@@ -447,13 +432,11 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
" a(x);\n",
|
||||
"\n", // supporting diagnostic
|
||||
" b(y);\n",
|
||||
"\n", // expand
|
||||
"\n", // context ellipsis
|
||||
" c(y);\n",
|
||||
" d(x);\n",
|
||||
"\n", // supporting diagnostic
|
||||
"}",
|
||||
"\n", // expand
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -535,7 +518,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
editor_blocks(&editor, cx),
|
||||
[
|
||||
(DisplayRow(0), FILE_HEADER.into()),
|
||||
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -546,9 +529,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"\n", // expand
|
||||
"a();\n", //
|
||||
"b();", "\n", // expand
|
||||
"b();",
|
||||
)
|
||||
);
|
||||
|
||||
@@ -584,9 +566,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
editor_blocks(&editor, cx),
|
||||
[
|
||||
(DisplayRow(0), FILE_HEADER.into()),
|
||||
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(7), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(9), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(6), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(7), DIAGNOSTIC_HEADER.into()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -597,10 +579,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"\n", // expand
|
||||
"a();\n", // location
|
||||
"b();\n", //
|
||||
"\n", // expand
|
||||
"\n", // collapsed context
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
@@ -608,7 +588,6 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
"a();\n", // context
|
||||
"b();\n", //
|
||||
"c();", // context
|
||||
"\n", // expand
|
||||
)
|
||||
);
|
||||
|
||||
@@ -655,9 +634,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
editor_blocks(&editor, cx),
|
||||
[
|
||||
(DisplayRow(0), FILE_HEADER.into()),
|
||||
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(8), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(10), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(7), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(8), DIAGNOSTIC_HEADER.into()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -668,11 +647,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"\n", // expand
|
||||
"a();\n", // location
|
||||
"b();\n", //
|
||||
"c();\n", // context
|
||||
"\n", // expand
|
||||
"\n", // collapsed context
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
@@ -680,7 +657,6 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
"b();\n", // context
|
||||
"c();\n", //
|
||||
"d();", // context
|
||||
"\n", // expand
|
||||
)
|
||||
);
|
||||
|
||||
@@ -716,9 +692,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
editor_blocks(&editor, cx),
|
||||
[
|
||||
(DisplayRow(0), FILE_HEADER.into()),
|
||||
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(8), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(10), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
|
||||
(DisplayRow(7), EXCERPT_HEADER.into()),
|
||||
(DisplayRow(8), DIAGNOSTIC_HEADER.into()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -729,11 +705,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"\n", // expand
|
||||
"b();\n", // location
|
||||
"c();\n", //
|
||||
"d();\n", // context
|
||||
"\n", // expand
|
||||
"\n", // collapsed context
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
@@ -741,7 +715,6 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
"c();\n", // context
|
||||
"d();\n", //
|
||||
"e();", // context
|
||||
"\n", // expand
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! This module contains all actions supported by [`Editor`].
|
||||
use super::*;
|
||||
use gpui::{action_as, action_with_deprecated_aliases};
|
||||
use gpui::{action_as, action_with_deprecated_aliases, actions};
|
||||
use schemars::JsonSchema;
|
||||
use util::serde::default_true;
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
|
||||
@@ -248,7 +248,7 @@ impl_actions!(
|
||||
]
|
||||
);
|
||||
|
||||
gpui::actions!(
|
||||
actions!(
|
||||
editor,
|
||||
[
|
||||
AcceptEditPrediction,
|
||||
@@ -404,6 +404,7 @@ gpui::actions!(
|
||||
ShowCharacterPalette,
|
||||
ShowEditPrediction,
|
||||
ShowSignatureHelp,
|
||||
ShowWordCompletions,
|
||||
ShuffleLines,
|
||||
SortLinesCaseInsensitive,
|
||||
SortLinesCaseSensitive,
|
||||
|
||||
@@ -118,10 +118,8 @@ impl DisplayMap {
|
||||
font: Font,
|
||||
font_size: Pixels,
|
||||
wrap_width: Option<Pixels>,
|
||||
show_excerpt_controls: bool,
|
||||
buffer_header_height: u32,
|
||||
excerpt_header_height: u32,
|
||||
excerpt_footer_height: u32,
|
||||
fold_placeholder: FoldPlaceholder,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -134,13 +132,7 @@ impl DisplayMap {
|
||||
let (fold_map, snapshot) = FoldMap::new(snapshot);
|
||||
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
|
||||
let (wrap_map, snapshot) = WrapMap::new(snapshot, font, font_size, wrap_width, cx);
|
||||
let block_map = BlockMap::new(
|
||||
snapshot,
|
||||
show_excerpt_controls,
|
||||
buffer_header_height,
|
||||
excerpt_header_height,
|
||||
excerpt_footer_height,
|
||||
);
|
||||
let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
|
||||
|
||||
cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
|
||||
|
||||
@@ -555,10 +547,6 @@ impl DisplayMap {
|
||||
pub fn is_rewrapping(&self, cx: &gpui::App) -> bool {
|
||||
self.wrap_map.read(cx).is_rewrapping()
|
||||
}
|
||||
|
||||
pub fn show_excerpt_controls(&self) -> bool {
|
||||
self.block_map.show_excerpt_controls()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -1102,8 +1090,8 @@ impl DisplaySnapshot {
|
||||
.map(|(row, block)| (DisplayRow(row), block))
|
||||
}
|
||||
|
||||
pub fn sticky_header_excerpt(&self, row: DisplayRow) -> Option<StickyHeaderExcerpt<'_>> {
|
||||
self.block_snapshot.sticky_header_excerpt(row.0)
|
||||
pub fn sticky_header_excerpt(&self, row: f32) -> Option<StickyHeaderExcerpt<'_>> {
|
||||
self.block_snapshot.sticky_header_excerpt(row)
|
||||
}
|
||||
|
||||
pub fn block_for_id(&self, id: BlockId) -> Option<Block> {
|
||||
@@ -1301,10 +1289,6 @@ impl DisplaySnapshot {
|
||||
self.block_snapshot.buffer_header_height
|
||||
}
|
||||
|
||||
pub fn excerpt_footer_height(&self) -> u32 {
|
||||
self.block_snapshot.excerpt_footer_height
|
||||
}
|
||||
|
||||
pub fn excerpt_header_height(&self) -> u32 {
|
||||
self.block_snapshot.excerpt_header_height
|
||||
}
|
||||
@@ -1514,10 +1498,8 @@ pub mod tests {
|
||||
font,
|
||||
font_size,
|
||||
wrap_width,
|
||||
true,
|
||||
buffer_start_excerpt_header_height,
|
||||
excerpt_header_height,
|
||||
0,
|
||||
FoldPlaceholder::test(),
|
||||
cx,
|
||||
)
|
||||
@@ -1764,10 +1746,8 @@ pub mod tests {
|
||||
font("Helvetica"),
|
||||
font_size,
|
||||
wrap_width,
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
FoldPlaceholder::test(),
|
||||
cx,
|
||||
)
|
||||
@@ -1875,10 +1855,8 @@ pub mod tests {
|
||||
font("Helvetica"),
|
||||
font_size,
|
||||
None,
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
FoldPlaceholder::test(),
|
||||
cx,
|
||||
)
|
||||
@@ -1938,8 +1916,6 @@ pub mod tests {
|
||||
font("Helvetica"),
|
||||
font_size,
|
||||
None,
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
FoldPlaceholder::test(),
|
||||
@@ -2032,8 +2008,6 @@ pub mod tests {
|
||||
font("Helvetica"),
|
||||
font_size,
|
||||
None,
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
FoldPlaceholder::test(),
|
||||
@@ -2134,10 +2108,8 @@ pub mod tests {
|
||||
font("Courier"),
|
||||
px(16.0),
|
||||
None,
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
FoldPlaceholder::test(),
|
||||
cx,
|
||||
)
|
||||
@@ -2239,10 +2211,8 @@ pub mod tests {
|
||||
font("Courier"),
|
||||
px(16.0),
|
||||
None,
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
FoldPlaceholder::test(),
|
||||
cx,
|
||||
)
|
||||
@@ -2328,10 +2298,8 @@ pub mod tests {
|
||||
font("Courier"),
|
||||
px(16.0),
|
||||
None,
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
FoldPlaceholder::test(),
|
||||
cx,
|
||||
)
|
||||
@@ -2472,10 +2440,8 @@ pub mod tests {
|
||||
font("Courier"),
|
||||
font_size,
|
||||
Some(px(40.0)),
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
FoldPlaceholder::test(),
|
||||
cx,
|
||||
)
|
||||
@@ -2556,8 +2522,6 @@ pub mod tests {
|
||||
font("Courier"),
|
||||
font_size,
|
||||
None,
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
FoldPlaceholder::test(),
|
||||
@@ -2682,10 +2646,8 @@ pub mod tests {
|
||||
font("Helvetica"),
|
||||
font_size,
|
||||
None,
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
FoldPlaceholder::test(),
|
||||
cx,
|
||||
);
|
||||
@@ -2721,10 +2683,8 @@ pub mod tests {
|
||||
font("Helvetica"),
|
||||
font_size,
|
||||
None,
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
FoldPlaceholder::test(),
|
||||
cx,
|
||||
)
|
||||
@@ -2798,10 +2758,8 @@ pub mod tests {
|
||||
font("Helvetica"),
|
||||
font_size,
|
||||
None,
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
FoldPlaceholder::test(),
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -37,10 +37,8 @@ pub struct BlockMap {
|
||||
custom_blocks: Vec<Arc<CustomBlock>>,
|
||||
custom_blocks_by_id: TreeMap<CustomBlockId, Arc<CustomBlock>>,
|
||||
transforms: RefCell<SumTree<Transform>>,
|
||||
show_excerpt_controls: bool,
|
||||
buffer_header_height: u32,
|
||||
excerpt_header_height: u32,
|
||||
excerpt_footer_height: u32,
|
||||
pub(super) folded_buffers: HashSet<BufferId>,
|
||||
}
|
||||
|
||||
@@ -58,7 +56,6 @@ pub struct BlockSnapshot {
|
||||
custom_blocks_by_id: TreeMap<CustomBlockId, Arc<CustomBlock>>,
|
||||
pub(super) buffer_header_height: u32,
|
||||
pub(super) excerpt_header_height: u32,
|
||||
pub(super) excerpt_footer_height: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
@@ -285,14 +282,12 @@ pub enum Block {
|
||||
first_excerpt: ExcerptInfo,
|
||||
prev_excerpt: Option<ExcerptInfo>,
|
||||
height: u32,
|
||||
show_excerpt_controls: bool,
|
||||
},
|
||||
ExcerptBoundary {
|
||||
prev_excerpt: Option<ExcerptInfo>,
|
||||
next_excerpt: Option<ExcerptInfo>,
|
||||
height: u32,
|
||||
starts_new_buffer: bool,
|
||||
show_excerpt_controls: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -362,13 +357,11 @@ impl Debug for Block {
|
||||
first_excerpt,
|
||||
prev_excerpt,
|
||||
height,
|
||||
show_excerpt_controls,
|
||||
} => f
|
||||
.debug_struct("FoldedBuffer")
|
||||
.field("first_excerpt", &first_excerpt)
|
||||
.field("prev_excerpt", prev_excerpt)
|
||||
.field("height", height)
|
||||
.field("show_excerpt_controls", show_excerpt_controls)
|
||||
.finish(),
|
||||
Self::ExcerptBoundary {
|
||||
starts_new_buffer,
|
||||
@@ -413,10 +406,8 @@ pub struct BlockRows<'a> {
|
||||
impl BlockMap {
|
||||
pub fn new(
|
||||
wrap_snapshot: WrapSnapshot,
|
||||
show_excerpt_controls: bool,
|
||||
buffer_header_height: u32,
|
||||
excerpt_header_height: u32,
|
||||
excerpt_footer_height: u32,
|
||||
) -> Self {
|
||||
let row_count = wrap_snapshot.max_point().row() + 1;
|
||||
let mut transforms = SumTree::default();
|
||||
@@ -428,10 +419,8 @@ impl BlockMap {
|
||||
folded_buffers: HashSet::default(),
|
||||
transforms: RefCell::new(transforms),
|
||||
wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
|
||||
show_excerpt_controls,
|
||||
buffer_header_height,
|
||||
excerpt_header_height,
|
||||
excerpt_footer_height,
|
||||
};
|
||||
map.sync(
|
||||
&wrap_snapshot,
|
||||
@@ -454,7 +443,6 @@ impl BlockMap {
|
||||
custom_blocks_by_id: self.custom_blocks_by_id.clone(),
|
||||
buffer_header_height: self.buffer_header_height,
|
||||
excerpt_header_height: self.excerpt_header_height,
|
||||
excerpt_footer_height: self.excerpt_footer_height,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -650,8 +638,6 @@ impl BlockMap {
|
||||
|
||||
if buffer.show_headers() {
|
||||
blocks_in_edit.extend(BlockMap::header_and_footer_blocks(
|
||||
self.show_excerpt_controls,
|
||||
self.excerpt_footer_height,
|
||||
self.buffer_header_height,
|
||||
self.excerpt_header_height,
|
||||
buffer,
|
||||
@@ -722,13 +708,7 @@ impl BlockMap {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_excerpt_controls(&self) -> bool {
|
||||
self.show_excerpt_controls
|
||||
}
|
||||
|
||||
fn header_and_footer_blocks<'a, R, T>(
|
||||
show_excerpt_controls: bool,
|
||||
excerpt_footer_height: u32,
|
||||
buffer_header_height: u32,
|
||||
excerpt_header_height: u32,
|
||||
buffer: &'a multi_buffer::MultiBufferSnapshot,
|
||||
@@ -774,11 +754,6 @@ impl BlockMap {
|
||||
.filter(|prev| !folded_buffers.contains(&prev.buffer_id));
|
||||
|
||||
let mut height = 0;
|
||||
if prev_excerpt.is_some() {
|
||||
if show_excerpt_controls {
|
||||
height += excerpt_footer_height;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(new_buffer_id) = new_buffer_id {
|
||||
let first_excerpt = excerpt_boundary.next.clone().unwrap();
|
||||
@@ -812,7 +787,6 @@ impl BlockMap {
|
||||
Block::FoldedBuffer {
|
||||
prev_excerpt,
|
||||
height: height + buffer_header_height,
|
||||
show_excerpt_controls,
|
||||
first_excerpt,
|
||||
},
|
||||
));
|
||||
@@ -822,9 +796,6 @@ impl BlockMap {
|
||||
if excerpt_boundary.next.is_some() {
|
||||
if new_buffer_id.is_some() {
|
||||
height += buffer_header_height;
|
||||
if show_excerpt_controls {
|
||||
height += excerpt_header_height;
|
||||
}
|
||||
} else {
|
||||
height += excerpt_header_height;
|
||||
}
|
||||
@@ -845,7 +816,6 @@ impl BlockMap {
|
||||
next_excerpt: excerpt_boundary.next,
|
||||
height,
|
||||
starts_new_buffer: new_buffer_id.is_some(),
|
||||
show_excerpt_controls,
|
||||
},
|
||||
))
|
||||
})
|
||||
@@ -1432,7 +1402,8 @@ impl BlockSnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sticky_header_excerpt(&self, top_row: u32) -> Option<StickyHeaderExcerpt<'_>> {
|
||||
pub fn sticky_header_excerpt(&self, position: f32) -> Option<StickyHeaderExcerpt<'_>> {
|
||||
let top_row = position as u32;
|
||||
let mut cursor = self.transforms.cursor::<BlockRow>(&());
|
||||
cursor.seek(&BlockRow(top_row), Bias::Left, &());
|
||||
|
||||
@@ -1445,19 +1416,13 @@ impl BlockSnapshot {
|
||||
prev_excerpt,
|
||||
next_excerpt,
|
||||
starts_new_buffer,
|
||||
show_excerpt_controls,
|
||||
..
|
||||
}) => {
|
||||
let matches_start = if *show_excerpt_controls && prev_excerpt.is_some() {
|
||||
start < top_row
|
||||
} else {
|
||||
start <= top_row
|
||||
};
|
||||
let matches_start = (start as f32) < position;
|
||||
|
||||
if matches_start && top_row <= end {
|
||||
return next_excerpt.as_ref().map(|excerpt| StickyHeaderExcerpt {
|
||||
next_buffer_row: None,
|
||||
next_excerpt_controls_present: *show_excerpt_controls,
|
||||
excerpt,
|
||||
});
|
||||
}
|
||||
@@ -1467,7 +1432,6 @@ impl BlockSnapshot {
|
||||
return prev_excerpt.as_ref().map(|excerpt| StickyHeaderExcerpt {
|
||||
excerpt,
|
||||
next_buffer_row,
|
||||
next_excerpt_controls_present: *show_excerpt_controls,
|
||||
});
|
||||
}
|
||||
Some(Block::FoldedBuffer {
|
||||
@@ -1476,7 +1440,6 @@ impl BlockSnapshot {
|
||||
}) if top_row <= start => {
|
||||
return Some(StickyHeaderExcerpt {
|
||||
next_buffer_row: Some(end),
|
||||
next_excerpt_controls_present: false,
|
||||
excerpt,
|
||||
});
|
||||
}
|
||||
@@ -1785,7 +1748,6 @@ impl BlockChunks<'_> {
|
||||
|
||||
pub struct StickyHeaderExcerpt<'a> {
|
||||
pub excerpt: &'a ExcerptInfo,
|
||||
pub next_excerpt_controls_present: bool,
|
||||
pub next_buffer_row: Option<u32>,
|
||||
}
|
||||
|
||||
@@ -2066,7 +2028,7 @@ mod tests {
|
||||
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
|
||||
let (wrap_map, wraps_snapshot) =
|
||||
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1);
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
|
||||
|
||||
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
let block_ids = writer.insert(vec![
|
||||
@@ -2279,14 +2241,11 @@ mod tests {
|
||||
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx);
|
||||
|
||||
let block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1);
|
||||
let block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
|
||||
let snapshot = block_map.read(wraps_snapshot, Default::default());
|
||||
|
||||
// Each excerpt has a header above and footer below. Excerpts are also *separated* by a newline.
|
||||
assert_eq!(
|
||||
snapshot.text(),
|
||||
"\n\nBuff\ner 1\n\n\n\nBuff\ner 2\n\n\n\nBuff\ner 3\n"
|
||||
);
|
||||
assert_eq!(snapshot.text(), "\nBuff\ner 1\n\nBuff\ner 2\n\nBuff\ner 3");
|
||||
|
||||
let blocks: Vec<_> = snapshot
|
||||
.blocks_in_range(0..u32::MAX)
|
||||
@@ -2295,10 +2254,9 @@ mod tests {
|
||||
assert_eq!(
|
||||
blocks,
|
||||
vec![
|
||||
(0..2, BlockId::ExcerptBoundary(Some(excerpt_ids[0]))), // path, header
|
||||
(4..7, BlockId::ExcerptBoundary(Some(excerpt_ids[1]))), // footer, path, header
|
||||
(9..12, BlockId::ExcerptBoundary(Some(excerpt_ids[2]))), // footer, path, header
|
||||
(14..15, BlockId::ExcerptBoundary(None)), // footer
|
||||
(0..1, BlockId::ExcerptBoundary(Some(excerpt_ids[0]))), // path, header
|
||||
(3..4, BlockId::ExcerptBoundary(Some(excerpt_ids[1]))), // path, header
|
||||
(6..7, BlockId::ExcerptBoundary(Some(excerpt_ids[2]))), // path, header
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -2317,7 +2275,7 @@ mod tests {
|
||||
let (_tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
|
||||
let (_wrap_map, wraps_snapshot) =
|
||||
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0);
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
|
||||
|
||||
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
let block_ids = writer.insert(vec![
|
||||
@@ -2420,7 +2378,7 @@ mod tests {
|
||||
let (_, wraps_snapshot) = cx.update(|cx| {
|
||||
WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), Some(px(60.)), cx)
|
||||
});
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 0);
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
|
||||
|
||||
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
writer.insert(vec![
|
||||
@@ -2464,7 +2422,7 @@ mod tests {
|
||||
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, tab_size);
|
||||
let (wrap_map, wraps_snapshot) =
|
||||
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0);
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
|
||||
|
||||
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
let replace_block_id = writer.insert(vec![BlockProperties {
|
||||
@@ -2631,12 +2589,12 @@ mod tests {
|
||||
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
let (_, wrap_snapshot) =
|
||||
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
|
||||
let mut block_map = BlockMap::new(wrap_snapshot.clone(), true, 2, 1, 1);
|
||||
let mut block_map = BlockMap::new(wrap_snapshot.clone(), 2, 1);
|
||||
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
|
||||
|
||||
assert_eq!(
|
||||
blocks_snapshot.text(),
|
||||
"\n\n\n111\n\n\n\n\n222\n\n\n333\n\n\n444\n\n\n\n\n555\n\n\n666\n"
|
||||
"\n\n111\n\n\n222\n\n333\n\n444\n\n\n555\n\n666"
|
||||
);
|
||||
assert_eq!(
|
||||
blocks_snapshot
|
||||
@@ -2644,30 +2602,21 @@ mod tests {
|
||||
.map(|i| i.buffer_row)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(0),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
None,
|
||||
Some(2),
|
||||
None,
|
||||
None,
|
||||
Some(3),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(4),
|
||||
None,
|
||||
None,
|
||||
Some(5),
|
||||
None,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -2715,7 +2664,7 @@ mod tests {
|
||||
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
|
||||
assert_eq!(
|
||||
blocks_snapshot.text(),
|
||||
"\n\n\n111\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n"
|
||||
"\n\n111\n\n\n\n222\n\n\n333\n\n444\n\n\n\n\n555\n\n666\n"
|
||||
);
|
||||
assert_eq!(
|
||||
blocks_snapshot
|
||||
@@ -2723,35 +2672,26 @@ mod tests {
|
||||
.map(|i| i.buffer_row)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(0),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(2),
|
||||
None,
|
||||
None,
|
||||
Some(3),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(4),
|
||||
None,
|
||||
None,
|
||||
Some(5),
|
||||
None,
|
||||
None,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -2793,7 +2733,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
blocks_snapshot.text(),
|
||||
"\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n"
|
||||
"\n\n\n\n\n222\n\n\n333\n\n444\n\n\n\n\n555\n\n666\n"
|
||||
);
|
||||
assert_eq!(
|
||||
blocks_snapshot
|
||||
@@ -2806,27 +2746,20 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(2),
|
||||
None,
|
||||
None,
|
||||
Some(3),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(4),
|
||||
None,
|
||||
None,
|
||||
Some(5),
|
||||
None,
|
||||
None,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -2862,7 +2795,7 @@ mod tests {
|
||||
.count(),
|
||||
"Should have two folded blocks, producing headers"
|
||||
);
|
||||
assert_eq!(blocks_snapshot.text(), "\n\n\n\n\n\n\n\n555\n\n\n666\n\n");
|
||||
assert_eq!(blocks_snapshot.text(), "\n\n\n\n\n\n\n555\n\n666\n");
|
||||
assert_eq!(
|
||||
blocks_snapshot
|
||||
.row_infos(BlockRow(0))
|
||||
@@ -2876,13 +2809,10 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(4),
|
||||
None,
|
||||
None,
|
||||
Some(5),
|
||||
None,
|
||||
None,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -2917,7 +2847,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
blocks_snapshot.text(),
|
||||
"\n\n\n\n111\n\n\n\n\n\n\n\n555\n\n\n666\n\n",
|
||||
"\n\n\n111\n\n\n\n\n\n555\n\n666\n",
|
||||
"Should have extra newline for 111 buffer, due to a new block added when it was folded"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -2929,21 +2859,16 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(0),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(4),
|
||||
None,
|
||||
None,
|
||||
Some(5),
|
||||
None,
|
||||
None,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -2974,7 +2899,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
blocks_snapshot.text(),
|
||||
"\n\n\n\n111\n\n\n\n\n",
|
||||
"\n\n\n111\n\n\n\n",
|
||||
"Should have a single, first buffer left after folding"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -2982,18 +2907,7 @@ mod tests {
|
||||
.row_infos(BlockRow(0))
|
||||
.map(|i| i.buffer_row)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(0),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
]
|
||||
vec![None, None, None, Some(0), None, None, None, None,]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3020,10 +2934,10 @@ mod tests {
|
||||
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
let (_, wrap_snapshot) =
|
||||
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
|
||||
let mut block_map = BlockMap::new(wrap_snapshot.clone(), true, 2, 1, 1);
|
||||
let mut block_map = BlockMap::new(wrap_snapshot.clone(), 2, 1);
|
||||
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
|
||||
|
||||
assert_eq!(blocks_snapshot.text(), "\n\n\n111\n");
|
||||
assert_eq!(blocks_snapshot.text(), "\n\n111");
|
||||
|
||||
let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
|
||||
buffer.read_with(cx, |buffer, cx| {
|
||||
@@ -3077,11 +2991,9 @@ mod tests {
|
||||
let font_size = px(14.0);
|
||||
let buffer_start_header_height = rng.gen_range(1..=5);
|
||||
let excerpt_header_height = rng.gen_range(1..=5);
|
||||
let excerpt_footer_height = rng.gen_range(1..=5);
|
||||
|
||||
log::info!("Wrap width: {:?}", wrap_width);
|
||||
log::info!("Excerpt Header Height: {:?}", excerpt_header_height);
|
||||
log::info!("Excerpt Footer Height: {:?}", excerpt_footer_height);
|
||||
let is_singleton = rng.gen();
|
||||
let buffer = if is_singleton {
|
||||
let len = rng.gen_range(0..10);
|
||||
@@ -3108,10 +3020,8 @@ mod tests {
|
||||
cx.update(|cx| WrapMap::new(tab_snapshot, font, font_size, wrap_width, cx));
|
||||
let mut block_map = BlockMap::new(
|
||||
wraps_snapshot,
|
||||
true,
|
||||
buffer_start_header_height,
|
||||
excerpt_header_height,
|
||||
excerpt_footer_height,
|
||||
);
|
||||
|
||||
for _ in 0..operations {
|
||||
@@ -3329,8 +3239,6 @@ mod tests {
|
||||
|
||||
// Note that this needs to be synced with the related section in BlockMap::sync
|
||||
expected_blocks.extend(BlockMap::header_and_footer_blocks(
|
||||
true,
|
||||
excerpt_footer_height,
|
||||
buffer_start_header_height,
|
||||
excerpt_header_height,
|
||||
&buffer_snapshot,
|
||||
|
||||
@@ -983,6 +983,7 @@ impl Iterator for WrapRows<'_> {
|
||||
buffer_row: None,
|
||||
multibuffer_row: None,
|
||||
diff_status,
|
||||
expand_info: None,
|
||||
}
|
||||
} else {
|
||||
buffer_row
|
||||
|
||||
@@ -106,7 +106,7 @@ use language::{
|
||||
point_from_lsp, text_diff_with_options, AutoindentMode, BracketMatch, BracketPair, Buffer,
|
||||
Capability, CharKind, CodeLabel, CursorShape, Diagnostic, DiffOptions, EditPredictionsMode,
|
||||
EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point,
|
||||
Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
|
||||
Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
|
||||
};
|
||||
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
|
||||
use linked_editing_ranges::refresh_linked_ranges;
|
||||
@@ -196,7 +196,6 @@ use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState};
|
||||
|
||||
pub const FILE_HEADER_HEIGHT: u32 = 2;
|
||||
pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1;
|
||||
pub const MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT: u32 = 1;
|
||||
pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2;
|
||||
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
const MAX_LINE_LEN: usize = 1024;
|
||||
@@ -1092,7 +1091,6 @@ impl Editor {
|
||||
EditorMode::SingleLine { auto_width: false },
|
||||
buffer,
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -1101,7 +1099,7 @@ impl Editor {
|
||||
pub fn multi_line(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
Self::new(EditorMode::Full, buffer, None, false, window, cx)
|
||||
Self::new(EditorMode::Full, buffer, None, window, cx)
|
||||
}
|
||||
|
||||
pub fn auto_width(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
@@ -1111,7 +1109,6 @@ impl Editor {
|
||||
EditorMode::SingleLine { auto_width: true },
|
||||
buffer,
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -1124,7 +1121,6 @@ impl Editor {
|
||||
EditorMode::AutoHeight { max_lines },
|
||||
buffer,
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -1137,33 +1133,23 @@ impl Editor {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
Self::new(EditorMode::Full, buffer, project, false, window, cx)
|
||||
Self::new(EditorMode::Full, buffer, project, window, cx)
|
||||
}
|
||||
|
||||
pub fn for_multibuffer(
|
||||
buffer: Entity<MultiBuffer>,
|
||||
project: Option<Entity<Project>>,
|
||||
show_excerpt_controls: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
EditorMode::Full,
|
||||
buffer,
|
||||
project,
|
||||
show_excerpt_controls,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
Self::new(EditorMode::Full, buffer, project, window, cx)
|
||||
}
|
||||
|
||||
pub fn clone(&self, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let show_excerpt_controls = self.display_map.read(cx).show_excerpt_controls();
|
||||
let mut clone = Self::new(
|
||||
self.mode,
|
||||
self.buffer.clone(),
|
||||
self.project.clone(),
|
||||
show_excerpt_controls,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -1183,7 +1169,6 @@ impl Editor {
|
||||
mode: EditorMode,
|
||||
buffer: Entity<MultiBuffer>,
|
||||
project: Option<Entity<Project>>,
|
||||
show_excerpt_controls: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -1228,10 +1213,8 @@ impl Editor {
|
||||
style.font(),
|
||||
font_size,
|
||||
None,
|
||||
show_excerpt_controls,
|
||||
FILE_HEADER_HEIGHT,
|
||||
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT,
|
||||
fold_placeholder,
|
||||
cx,
|
||||
)
|
||||
@@ -1358,10 +1341,7 @@ impl Editor {
|
||||
project,
|
||||
blink_manager: blink_manager.clone(),
|
||||
show_local_selections: true,
|
||||
show_scrollbars: match mode {
|
||||
EditorMode::AutoHeight { .. } | EditorMode::SingleLine { .. } => false,
|
||||
EditorMode::Full => true,
|
||||
},
|
||||
show_scrollbars: true,
|
||||
mode,
|
||||
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
|
||||
show_gutter: mode == EditorMode::Full,
|
||||
@@ -3997,20 +3977,34 @@ impl Editor {
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn show_word_completions(
|
||||
&mut self,
|
||||
_: &ShowWordCompletions,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.open_completions_menu(true, None, window, cx);
|
||||
}
|
||||
|
||||
pub fn show_completions(
|
||||
&mut self,
|
||||
options: &ShowCompletions,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.open_completions_menu(false, options.trigger.as_deref(), window, cx);
|
||||
}
|
||||
|
||||
fn open_completions_menu(
|
||||
&mut self,
|
||||
ignore_completion_provider: bool,
|
||||
trigger: Option<&str>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.pending_rename.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(provider) = self.completion_provider.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !self.snippet_stack.is_empty() && self.context_menu.borrow().as_ref().is_some() {
|
||||
return;
|
||||
}
|
||||
@@ -4032,14 +4026,14 @@ impl Editor {
|
||||
|
||||
let query = Self::completion_query(&self.buffer.read(cx).read(cx), position);
|
||||
|
||||
let trigger_kind = match &options.trigger {
|
||||
let trigger_kind = match trigger {
|
||||
Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => {
|
||||
CompletionTriggerKind::TRIGGER_CHARACTER
|
||||
}
|
||||
_ => CompletionTriggerKind::INVOKED,
|
||||
};
|
||||
let completion_context = CompletionContext {
|
||||
trigger_character: options.trigger.as_ref().and_then(|trigger| {
|
||||
trigger_character: trigger.and_then(|trigger| {
|
||||
if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER {
|
||||
Some(String::from(trigger))
|
||||
} else {
|
||||
@@ -4048,8 +4042,7 @@ impl Editor {
|
||||
}),
|
||||
trigger_kind,
|
||||
};
|
||||
let completions =
|
||||
provider.completions(&buffer, buffer_position, completion_context, window, cx);
|
||||
|
||||
let (old_range, word_kind) = buffer_snapshot.surrounding_word(buffer_position);
|
||||
let (old_range, word_to_exclude) = if word_kind == Some(CharKind::Word) {
|
||||
let word_to_exclude = buffer_snapshot
|
||||
@@ -4087,15 +4080,49 @@ impl Editor {
|
||||
);
|
||||
let word_search_range = buffer_snapshot.point_to_offset(min_word_search)
|
||||
..buffer_snapshot.point_to_offset(max_word_search);
|
||||
let words = match completion_settings.words {
|
||||
WordsCompletionMode::Disabled => Task::ready(HashMap::default()),
|
||||
WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => {
|
||||
cx.background_spawn(async move {
|
||||
buffer_snapshot.words_in_range(None, word_search_range)
|
||||
})
|
||||
|
||||
let provider = self
|
||||
.completion_provider
|
||||
.as_ref()
|
||||
.filter(|_| !ignore_completion_provider);
|
||||
let skip_digits = query
|
||||
.as_ref()
|
||||
.map_or(true, |query| !query.chars().any(|c| c.is_digit(10)));
|
||||
|
||||
let (mut words, provided_completions) = match provider {
|
||||
Some(provider) => {
|
||||
let completions =
|
||||
provider.completions(&buffer, buffer_position, completion_context, window, cx);
|
||||
|
||||
let words = match completion_settings.words {
|
||||
WordsCompletionMode::Disabled => Task::ready(HashMap::default()),
|
||||
WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => cx
|
||||
.background_spawn(async move {
|
||||
buffer_snapshot.words_in_range(WordsQuery {
|
||||
fuzzy_contents: None,
|
||||
range: word_search_range,
|
||||
skip_digits,
|
||||
})
|
||||
}),
|
||||
};
|
||||
|
||||
(words, completions)
|
||||
}
|
||||
None => (
|
||||
cx.background_spawn(async move {
|
||||
buffer_snapshot.words_in_range(WordsQuery {
|
||||
fuzzy_contents: None,
|
||||
range: word_search_range,
|
||||
skip_digits,
|
||||
})
|
||||
}),
|
||||
Task::ready(Ok(None)),
|
||||
),
|
||||
};
|
||||
let sort_completions = provider.sort_completions();
|
||||
|
||||
let sort_completions = provider
|
||||
.as_ref()
|
||||
.map_or(true, |provider| provider.sort_completions());
|
||||
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
let task = cx.spawn_in(window, |editor, mut cx| {
|
||||
@@ -4103,52 +4130,34 @@ impl Editor {
|
||||
editor.update(&mut cx, |this, _| {
|
||||
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
|
||||
})?;
|
||||
let mut completions = completions.await.log_err().unwrap_or_default();
|
||||
|
||||
match completion_settings.words {
|
||||
WordsCompletionMode::Enabled => {
|
||||
completions.extend(
|
||||
words
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|(word, _)| word_to_exclude.as_ref() != Some(word))
|
||||
.map(|(word, word_range)| Completion {
|
||||
old_range: old_range.clone(),
|
||||
new_text: word.clone(),
|
||||
label: CodeLabel::plain(word, None),
|
||||
documentation: None,
|
||||
source: CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved: false,
|
||||
},
|
||||
confirm: None,
|
||||
}),
|
||||
);
|
||||
let mut completions = Vec::new();
|
||||
if let Some(provided_completions) = provided_completions.await.log_err().flatten() {
|
||||
completions.extend(provided_completions);
|
||||
if completion_settings.words == WordsCompletionMode::Fallback {
|
||||
words = Task::ready(HashMap::default());
|
||||
}
|
||||
WordsCompletionMode::Fallback => {
|
||||
if completions.is_empty() {
|
||||
completions.extend(
|
||||
words
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|(word, _)| word_to_exclude.as_ref() != Some(word))
|
||||
.map(|(word, word_range)| Completion {
|
||||
old_range: old_range.clone(),
|
||||
new_text: word.clone(),
|
||||
label: CodeLabel::plain(word, None),
|
||||
documentation: None,
|
||||
source: CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved: false,
|
||||
},
|
||||
confirm: None,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
WordsCompletionMode::Disabled => {}
|
||||
}
|
||||
|
||||
let mut words = words.await;
|
||||
if let Some(word_to_exclude) = &word_to_exclude {
|
||||
words.remove(word_to_exclude);
|
||||
}
|
||||
for lsp_completion in &completions {
|
||||
words.remove(&lsp_completion.new_text);
|
||||
}
|
||||
completions.extend(words.into_iter().map(|(word, word_range)| Completion {
|
||||
old_range: old_range.clone(),
|
||||
new_text: word.clone(),
|
||||
label: CodeLabel::plain(word, None),
|
||||
documentation: None,
|
||||
source: CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved: false,
|
||||
},
|
||||
confirm: None,
|
||||
}));
|
||||
|
||||
let menu = if completions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -4205,7 +4214,7 @@ impl Editor {
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok::<_, anyhow::Error>(())
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
@@ -4693,8 +4702,8 @@ impl Editor {
|
||||
|
||||
workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let editor = cx
|
||||
.new(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), true, window, cx));
|
||||
let editor =
|
||||
cx.new(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), window, cx));
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.highlight_background::<Self>(
|
||||
@@ -12381,7 +12390,6 @@ impl Editor {
|
||||
Editor::for_multibuffer(
|
||||
excerpt_buffer,
|
||||
Some(workspace.project().clone()),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -16779,8 +16787,7 @@ fn wrap_with_prefix(
|
||||
is_whitespace,
|
||||
} in tokenizer
|
||||
{
|
||||
if (current_line_len + grapheme_len) > wrap_column && (current_line_len != line_prefix_len)
|
||||
{
|
||||
if current_line_len + grapheme_len > wrap_column && current_line_len != line_prefix_len {
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
@@ -16932,7 +16939,7 @@ pub trait CompletionProvider {
|
||||
trigger: CompletionContext,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Vec<Completion>>>;
|
||||
) -> Task<Result<Option<Vec<Completion>>>>;
|
||||
|
||||
fn resolve_completions(
|
||||
&self,
|
||||
@@ -17172,15 +17179,25 @@ impl CompletionProvider for Entity<Project> {
|
||||
options: CompletionContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Vec<Completion>>> {
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
self.update(cx, |project, cx| {
|
||||
let snippets = snippet_completions(project, buffer, buffer_position, cx);
|
||||
let project_completions = project.completions(buffer, buffer_position, options, cx);
|
||||
cx.background_spawn(async move {
|
||||
let mut completions = project_completions.await?;
|
||||
let snippets_completions = snippets.await?;
|
||||
completions.extend(snippets_completions);
|
||||
Ok(completions)
|
||||
match project_completions.await? {
|
||||
Some(mut completions) => {
|
||||
completions.extend(snippets_completions);
|
||||
Ok(Some(completions))
|
||||
}
|
||||
None => {
|
||||
if snippets_completions.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(snippets_completions))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7676,7 +7676,6 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
|
||||
EditorMode::Full,
|
||||
multi_buffer,
|
||||
Some(project.clone()),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -9236,11 +9235,11 @@ async fn test_words_completion(cx: &mut TestAppContext) {
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "first".into(),
|
||||
..Default::default()
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "last".into(),
|
||||
..Default::default()
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
])))
|
||||
}
|
||||
@@ -9290,6 +9289,130 @@ async fn test_words_completion(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Enabled,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
});
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..lsp::CompletionOptions::default()
|
||||
}),
|
||||
signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let _completion_requests_handler =
|
||||
cx.lsp
|
||||
.server
|
||||
.on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "first".into(),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "last".into(),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
])))
|
||||
});
|
||||
|
||||
cx.set_state(indoc! {"ˇ
|
||||
first
|
||||
last
|
||||
second
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
cx.executor().run_until_parked();
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(
|
||||
completion_menu_entries(&menu),
|
||||
&["first", "last", "second"],
|
||||
"Word completions that has the same edit as the any of the LSP ones, should not be proposed"
|
||||
);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
editor.cancel(&Cancel, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Fallback,
|
||||
lsp: false,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
});
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
|
||||
|
||||
cx.set_state(indoc! {"ˇ
|
||||
0_usize
|
||||
let
|
||||
33
|
||||
4.5f32
|
||||
"});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_completions(&ShowCompletions::default(), window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(
|
||||
completion_menu_entries(&menu),
|
||||
&["let"],
|
||||
"With no digits in the completion query, no digits should be in the word completions"
|
||||
);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
editor.cancel(&Cancel, window, cx);
|
||||
});
|
||||
|
||||
cx.set_state(indoc! {"3ˇ
|
||||
0_usize
|
||||
let
|
||||
3
|
||||
33.35f32
|
||||
"});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_completions(&ShowCompletions::default(), window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.update_editor(|editor, _, _| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
|
||||
return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multiline_completion(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -13438,7 +13561,6 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
|
||||
EditorMode::Full,
|
||||
multi_buffer,
|
||||
Some(project.clone()),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -13921,9 +14043,8 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
|
||||
multibuffer
|
||||
});
|
||||
|
||||
let editor = cx.add_window(|window, cx| {
|
||||
Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
|
||||
});
|
||||
let editor =
|
||||
cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
|
||||
editor
|
||||
.update(cx, |editor, _window, cx| {
|
||||
for (buffer, diff_base) in [
|
||||
@@ -14042,9 +14163,8 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
|
||||
multibuffer
|
||||
});
|
||||
|
||||
let editor = cx.add_window(|window, cx| {
|
||||
Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
|
||||
});
|
||||
let editor =
|
||||
cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
|
||||
editor
|
||||
.update(cx, |editor, _window, cx| {
|
||||
let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
|
||||
@@ -15600,14 +15720,7 @@ async fn test_display_diff_hunks(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
let editor = cx.add_window(|window, cx| {
|
||||
Editor::new(
|
||||
EditorMode::Full,
|
||||
multibuffer,
|
||||
Some(project),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
Editor::new(EditorMode::Full, multibuffer, Some(project), window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -15626,9 +15739,9 @@ async fn test_display_diff_hunks(cx: &mut TestAppContext) {
|
||||
assert_eq!(
|
||||
hunks,
|
||||
[
|
||||
DisplayRow(3)..DisplayRow(5),
|
||||
DisplayRow(10)..DisplayRow(12),
|
||||
DisplayRow(17)..DisplayRow(19),
|
||||
DisplayRow(2)..DisplayRow(4),
|
||||
DisplayRow(7)..DisplayRow(9),
|
||||
DisplayRow(12)..DisplayRow(14),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -16059,7 +16172,6 @@ async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
|
||||
EditorMode::Full,
|
||||
multi_buffer,
|
||||
Some(project.clone()),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -16214,7 +16326,6 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
|
||||
EditorMode::Full,
|
||||
multi_buffer.clone(),
|
||||
Some(project.clone()),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -16222,7 +16333,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
|
||||
|
||||
assert_eq!(
|
||||
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
"\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
|
||||
"\n\naaaa\nbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
|
||||
);
|
||||
|
||||
multi_buffer_editor.update(cx, |editor, cx| {
|
||||
@@ -16230,7 +16341,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
|
||||
});
|
||||
assert_eq!(
|
||||
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
"\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
|
||||
"\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
|
||||
"After folding the first buffer, its text should not be displayed"
|
||||
);
|
||||
|
||||
@@ -16239,7 +16350,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
|
||||
});
|
||||
assert_eq!(
|
||||
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
"\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
|
||||
"\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
|
||||
"After folding the second buffer, its text should not be displayed"
|
||||
);
|
||||
|
||||
@@ -16264,7 +16375,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
|
||||
});
|
||||
assert_eq!(
|
||||
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
"\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
|
||||
"\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
|
||||
"After unfolding the second buffer, its text should be displayed"
|
||||
);
|
||||
|
||||
@@ -16286,7 +16397,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
|
||||
|
||||
assert_eq!(
|
||||
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
"\n\n\nB\n\n\n\n\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
|
||||
"\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
|
||||
"After unfolding the first buffer, its and 2nd buffer's text should be displayed"
|
||||
);
|
||||
|
||||
@@ -16295,7 +16406,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
|
||||
});
|
||||
assert_eq!(
|
||||
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
"\n\n\nB\n\n\n\n\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
|
||||
"\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
|
||||
"After unfolding the all buffers, all original text should be displayed"
|
||||
);
|
||||
}
|
||||
@@ -16381,13 +16492,12 @@ async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
|
||||
EditorMode::Full,
|
||||
multi_buffer,
|
||||
Some(project.clone()),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
|
||||
let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
|
||||
assert_eq!(
|
||||
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
full_text,
|
||||
@@ -16398,7 +16508,7 @@ async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
|
||||
});
|
||||
assert_eq!(
|
||||
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
"\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
|
||||
"\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
|
||||
"After folding the first buffer, its text should not be displayed"
|
||||
);
|
||||
|
||||
@@ -16408,7 +16518,7 @@ async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
|
||||
|
||||
assert_eq!(
|
||||
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
"\n\n\n\n\n\n\n7777\n8888\n9999\n",
|
||||
"\n\n\n\n\n\n7777\n8888\n9999",
|
||||
"After folding the second buffer, its text should not be displayed"
|
||||
);
|
||||
|
||||
@@ -16426,7 +16536,7 @@ async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
|
||||
});
|
||||
assert_eq!(
|
||||
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
"\n\n\n\n\n4444\n5555\n6666\n\n\n",
|
||||
"\n\n\n\n4444\n5555\n6666\n\n",
|
||||
"After unfolding the second buffer, its text should be displayed"
|
||||
);
|
||||
|
||||
@@ -16435,7 +16545,7 @@ async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
|
||||
});
|
||||
assert_eq!(
|
||||
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
"\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
|
||||
"\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
|
||||
"After unfolding the first buffer, its text should be displayed"
|
||||
);
|
||||
|
||||
@@ -16501,7 +16611,6 @@ async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut Test
|
||||
EditorMode::Full,
|
||||
multi_buffer,
|
||||
Some(project.clone()),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -16520,7 +16629,7 @@ async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut Test
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
|
||||
});
|
||||
|
||||
let full_text = format!("\n\n\n{sample_text}\n");
|
||||
let full_text = format!("\n\n{sample_text}");
|
||||
assert_eq!(
|
||||
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
full_text,
|
||||
@@ -16549,14 +16658,7 @@ async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContex
|
||||
],
|
||||
cx,
|
||||
);
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::Full,
|
||||
multi_buffer.clone(),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let mut editor = Editor::new(EditorMode::Full, multi_buffer.clone(), None, window, cx);
|
||||
|
||||
let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
|
||||
// fold all but the second buffer, so that we test navigating between two
|
||||
@@ -16868,7 +16970,7 @@ async fn assert_highlighted_edits(
|
||||
) {
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple(text, cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
Editor::new(EditorMode::Full, buffer, None, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
|
||||
@@ -15,15 +15,15 @@ use crate::{
|
||||
inlay_hint_settings,
|
||||
items::BufferSearchHighlights,
|
||||
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
|
||||
scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair},
|
||||
scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair, HorizontalLayoutDetails},
|
||||
BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint,
|
||||
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
|
||||
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
|
||||
GoToPreviousHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
|
||||
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
|
||||
OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight,
|
||||
Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS,
|
||||
CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
|
||||
EditorSettings, EditorSnapshot, EditorStyle, FocusedBlock, GoToHunk, GoToPreviousHunk,
|
||||
GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, InlayHintRefreshReason,
|
||||
InlineCompletion, JumpData, LineDown, LineHighlight, LineUp, OpenExcerpts, PageDown, PageUp,
|
||||
Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
|
||||
StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
|
||||
FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
|
||||
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
};
|
||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||
@@ -33,10 +33,10 @@ use file_icons::FileIcons;
|
||||
use git::{blame::BlameEntry, status::FileStatus, Oid};
|
||||
use gpui::{
|
||||
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
|
||||
relative, size, solid_background, svg, transparent_black, Action, AnyElement, App,
|
||||
AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners,
|
||||
CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _,
|
||||
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Keystroke, Length,
|
||||
relative, size, solid_background, transparent_black, Action, AnyElement, App, AvailableSpace,
|
||||
Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
||||
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
|
||||
GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Keystroke, Length,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
|
||||
StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement, Window,
|
||||
@@ -45,15 +45,15 @@ use inline_completion::Direction;
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
language_settings::{
|
||||
self, IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
|
||||
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
|
||||
ShowWhitespaceSetting,
|
||||
},
|
||||
ChunkRendererContext,
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::{
|
||||
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
|
||||
RowInfo,
|
||||
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, ExpandInfo, MultiBufferPoint,
|
||||
MultiBufferRow, RowInfo,
|
||||
};
|
||||
use project::project_settings::{self, GitGutterSetting, ProjectSettings};
|
||||
use settings::Settings;
|
||||
@@ -72,7 +72,7 @@ use sum_tree::Bias;
|
||||
use text::BufferId;
|
||||
use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
|
||||
use ui::{
|
||||
h_flex, prelude::*, ButtonLike, ButtonStyle, ContextMenu, IconButtonShape, KeyBinding, Tooltip,
|
||||
h_flex, prelude::*, ButtonLike, ContextMenu, IconButtonShape, KeyBinding, Tooltip,
|
||||
POPOVER_Y_PADDING,
|
||||
};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
@@ -390,6 +390,7 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::set_mark);
|
||||
register_action(editor, window, Editor::swap_selection_ends);
|
||||
register_action(editor, window, Editor::show_completions);
|
||||
register_action(editor, window, Editor::show_word_completions);
|
||||
register_action(editor, window, Editor::toggle_code_actions);
|
||||
register_action(editor, window, Editor::open_excerpts);
|
||||
register_action(editor, window, Editor::open_excerpts_in_split);
|
||||
@@ -1308,8 +1309,7 @@ impl EditorElement {
|
||||
);
|
||||
|
||||
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
|
||||
let editor_show_scrollbars = self.editor.read(cx).show_scrollbars;
|
||||
let show_scrollbars = editor_show_scrollbars
|
||||
let show_scrollbars = self.editor.read(cx).show_scrollbars
|
||||
&& match scrollbar_settings.show {
|
||||
ShowScrollbar::Auto => {
|
||||
let editor = self.editor.read(cx);
|
||||
@@ -1339,11 +1339,10 @@ impl EditorElement {
|
||||
ShowScrollbar::Always => true,
|
||||
ShowScrollbar::Never => false,
|
||||
};
|
||||
|
||||
let axes: AxisPair<bool> = scrollbar_settings.axes.into();
|
||||
|
||||
if snapshot.mode == EditorMode::Full
|
||||
|| matches!(snapshot.mode, EditorMode::AutoHeight { .. }) && editor_show_scrollbars
|
||||
{
|
||||
if snapshot.mode != EditorMode::Full {
|
||||
return axis_pair(None, None);
|
||||
}
|
||||
|
||||
@@ -1517,6 +1516,19 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn prepaint_expand_toggles(
|
||||
&self,
|
||||
expand_toggles: &mut [Option<(AnyElement, gpui::Point<Pixels>)>],
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
for (expand_toggle, origin) in expand_toggles.iter_mut().flatten() {
|
||||
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||
expand_toggle.layout_as_root(available_space, window, cx);
|
||||
expand_toggle.prepaint_as_root(*origin, available_space, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn prepaint_crease_trailers(
|
||||
&self,
|
||||
trailers: Vec<Option<AnyElement>>,
|
||||
@@ -2011,6 +2023,7 @@ impl EditorElement {
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
range: Range<DisplayRow>,
|
||||
row_infos: &[RowInfo],
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
@@ -2076,6 +2089,12 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
let display_row = multibuffer_point.to_display_point(snapshot).row();
|
||||
if row_infos
|
||||
.get((display_row - range.start).0 as usize)
|
||||
.is_some_and(|row_info| row_info.expand_info.is_some())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let button = editor.render_run_indicator(
|
||||
&self.style,
|
||||
Some(display_row) == active_task_indicator_row,
|
||||
@@ -2100,6 +2119,82 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
fn layout_excerpt_gutter(
|
||||
&self,
|
||||
gutter_hitbox: &Hitbox,
|
||||
line_height: Pixels,
|
||||
scroll_position: gpui::Point<f32>,
|
||||
buffer_rows: &[RowInfo],
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Vec<Option<(AnyElement, gpui::Point<Pixels>)>> {
|
||||
let editor_font_size = self.style.text.font_size.to_pixels(window.rem_size()) * 1.2;
|
||||
|
||||
let icon_size = editor_font_size.round();
|
||||
let button_h_padding = ((icon_size - px(1.0)) / 2.0).round() - px(2.0);
|
||||
|
||||
let scroll_top = scroll_position.y * line_height;
|
||||
|
||||
let elements = buffer_rows
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, row_info)| {
|
||||
let ExpandInfo {
|
||||
excerpt_id,
|
||||
direction,
|
||||
} = row_info.expand_info?;
|
||||
|
||||
let icon_name = match direction {
|
||||
ExpandExcerptDirection::Up => IconName::ExpandUp,
|
||||
ExpandExcerptDirection::Down => IconName::ExpandDown,
|
||||
ExpandExcerptDirection::UpAndDown => IconName::ExpandVertical,
|
||||
};
|
||||
|
||||
let editor = self.editor.clone();
|
||||
let max_row = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.widest_line_number();
|
||||
let is_wide = max_row > 999
|
||||
&& row_info
|
||||
.buffer_row
|
||||
.is_some_and(|row| row.ilog10() == max_row.ilog10());
|
||||
|
||||
let toggle = IconButton::new(("expand", ix), icon_name)
|
||||
.icon_color(Color::Custom(cx.theme().colors().editor_line_number))
|
||||
.selected_icon_color(Color::Custom(cx.theme().colors().editor_foreground))
|
||||
.icon_size(IconSize::Custom(rems(editor_font_size / window.rem_size())))
|
||||
.width((icon_size + button_h_padding * 2).into())
|
||||
.when(is_wide, |el| {
|
||||
el.width((icon_size + button_h_padding).into())
|
||||
})
|
||||
.on_click(move |_, _, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.expand_excerpt(excerpt_id, direction, cx);
|
||||
});
|
||||
})
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
"Expand excerpt",
|
||||
&crate::actions::ExpandExcerpts::default(),
|
||||
))
|
||||
.into_any_element();
|
||||
|
||||
let position = point(
|
||||
px(1.),
|
||||
ix as f32 * line_height - (scroll_top % line_height) + px(1.),
|
||||
);
|
||||
let origin = gutter_hitbox.origin + position;
|
||||
|
||||
Some((toggle, origin))
|
||||
})
|
||||
.collect();
|
||||
|
||||
elements
|
||||
}
|
||||
|
||||
fn layout_code_actions_indicator(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -2317,6 +2412,9 @@ impl EditorElement {
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, info)| {
|
||||
if info.expand_info.is_some() {
|
||||
return None;
|
||||
}
|
||||
let row = info.multibuffer_row?;
|
||||
let display_row = DisplayRow(rows.start.0 + ix as u32);
|
||||
let active = active_rows.contains_key(&display_row);
|
||||
@@ -2339,6 +2437,9 @@ impl EditorElement {
|
||||
buffer_rows
|
||||
.into_iter()
|
||||
.map(|row_info| {
|
||||
if row_info.expand_info.is_some() {
|
||||
return None;
|
||||
}
|
||||
if let Some(row) = row_info.multibuffer_row {
|
||||
snapshot.render_crease_trailer(row, window, cx)
|
||||
} else {
|
||||
@@ -2516,25 +2617,11 @@ impl EditorElement {
|
||||
|
||||
Block::FoldedBuffer {
|
||||
first_excerpt,
|
||||
prev_excerpt,
|
||||
show_excerpt_controls,
|
||||
height,
|
||||
..
|
||||
} => {
|
||||
let selected = selected_buffer_ids.contains(&first_excerpt.buffer_id);
|
||||
let mut result = v_flex().id(block_id).w_full();
|
||||
|
||||
if let Some(prev_excerpt) = prev_excerpt {
|
||||
if *show_excerpt_controls {
|
||||
result = result.child(self.render_expand_excerpt_control(
|
||||
block_id,
|
||||
ExpandExcerptDirection::Down,
|
||||
prev_excerpt.id,
|
||||
gutter_dimensions,
|
||||
window,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
}
|
||||
let result = v_flex().id(block_id).w_full();
|
||||
|
||||
let jump_data = header_jump_data(snapshot, block_row_start, *height, first_excerpt);
|
||||
result
|
||||
@@ -2542,6 +2629,7 @@ impl EditorElement {
|
||||
first_excerpt,
|
||||
true,
|
||||
selected,
|
||||
false,
|
||||
jump_data,
|
||||
window,
|
||||
cx,
|
||||
@@ -2550,28 +2638,14 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
Block::ExcerptBoundary {
|
||||
prev_excerpt,
|
||||
next_excerpt,
|
||||
show_excerpt_controls,
|
||||
height,
|
||||
starts_new_buffer,
|
||||
..
|
||||
} => {
|
||||
let color = cx.theme().colors().clone();
|
||||
let mut result = v_flex().id(block_id).w_full();
|
||||
|
||||
if let Some(prev_excerpt) = prev_excerpt {
|
||||
if *show_excerpt_controls {
|
||||
result = result.child(self.render_expand_excerpt_control(
|
||||
block_id,
|
||||
ExpandExcerptDirection::Down,
|
||||
prev_excerpt.id,
|
||||
gutter_dimensions,
|
||||
window,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(next_excerpt) = next_excerpt {
|
||||
let jump_data =
|
||||
header_jump_data(snapshot, block_row_start, *height, next_excerpt);
|
||||
@@ -2584,6 +2658,7 @@ impl EditorElement {
|
||||
next_excerpt,
|
||||
false,
|
||||
selected,
|
||||
false,
|
||||
jump_data,
|
||||
window,
|
||||
cx,
|
||||
@@ -2592,40 +2667,17 @@ impl EditorElement {
|
||||
result = result
|
||||
.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
|
||||
}
|
||||
|
||||
if *show_excerpt_controls {
|
||||
result = result.child(self.render_expand_excerpt_control(
|
||||
block_id,
|
||||
ExpandExcerptDirection::Up,
|
||||
next_excerpt.id,
|
||||
gutter_dimensions,
|
||||
window,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
if *show_excerpt_controls {
|
||||
result = result.child(
|
||||
h_flex()
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.top(px(0.))
|
||||
.absolute()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(color.border_variant),
|
||||
)
|
||||
.child(self.render_expand_excerpt_control(
|
||||
block_id,
|
||||
ExpandExcerptDirection::Up,
|
||||
next_excerpt.id,
|
||||
gutter_dimensions,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
);
|
||||
}
|
||||
result = result.child(
|
||||
h_flex().relative().child(
|
||||
div()
|
||||
.top(line_height / 2.)
|
||||
.absolute()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(color.border_variant),
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2664,6 +2716,7 @@ impl EditorElement {
|
||||
for_excerpt: &ExcerptInfo,
|
||||
is_folded: bool,
|
||||
is_selected: bool,
|
||||
_is_sticky: bool,
|
||||
jump_data: JumpData,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -2710,7 +2763,7 @@ impl EditorElement {
|
||||
.pl_0p5()
|
||||
.pr_5()
|
||||
.rounded_sm()
|
||||
.shadow_md()
|
||||
// .when(is_sticky, |el| el.shadow_md())
|
||||
.border_1()
|
||||
.map(|div| {
|
||||
let border_color = if is_selected
|
||||
@@ -2849,95 +2902,6 @@ impl EditorElement {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_expand_excerpt_control(
|
||||
&self,
|
||||
block_id: BlockId,
|
||||
direction: ExpandExcerptDirection,
|
||||
excerpt_id: ExcerptId,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
window: &Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
let color = cx.theme().colors().clone();
|
||||
let hover_color = color.border_variant.opacity(0.5);
|
||||
let focus_handle = self.editor.focus_handle(cx).clone();
|
||||
|
||||
let icon_offset =
|
||||
gutter_dimensions.width - (gutter_dimensions.left_padding + gutter_dimensions.margin);
|
||||
let header_height = MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * window.line_height();
|
||||
let group_name = if direction == ExpandExcerptDirection::Down {
|
||||
"expand-down"
|
||||
} else {
|
||||
"expand-up"
|
||||
};
|
||||
|
||||
let expand_area = |id: SharedString| {
|
||||
h_flex()
|
||||
.id(id)
|
||||
.w_full()
|
||||
.cursor_pointer()
|
||||
.block_mouse_down()
|
||||
.on_mouse_move(|_, _, cx| cx.stop_propagation())
|
||||
.hover(|style| style.bg(hover_color))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Expand Excerpt",
|
||||
&ExpandExcerpts { lines: 0 },
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
expand_area(
|
||||
format!(
|
||||
"block-{}-{}",
|
||||
block_id,
|
||||
if direction == ExpandExcerptDirection::Down {
|
||||
"down"
|
||||
} else {
|
||||
"up"
|
||||
}
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
.group(group_name)
|
||||
.child(
|
||||
h_flex()
|
||||
.w(icon_offset)
|
||||
.h(header_height)
|
||||
.flex_none()
|
||||
.justify_end()
|
||||
.child(
|
||||
ButtonLike::new("expand-icon")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.child(
|
||||
svg()
|
||||
.path(if direction == ExpandExcerptDirection::Down {
|
||||
IconName::ArrowDownFromLine.path()
|
||||
} else {
|
||||
IconName::ArrowUpFromLine.path()
|
||||
})
|
||||
.size(IconSize::XSmall.rems())
|
||||
.text_color(cx.theme().colors().editor_line_number)
|
||||
.group_hover(group_name, |style| {
|
||||
style.text_color(cx.theme().colors().editor_active_line_number)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.on_click(window.listener_for(&self.editor, {
|
||||
move |editor, _, _, cx| {
|
||||
editor.expand_excerpt(excerpt_id, direction, cx);
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_blocks(
|
||||
&self,
|
||||
rows: Range<DisplayRow>,
|
||||
@@ -3169,7 +3133,6 @@ impl EditorElement {
|
||||
&self,
|
||||
StickyHeaderExcerpt {
|
||||
excerpt,
|
||||
next_excerpt_controls_present,
|
||||
next_buffer_row,
|
||||
}: StickyHeaderExcerpt<'_>,
|
||||
scroll_position: f32,
|
||||
@@ -3206,7 +3169,7 @@ impl EditorElement {
|
||||
.top_0(),
|
||||
)
|
||||
.child(
|
||||
self.render_buffer_header(excerpt, false, selected, jump_data, window, cx)
|
||||
self.render_buffer_header(excerpt, false, selected, true, jump_data, window, cx)
|
||||
.into_any_element(),
|
||||
)
|
||||
.into_any_element();
|
||||
@@ -3216,11 +3179,7 @@ impl EditorElement {
|
||||
if let Some(next_buffer_row) = next_buffer_row {
|
||||
// Push up the sticky header when the excerpt is getting close to the top of the viewport
|
||||
|
||||
let mut max_row = next_buffer_row - FILE_HEADER_HEIGHT * 2;
|
||||
|
||||
if next_excerpt_controls_present {
|
||||
max_row -= MULTI_BUFFER_EXCERPT_HEADER_HEIGHT;
|
||||
}
|
||||
let max_row = next_buffer_row - FILE_HEADER_HEIGHT * 2;
|
||||
|
||||
let offset = scroll_position - max_row as f32;
|
||||
|
||||
@@ -4516,6 +4475,12 @@ impl EditorElement {
|
||||
}
|
||||
});
|
||||
|
||||
window.with_element_namespace("expand_toggles", |window| {
|
||||
for (expand_toggle, _) in layout.expand_toggles.iter_mut().flatten() {
|
||||
expand_toggle.paint(window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
for test_indicator in layout.test_indicators.iter_mut() {
|
||||
test_indicator.paint(window, cx);
|
||||
}
|
||||
@@ -6893,6 +6858,18 @@ impl Element for EditorElement {
|
||||
cx,
|
||||
);
|
||||
|
||||
let mut expand_toggles =
|
||||
window.with_element_namespace("expand_toggles", |window| {
|
||||
self.layout_excerpt_gutter(
|
||||
&gutter_hitbox,
|
||||
line_height,
|
||||
scroll_position,
|
||||
&row_infos,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let mut crease_toggles =
|
||||
window.with_element_namespace("crease_toggles", |window| {
|
||||
self.layout_crease_toggles(
|
||||
@@ -6990,7 +6967,7 @@ impl Element for EditorElement {
|
||||
let mut scroll_width = scroll_range_bounds.size.width;
|
||||
|
||||
let sticky_header_excerpt = if snapshot.buffer_snapshot.show_headers() {
|
||||
snapshot.sticky_header_excerpt(start_row)
|
||||
snapshot.sticky_header_excerpt(scroll_position.y)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -7054,6 +7031,11 @@ impl Element for EditorElement {
|
||||
);
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.scroll_manager.latest_horizontal_details = HorizontalLayoutDetails {
|
||||
letter_width: letter_size.width.0,
|
||||
editor_width: editor_width.0,
|
||||
scroll_max: scroll_max.x,
|
||||
};
|
||||
let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
|
||||
|
||||
let autoscrolled = if autoscroll_horizontally {
|
||||
@@ -7307,7 +7289,14 @@ impl Element for EditorElement {
|
||||
.tasks
|
||||
.contains_key(&(buffer_id, row));
|
||||
|
||||
if !has_test_indicator {
|
||||
let has_expand_indicator = row_infos
|
||||
.get(
|
||||
(newest_selection_head.row() - start_row).0
|
||||
as usize,
|
||||
)
|
||||
.is_some_and(|row_info| row_info.expand_info.is_some());
|
||||
|
||||
if !has_test_indicator && !has_expand_indicator {
|
||||
code_actions_indicator = self
|
||||
.layout_code_actions_indicator(
|
||||
line_height,
|
||||
@@ -7340,6 +7329,7 @@ impl Element for EditorElement {
|
||||
self.layout_run_indicators(
|
||||
line_height,
|
||||
start_row..end_row,
|
||||
&row_infos,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
@@ -7402,6 +7392,10 @@ impl Element for EditorElement {
|
||||
)
|
||||
});
|
||||
|
||||
window.with_element_namespace("expand_toggles", |window| {
|
||||
self.prepaint_expand_toggles(&mut expand_toggles, window, cx)
|
||||
});
|
||||
|
||||
let invisible_symbol_font_size = font_size / 2.;
|
||||
let tab_invisible = window
|
||||
.text_system()
|
||||
@@ -7503,6 +7497,7 @@ impl Element for EditorElement {
|
||||
tab_invisible,
|
||||
space_invisible,
|
||||
sticky_buffer_header,
|
||||
expand_toggles,
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -7676,6 +7671,7 @@ pub struct EditorLayout {
|
||||
code_actions_indicator: Option<AnyElement>,
|
||||
test_indicators: Vec<AnyElement>,
|
||||
crease_toggles: Vec<Option<AnyElement>>,
|
||||
expand_toggles: Vec<Option<(AnyElement, gpui::Point<Pixels>)>>,
|
||||
diff_hunk_controls: Vec<AnyElement>,
|
||||
crease_trailers: Vec<Option<CreaseTrailerLayout>>,
|
||||
inline_completion_popover: Option<AnyElement>,
|
||||
@@ -7703,7 +7699,7 @@ struct ColoredRange<T> {
|
||||
color: Hsla,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
struct ScrollbarLayout {
|
||||
hitbox: Hitbox,
|
||||
visible_range: Range<f32>,
|
||||
@@ -8278,15 +8274,8 @@ fn compute_auto_height_layout(
|
||||
let overscroll = size(em_width, px(0.));
|
||||
|
||||
let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width;
|
||||
|
||||
let soft_wrap_disabled = editor
|
||||
.soft_wrap_mode_override
|
||||
.is_some_and(|soft_wrap| matches!(soft_wrap, language_settings::SoftWrap::None));
|
||||
|
||||
if !soft_wrap_disabled {
|
||||
if editor.set_wrap_width(Some(editor_width), cx) {
|
||||
snapshot = editor.snapshot(window, cx);
|
||||
}
|
||||
if editor.set_wrap_width(Some(editor_width), cx) {
|
||||
snapshot = editor.snapshot(window, cx);
|
||||
}
|
||||
|
||||
let scroll_height = Pixels::from(snapshot.max_point().row().next_row().0) * line_height;
|
||||
@@ -8316,7 +8305,7 @@ mod tests {
|
||||
init_test(cx, |_| {});
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
Editor::new(EditorMode::Full, buffer, None, window, cx)
|
||||
});
|
||||
|
||||
let editor = window.root(cx).unwrap();
|
||||
@@ -8416,7 +8405,7 @@ mod tests {
|
||||
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
Editor::new(EditorMode::Full, buffer, None, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
let editor = window.root(cx).unwrap();
|
||||
@@ -8479,90 +8468,6 @@ mod tests {
|
||||
state.active_rows.keys().cloned().collect::<Vec<_>>(),
|
||||
vec![DisplayRow(0), DisplayRow(3), DisplayRow(5), DisplayRow(6)]
|
||||
);
|
||||
|
||||
// multi-buffer support
|
||||
// in DisplayPoint coordinates, this is what we're dealing with:
|
||||
// 0: [[file
|
||||
// 1: header
|
||||
// 2: section]]
|
||||
// 3: aaaaaa
|
||||
// 4: bbbbbb
|
||||
// 5: cccccc
|
||||
// 6:
|
||||
// 7: [[footer]]
|
||||
// 8: [[header]]
|
||||
// 9: ffffff
|
||||
// 10: gggggg
|
||||
// 11: hhhhhh
|
||||
// 12:
|
||||
// 13: [[footer]]
|
||||
// 14: [[file
|
||||
// 15: header
|
||||
// 16: section]]
|
||||
// 17: bbbbbb
|
||||
// 18: cccccc
|
||||
// 19: dddddd
|
||||
// 20: [[footer]]
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_multi(
|
||||
[
|
||||
(
|
||||
&(sample_text(8, 6, 'a') + "\n"),
|
||||
vec![
|
||||
Point::new(0, 0)..Point::new(3, 0),
|
||||
Point::new(4, 0)..Point::new(7, 0),
|
||||
],
|
||||
),
|
||||
(
|
||||
&(sample_text(8, 6, 'a') + "\n"),
|
||||
vec![Point::new(1, 0)..Point::new(3, 0)],
|
||||
),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
});
|
||||
let editor = window.root(cx).unwrap();
|
||||
let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
|
||||
let _state = window.update(cx, |editor, window, cx| {
|
||||
editor.cursor_shape = CursorShape::Block;
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(7), 0),
|
||||
DisplayPoint::new(DisplayRow(10), 0)..DisplayPoint::new(DisplayRow(13), 0),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
let (_, state) = cx.draw(
|
||||
point(px(500.), px(500.)),
|
||||
size(px(500.), px(500.)),
|
||||
|_, _| EditorElement::new(&editor, style),
|
||||
);
|
||||
assert_eq!(state.selections.len(), 1);
|
||||
let local_selections = &state.selections[0].1;
|
||||
assert_eq!(local_selections.len(), 2);
|
||||
|
||||
// moves cursor on excerpt boundary back a line
|
||||
// and doesn't allow selection to bleed through
|
||||
assert_eq!(
|
||||
local_selections[0].range,
|
||||
DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(7), 0)
|
||||
);
|
||||
assert_eq!(
|
||||
local_selections[0].head,
|
||||
DisplayPoint::new(DisplayRow(6), 0)
|
||||
);
|
||||
// moves cursor on buffer boundary back two lines
|
||||
// and doesn't allow selection to bleed through
|
||||
assert_eq!(
|
||||
local_selections[1].range,
|
||||
DisplayPoint::new(DisplayRow(10), 0)..DisplayPoint::new(DisplayRow(13), 0)
|
||||
);
|
||||
assert_eq!(
|
||||
local_selections[1].head,
|
||||
DisplayPoint::new(DisplayRow(12), 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -8571,7 +8476,7 @@ mod tests {
|
||||
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple("", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
Editor::new(EditorMode::Full, buffer, None, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
let editor = window.root(cx).unwrap();
|
||||
@@ -8797,7 +8702,7 @@ mod tests {
|
||||
);
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple(input_text, cx);
|
||||
Editor::new(editor_mode, buffer, None, true, window, cx)
|
||||
Editor::new(editor_mode, buffer, None, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
let editor = window.root(cx).unwrap();
|
||||
|
||||
@@ -488,7 +488,7 @@ async fn parse_commit_messages(
|
||||
},
|
||||
))
|
||||
} else {
|
||||
continue;
|
||||
None
|
||||
};
|
||||
|
||||
let remote = parsed_remote_url
|
||||
|
||||
@@ -2618,7 +2618,7 @@ pub mod tests {
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
let editor = cx.add_window(|window, cx| {
|
||||
Editor::for_multibuffer(multibuffer, Some(project.clone()), true, window, cx)
|
||||
Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
|
||||
});
|
||||
|
||||
let editor_edited = Arc::new(AtomicBool::new(false));
|
||||
@@ -2830,7 +2830,6 @@ pub mod tests {
|
||||
"main hint #5".to_string(),
|
||||
"other hint(edited) #0".to_string(),
|
||||
"other hint(edited) #1".to_string(),
|
||||
"other hint(edited) #2".to_string(),
|
||||
];
|
||||
assert_eq!(
|
||||
expected_hints,
|
||||
@@ -2921,7 +2920,7 @@ pub mod tests {
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
let editor = cx.add_window(|window, cx| {
|
||||
Editor::for_multibuffer(multibuffer, Some(project.clone()), true, window, cx)
|
||||
Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
|
||||
});
|
||||
let editor_edited = Arc::new(AtomicBool::new(false));
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
@@ -129,13 +129,8 @@ impl FollowableItem for Editor {
|
||||
});
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut editor = Editor::for_multibuffer(
|
||||
multibuffer,
|
||||
Some(project.clone()),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx);
|
||||
editor.remote_id = Some(remote_id);
|
||||
editor
|
||||
})
|
||||
|
||||
@@ -893,8 +893,6 @@ mod tests {
|
||||
font,
|
||||
font_size,
|
||||
None,
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
FoldPlaceholder::test(),
|
||||
@@ -1110,138 +1108,136 @@ mod tests {
|
||||
font,
|
||||
px(14.0),
|
||||
None,
|
||||
true,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
FoldPlaceholder::test(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
|
||||
assert_eq!(snapshot.text(), "abc\ndefg\nhijkl\nmn");
|
||||
|
||||
let col_2_x = snapshot
|
||||
.x_for_display_point(DisplayPoint::new(DisplayRow(2), 2), &text_layout_details);
|
||||
.x_for_display_point(DisplayPoint::new(DisplayRow(0), 2), &text_layout_details);
|
||||
|
||||
// Can't move up into the first excerpt's header
|
||||
assert_eq!(
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(DisplayRow(2), 2),
|
||||
DisplayPoint::new(DisplayRow(0), 2),
|
||||
SelectionGoal::HorizontalPosition(col_2_x.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(2), 0),
|
||||
DisplayPoint::new(DisplayRow(0), 0),
|
||||
SelectionGoal::HorizontalPosition(col_2_x.0),
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(DisplayRow(2), 0),
|
||||
DisplayPoint::new(DisplayRow(0), 0),
|
||||
SelectionGoal::None,
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(2), 0),
|
||||
DisplayPoint::new(DisplayRow(0), 0),
|
||||
SelectionGoal::HorizontalPosition(0.0),
|
||||
),
|
||||
);
|
||||
|
||||
let col_4_x = snapshot
|
||||
.x_for_display_point(DisplayPoint::new(DisplayRow(3), 4), &text_layout_details);
|
||||
.x_for_display_point(DisplayPoint::new(DisplayRow(1), 4), &text_layout_details);
|
||||
|
||||
// Move up and down within first excerpt
|
||||
assert_eq!(
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(DisplayRow(3), 4),
|
||||
DisplayPoint::new(DisplayRow(1), 4),
|
||||
SelectionGoal::HorizontalPosition(col_4_x.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(2), 3),
|
||||
DisplayPoint::new(DisplayRow(0), 3),
|
||||
SelectionGoal::HorizontalPosition(col_4_x.0)
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(DisplayRow(2), 3),
|
||||
DisplayPoint::new(DisplayRow(0), 3),
|
||||
SelectionGoal::HorizontalPosition(col_4_x.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(3), 4),
|
||||
DisplayPoint::new(DisplayRow(1), 4),
|
||||
SelectionGoal::HorizontalPosition(col_4_x.0)
|
||||
),
|
||||
);
|
||||
|
||||
let col_5_x = snapshot
|
||||
.x_for_display_point(DisplayPoint::new(DisplayRow(6), 5), &text_layout_details);
|
||||
.x_for_display_point(DisplayPoint::new(DisplayRow(2), 5), &text_layout_details);
|
||||
|
||||
// Move up and down across second excerpt's header
|
||||
assert_eq!(
|
||||
up(
|
||||
&snapshot,
|
||||
DisplayPoint::new(DisplayRow(6), 5),
|
||||
DisplayPoint::new(DisplayRow(2), 5),
|
||||
SelectionGoal::HorizontalPosition(col_5_x.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(3), 4),
|
||||
DisplayPoint::new(DisplayRow(1), 4),
|
||||
SelectionGoal::HorizontalPosition(col_5_x.0)
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(DisplayRow(3), 4),
|
||||
DisplayPoint::new(DisplayRow(1), 4),
|
||||
SelectionGoal::HorizontalPosition(col_5_x.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(6), 5),
|
||||
DisplayPoint::new(DisplayRow(2), 5),
|
||||
SelectionGoal::HorizontalPosition(col_5_x.0)
|
||||
),
|
||||
);
|
||||
|
||||
let max_point_x = snapshot
|
||||
.x_for_display_point(DisplayPoint::new(DisplayRow(7), 2), &text_layout_details);
|
||||
.x_for_display_point(DisplayPoint::new(DisplayRow(3), 2), &text_layout_details);
|
||||
|
||||
// Can't move down off the end, and attempting to do so leaves the selection goal unchanged
|
||||
assert_eq!(
|
||||
down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(DisplayRow(7), 0),
|
||||
DisplayPoint::new(DisplayRow(3), 0),
|
||||
SelectionGoal::HorizontalPosition(0.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(7), 2),
|
||||
DisplayPoint::new(DisplayRow(3), 2),
|
||||
SelectionGoal::HorizontalPosition(0.0)
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(DisplayRow(7), 2),
|
||||
DisplayPoint::new(DisplayRow(3), 2),
|
||||
SelectionGoal::HorizontalPosition(max_point_x.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(7), 2),
|
||||
DisplayPoint::new(DisplayRow(3), 2),
|
||||
SelectionGoal::HorizontalPosition(max_point_x.0)
|
||||
),
|
||||
);
|
||||
|
||||
@@ -62,8 +62,7 @@ impl ProposedChangesEditor {
|
||||
let (recalculate_diffs_tx, mut recalculate_diffs_rx) = mpsc::unbounded();
|
||||
let mut this = Self {
|
||||
editor: cx.new(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(multibuffer.clone(), project, true, window, cx);
|
||||
let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, window, cx);
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
editor.set_completion_provider(None);
|
||||
editor.clear_code_action_providers();
|
||||
|
||||
@@ -86,7 +86,7 @@ pub fn expand_macro_recursively(
|
||||
cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name));
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(cx.new(|cx| {
|
||||
let mut editor = Editor::for_multibuffer(multibuffer, None, false, window, cx);
|
||||
let mut editor = Editor::for_multibuffer(multibuffer, None, window, cx);
|
||||
editor.set_read_only(true);
|
||||
editor
|
||||
})),
|
||||
|
||||
@@ -172,6 +172,13 @@ impl OngoingScroll {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct HorizontalLayoutDetails {
|
||||
pub letter_width: f32,
|
||||
pub editor_width: f32,
|
||||
pub scroll_max: f32,
|
||||
}
|
||||
|
||||
pub struct ScrollManager {
|
||||
pub(crate) vertical_scroll_margin: f32,
|
||||
anchor: ScrollAnchor,
|
||||
@@ -183,6 +190,7 @@ pub struct ScrollManager {
|
||||
dragging_scrollbar: AxisPair<bool>,
|
||||
visible_line_count: Option<f32>,
|
||||
forbid_vertical_scroll: bool,
|
||||
pub(crate) latest_horizontal_details: HorizontalLayoutDetails,
|
||||
}
|
||||
|
||||
impl ScrollManager {
|
||||
@@ -198,6 +206,7 @@ impl ScrollManager {
|
||||
last_autoscroll: None,
|
||||
visible_line_count: None,
|
||||
forbid_vertical_scroll: false,
|
||||
latest_horizontal_details: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,6 +388,15 @@ impl ScrollManager {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn horizontal_scroll(
|
||||
&mut self,
|
||||
f: impl Fn(f32, &HorizontalLayoutDetails) -> f32,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
self.anchor.offset.x = f(self.anchor.offset.x, &self.latest_horizontal_details);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
|
||||
if max < self.anchor.offset.x {
|
||||
self.anchor.offset.x = max;
|
||||
|
||||
@@ -61,8 +61,6 @@ pub fn marked_display_snapshot(
|
||||
font,
|
||||
font_size,
|
||||
None,
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
FoldPlaceholder::test(),
|
||||
@@ -108,7 +106,7 @@ pub(crate) fn build_editor(
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Editor {
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
Editor::new(EditorMode::Full, buffer, None, window, cx)
|
||||
}
|
||||
|
||||
pub(crate) fn build_editor_with_project(
|
||||
@@ -117,5 +115,5 @@ pub(crate) fn build_editor_with_project(
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Editor {
|
||||
Editor::new(EditorMode::Full, buffer, Some(project), true, window, cx)
|
||||
Editor::new(EditorMode::Full, buffer, Some(project), window, cx)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, ReadGlobal as _};
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let extension_events = cx.new(ExtensionEvents::new);
|
||||
@@ -14,8 +14,10 @@ pub struct ExtensionEvents;
|
||||
|
||||
impl ExtensionEvents {
|
||||
/// Returns the global [`ExtensionEvents`].
|
||||
pub fn global(cx: &App) -> Entity<Self> {
|
||||
GlobalExtensionEvents::global(cx).0.clone()
|
||||
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
|
||||
return cx
|
||||
.try_global::<GlobalExtensionEvents>()
|
||||
.map(|g| g.0.clone());
|
||||
}
|
||||
|
||||
fn new(_cx: &mut Context<Self>) -> Self {
|
||||
@@ -29,7 +31,7 @@ impl ExtensionEvents {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Event {
|
||||
ExtensionsUpdated,
|
||||
ExtensionsInstalledChanged,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ExtensionEvents {}
|
||||
|
||||
@@ -127,6 +127,7 @@ pub enum ExtensionOperation {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Event {
|
||||
ExtensionsUpdated,
|
||||
StartedReloading,
|
||||
ExtensionInstalled(Arc<str>),
|
||||
ExtensionFailedToLoad(Arc<str>),
|
||||
@@ -1213,9 +1214,7 @@ impl ExtensionStore {
|
||||
|
||||
self.extension_index = new_index;
|
||||
cx.notify();
|
||||
ExtensionEvents::global(cx).update(cx, |this, cx| {
|
||||
this.emit(extension::Event::ExtensionsUpdated, cx)
|
||||
});
|
||||
cx.emit(Event::ExtensionsUpdated);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.background_spawn({
|
||||
@@ -1317,6 +1316,12 @@ impl ExtensionStore {
|
||||
this.proxy.set_extensions_loaded();
|
||||
this.proxy.reload_current_theme(cx);
|
||||
this.proxy.reload_current_icon_theme(cx);
|
||||
|
||||
if let Some(events) = ExtensionEvents::try_global(cx) {
|
||||
events.update(cx, |this, cx| {
|
||||
this.emit(extension::Event::ExtensionsInstalledChanged, cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
|
||||
@@ -714,6 +714,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|c| c.label.text)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -17,7 +17,6 @@ client.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
extension.workspace = true
|
||||
extension_host.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
|
||||
@@ -9,7 +9,6 @@ use std::{ops::Range, sync::Arc};
|
||||
use client::{ExtensionMetadata, ExtensionProvides};
|
||||
use collections::{BTreeMap, BTreeSet};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use extension::ExtensionEvents;
|
||||
use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
@@ -213,7 +212,7 @@ pub struct ExtensionsPage {
|
||||
query_editor: Entity<Editor>,
|
||||
query_contains_error: bool,
|
||||
provides_filter: Option<ExtensionProvides>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
_subscriptions: [gpui::Subscription; 2],
|
||||
extension_fetch_task: Option<Task<()>>,
|
||||
upsells: BTreeSet<Feature>,
|
||||
}
|
||||
@@ -227,12 +226,15 @@ impl ExtensionsPage {
|
||||
cx.new(|cx| {
|
||||
let store = ExtensionStore::global(cx);
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let subscriptions = vec![
|
||||
let subscriptions = [
|
||||
cx.observe(&store, |_: &mut Self, _, cx| cx.notify()),
|
||||
cx.subscribe_in(
|
||||
&store,
|
||||
window,
|
||||
move |this, _, event, window, cx| match event {
|
||||
extension_host::Event::ExtensionsUpdated => {
|
||||
this.fetch_extensions_debounced(cx)
|
||||
}
|
||||
extension_host::Event::ExtensionInstalled(extension_id) => this
|
||||
.on_extension_installed(
|
||||
workspace_handle.clone(),
|
||||
@@ -243,15 +245,6 @@ impl ExtensionsPage {
|
||||
_ => {}
|
||||
},
|
||||
),
|
||||
cx.subscribe_in(
|
||||
&ExtensionEvents::global(cx),
|
||||
window,
|
||||
move |this, _, event, _window, cx| match event {
|
||||
extension::Event::ExtensionsUpdated => {
|
||||
this.fetch_extensions_debounced(cx);
|
||||
}
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
let query_editor = cx.new(|cx| {
|
||||
|
||||
@@ -795,9 +795,8 @@ impl GitRepository for RealGitRepository {
|
||||
cx: AsyncApp,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
cx.background_spawn(async move {
|
||||
let mut cmd = new_smol_command(&git_binary_path);
|
||||
let mut cmd = new_smol_command("git");
|
||||
cmd.current_dir(&working_directory?)
|
||||
.envs(env)
|
||||
.args(["commit", "--quiet", "-m"])
|
||||
|
||||
@@ -35,7 +35,6 @@ use gpui::{
|
||||
Transformation, UniformListScrollHandle, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::language_settings::SoftWrap;
|
||||
use language::{Buffer, File};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
@@ -359,7 +358,6 @@ pub(crate) fn commit_message_editor(
|
||||
EditorMode::AutoHeight { max_lines },
|
||||
buffer,
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -369,8 +367,6 @@ pub(crate) fn commit_message_editor(
|
||||
commit_editor.set_show_wrap_guides(false, cx);
|
||||
commit_editor.set_show_indent_guides(false, cx);
|
||||
commit_editor.set_hard_wrap(Some(72), cx);
|
||||
commit_editor.set_soft_wrap_mode(SoftWrap::None, cx);
|
||||
commit_editor.set_show_scrollbars(true, cx);
|
||||
let placeholder = placeholder.unwrap_or("Enter commit message");
|
||||
commit_editor.set_placeholder_text(placeholder, cx);
|
||||
commit_editor
|
||||
@@ -3051,21 +3047,24 @@ impl GitPanel {
|
||||
"No Git repositories"
|
||||
},
|
||||
))
|
||||
.children(self.active_repository.is_none().then(|| {
|
||||
h_flex().w_full().justify_around().child(
|
||||
panel_filled_button("Initialize Repository")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"git init",
|
||||
&git::Init,
|
||||
&self.focus_handle,
|
||||
))
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(&git::Init);
|
||||
})
|
||||
}),
|
||||
)
|
||||
}))
|
||||
.children({
|
||||
let worktree_count = self.project.read(cx).visible_worktrees(cx).count();
|
||||
(worktree_count > 0 && self.active_repository.is_none()).then(|| {
|
||||
h_flex().w_full().justify_around().child(
|
||||
panel_filled_button("Initialize Repository")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"git init",
|
||||
&git::Init,
|
||||
&self.focus_handle,
|
||||
))
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(&git::Init);
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
.text_ui_sm(cx)
|
||||
.mx_auto()
|
||||
.text_color(Color::Placeholder.color(cx)),
|
||||
|
||||
@@ -141,13 +141,8 @@ impl ProjectDiff {
|
||||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut diff_display_editor = Editor::for_multibuffer(
|
||||
multibuffer.clone(),
|
||||
Some(project.clone()),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let mut diff_display_editor =
|
||||
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
|
||||
diff_display_editor.disable_inline_diagnostics();
|
||||
diff_display_editor.set_expand_all_diff_hunks(cx);
|
||||
diff_display_editor.register_addon(GitPanelAddon {
|
||||
@@ -278,7 +273,7 @@ impl ProjectDiff {
|
||||
has_staged_hunks = true;
|
||||
has_unstaged_hunks = true;
|
||||
}
|
||||
DiffHunkSecondaryStatus::None
|
||||
DiffHunkSecondaryStatus::NoSecondaryHunk
|
||||
| DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
|
||||
has_staged_hunks = true;
|
||||
}
|
||||
@@ -308,7 +303,7 @@ impl ProjectDiff {
|
||||
|
||||
fn handle_editor_event(
|
||||
&mut self,
|
||||
_: &Entity<Editor>,
|
||||
editor: &Entity<Editor>,
|
||||
event: &EditorEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -330,6 +325,11 @@ impl ProjectDiff {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if editor.focus_handle(cx).contains_focused(window, cx) {
|
||||
if self.multibuffer.read(cx).is_empty() {
|
||||
self.focus_handle.focus(window)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_buffers(&mut self, cx: &mut Context<Self>) -> Vec<Task<Result<DiffBuffer>>> {
|
||||
|
||||
@@ -98,8 +98,8 @@ profiling.workspace = true
|
||||
rand = { optional = true, workspace = true }
|
||||
raw-window-handle = "0.6"
|
||||
refineable.workspace = true
|
||||
resvg = { version = "0.44.0", default-features = false }
|
||||
usvg = { version = "0.44.0", default-features = false }
|
||||
resvg = { version = "0.45.0", default-features = false, features = ["text", "system-fonts", "memmap-fonts"] }
|
||||
usvg = { version = "0.45.0", default-features = false }
|
||||
schemars.workspace = true
|
||||
seahash = "4.1"
|
||||
semantic_version.workspace = true
|
||||
|
||||
@@ -1046,7 +1046,7 @@ impl App {
|
||||
&self.foreground_executor
|
||||
}
|
||||
|
||||
/// Spawns the future returned by the given function on the thread pool. The closure will be invoked
|
||||
/// Spawns the future returned by the given function on the main thread. The closure will be invoked
|
||||
/// with [AsyncApp], which allows the application state to be accessed across await points.
|
||||
#[track_caller]
|
||||
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task<R>
|
||||
|
||||
@@ -512,6 +512,8 @@ impl MacWindow {
|
||||
unsafe {
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
|
||||
let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO];
|
||||
|
||||
let mut style_mask;
|
||||
if let Some(titlebar) = titlebar.as_ref() {
|
||||
style_mask = NSWindowStyleMask::NSClosableWindowMask
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use crate::{AssetSource, DevicePixels, IsZero, Result, SharedString, Size};
|
||||
use anyhow::anyhow;
|
||||
use resvg::tiny_skia::Pixmap;
|
||||
use std::{hash::Hash, sync::Arc};
|
||||
use std::{
|
||||
hash::Hash,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
|
||||
/// When rendering SVGs, we render them at twice the size to get a higher-quality result.
|
||||
pub const SMOOTH_SVG_SCALE_FACTOR: f32 = 2.;
|
||||
@@ -15,6 +18,7 @@ pub(crate) struct RenderSvgParams {
|
||||
#[derive(Clone)]
|
||||
pub struct SvgRenderer {
|
||||
asset_source: Arc<dyn AssetSource>,
|
||||
usvg_options: Arc<usvg::Options<'static>>,
|
||||
}
|
||||
|
||||
pub enum SvgSize {
|
||||
@@ -24,7 +28,31 @@ pub enum SvgSize {
|
||||
|
||||
impl SvgRenderer {
|
||||
pub fn new(asset_source: Arc<dyn AssetSource>) -> Self {
|
||||
Self { asset_source }
|
||||
let font_db = LazyLock::new(|| {
|
||||
let mut db = usvg::fontdb::Database::new();
|
||||
db.load_system_fonts();
|
||||
Arc::new(db)
|
||||
});
|
||||
let default_font_resolver = usvg::FontResolver::default_font_selector();
|
||||
let font_resolver = Box::new(
|
||||
move |font: &usvg::Font, db: &mut Arc<usvg::fontdb::Database>| {
|
||||
if db.is_empty() {
|
||||
*db = font_db.clone();
|
||||
}
|
||||
default_font_resolver(font, db)
|
||||
},
|
||||
);
|
||||
let options = usvg::Options {
|
||||
font_resolver: usvg::FontResolver {
|
||||
select_font: font_resolver,
|
||||
select_fallback: usvg::FontResolver::default_fallback_selector(),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
Self {
|
||||
asset_source,
|
||||
usvg_options: Arc::new(options),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn render(&self, params: &RenderSvgParams) -> Result<Option<Vec<u8>>> {
|
||||
@@ -49,7 +77,7 @@ impl SvgRenderer {
|
||||
}
|
||||
|
||||
pub fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {
|
||||
let tree = usvg::Tree::from_data(bytes, &usvg::Options::default())?;
|
||||
let tree = usvg::Tree::from_data(bytes, &self.usvg_options)?;
|
||||
|
||||
let size = match size {
|
||||
SvgSize::Size(size) => size,
|
||||
|
||||
@@ -18,7 +18,7 @@ use gpui::{
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{self, all_language_settings, AllLanguageSettings, EditPredictionProvider},
|
||||
File, Language,
|
||||
EditPredictionsMode, File, Language,
|
||||
};
|
||||
use regex::Regex;
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
@@ -456,13 +456,47 @@ impl InlineCompletionButton {
|
||||
}
|
||||
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
|
||||
let globally_enabled = settings.show_edit_predictions(None, cx);
|
||||
menu = menu.toggleable_entry("All Files", globally_enabled, IconPosition::Start, None, {
|
||||
let fs = fs.clone();
|
||||
move |_, cx| toggle_inline_completions_globally(fs.clone(), cx)
|
||||
});
|
||||
menu = menu.separator().header("Privacy Settings");
|
||||
|
||||
menu = menu.separator().header("Display Modes");
|
||||
let current_mode = settings.edit_predictions_mode();
|
||||
let subtle_mode = matches!(current_mode, EditPredictionsMode::Subtle);
|
||||
let eager_mode = matches!(current_mode, EditPredictionsMode::Eager);
|
||||
|
||||
menu = menu.item(
|
||||
ContextMenuEntry::new("Eager")
|
||||
.toggleable(IconPosition::Start, eager_mode)
|
||||
.documentation_aside(move |_| {
|
||||
Label::new("Display predictions inline when there are no language server completions available.").into_any_element()
|
||||
})
|
||||
.handler({
|
||||
let fs = fs.clone();
|
||||
move |_, cx| {
|
||||
toggle_edit_prediction_mode(fs.clone(), EditPredictionsMode::Eager, cx)
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
menu = menu.item(
|
||||
ContextMenuEntry::new("Subtle")
|
||||
.toggleable(IconPosition::Start, subtle_mode)
|
||||
.documentation_aside(move |_| {
|
||||
Label::new("Display predictions inline only when holding a modifier key (alt by default).").into_any_element()
|
||||
})
|
||||
.handler({
|
||||
let fs = fs.clone();
|
||||
move |_, cx| {
|
||||
toggle_edit_prediction_mode(fs.clone(), EditPredictionsMode::Subtle, cx)
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
menu = menu.separator().header("Privacy Settings");
|
||||
if let Some(provider) = &self.edit_prediction_provider {
|
||||
let data_collection = provider.data_collection_state(cx);
|
||||
if data_collection.is_supported() {
|
||||
@@ -590,44 +624,6 @@ impl InlineCompletionButton {
|
||||
);
|
||||
}
|
||||
|
||||
if cx.has_flag::<feature_flags::PredictEditsNonEagerModeFeatureFlag>() {
|
||||
let is_eager_preview_enabled = match settings.edit_predictions_mode() {
|
||||
language::EditPredictionsMode::Subtle => false,
|
||||
language::EditPredictionsMode::Eager => true,
|
||||
};
|
||||
menu = menu.separator().toggleable_entry(
|
||||
"Eager Preview Mode",
|
||||
is_eager_preview_enabled,
|
||||
IconPosition::Start,
|
||||
None,
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |_window, cx| {
|
||||
update_settings_file::<AllLanguageSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
let new_mode = match is_eager_preview_enabled {
|
||||
true => language::EditPredictionsMode::Subtle,
|
||||
false => language::EditPredictionsMode::Eager,
|
||||
};
|
||||
|
||||
if let Some(edit_predictions) = settings.edit_predictions.as_mut() {
|
||||
edit_predictions.mode = new_mode;
|
||||
} else {
|
||||
settings.edit_predictions =
|
||||
Some(language_settings::EditPredictionSettingsContent {
|
||||
mode: new_mode,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
|
||||
menu = menu
|
||||
.separator()
|
||||
@@ -861,3 +857,22 @@ fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
.edit_prediction_provider = Some(EditPredictionProvider::None);
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_edit_prediction_mode(fs: Arc<dyn Fs>, mode: EditPredictionsMode, cx: &mut App) {
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
let current_mode = settings.edit_predictions_mode();
|
||||
|
||||
if current_mode != mode {
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |settings, _cx| {
|
||||
if let Some(edit_predictions) = settings.edit_predictions.as_mut() {
|
||||
edit_predictions.mode = mode;
|
||||
} else {
|
||||
settings.edit_predictions =
|
||||
Some(language_settings::EditPredictionSettingsContent {
|
||||
mode,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,8 +526,8 @@ impl DerefMut for ChunkRendererContext<'_, '_> {
|
||||
/// A set of edits to a given version of a buffer, computed asynchronously.
|
||||
#[derive(Debug)]
|
||||
pub struct Diff {
|
||||
pub(crate) base_version: clock::Global,
|
||||
line_ending: LineEnding,
|
||||
pub base_version: clock::Global,
|
||||
pub line_ending: LineEnding,
|
||||
pub edits: Vec<(Range<usize>, Arc<str>)>,
|
||||
}
|
||||
|
||||
@@ -4146,12 +4146,9 @@ impl BufferSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn words_in_range(
|
||||
&self,
|
||||
query: Option<&str>,
|
||||
range: Range<usize>,
|
||||
) -> HashMap<String, Range<Anchor>> {
|
||||
if query.map_or(false, |query| query.is_empty()) {
|
||||
pub fn words_in_range(&self, query: WordsQuery) -> HashMap<String, Range<Anchor>> {
|
||||
let query_str = query.fuzzy_contents;
|
||||
if query_str.map_or(false, |query| query.is_empty()) {
|
||||
return HashMap::default();
|
||||
}
|
||||
|
||||
@@ -4161,13 +4158,13 @@ impl BufferSnapshot {
|
||||
}));
|
||||
|
||||
let mut query_ix = 0;
|
||||
let query = query.map(|query| query.chars().collect::<Vec<_>>());
|
||||
let query_len = query.as_ref().map_or(0, |query| query.len());
|
||||
let query_chars = query_str.map(|query| query.chars().collect::<Vec<_>>());
|
||||
let query_len = query_chars.as_ref().map_or(0, |query| query.len());
|
||||
|
||||
let mut words = HashMap::default();
|
||||
let mut current_word_start_ix = None;
|
||||
let mut chunk_ix = range.start;
|
||||
for chunk in self.chunks(range, false) {
|
||||
let mut chunk_ix = query.range.start;
|
||||
for chunk in self.chunks(query.range, false) {
|
||||
for (i, c) in chunk.text.char_indices() {
|
||||
let ix = chunk_ix + i;
|
||||
if classifier.is_word(c) {
|
||||
@@ -4175,12 +4172,9 @@ impl BufferSnapshot {
|
||||
current_word_start_ix = Some(ix);
|
||||
}
|
||||
|
||||
if let Some(query) = &query {
|
||||
if let Some(query_chars) = &query_chars {
|
||||
if query_ix < query_len {
|
||||
let query_c = query.get(query_ix).expect(
|
||||
"query_ix is a vec of chars, which we access only if before the end",
|
||||
);
|
||||
if c.to_lowercase().eq(query_c.to_lowercase()) {
|
||||
if c.to_lowercase().eq(query_chars[query_ix].to_lowercase()) {
|
||||
query_ix += 1;
|
||||
}
|
||||
}
|
||||
@@ -4189,10 +4183,16 @@ impl BufferSnapshot {
|
||||
} else if let Some(word_start) = current_word_start_ix.take() {
|
||||
if query_ix == query_len {
|
||||
let word_range = self.anchor_before(word_start)..self.anchor_after(ix);
|
||||
words.insert(
|
||||
self.text_for_range(word_start..ix).collect::<String>(),
|
||||
word_range,
|
||||
);
|
||||
let mut word_text = self.text_for_range(word_start..ix).peekable();
|
||||
let first_char = word_text
|
||||
.peek()
|
||||
.and_then(|first_chunk| first_chunk.chars().next());
|
||||
// Skip empty and "words" starting with digits as a heuristic to reduce useless completions
|
||||
if !query.skip_digits
|
||||
|| first_char.map_or(true, |first_char| !first_char.is_digit(10))
|
||||
{
|
||||
words.insert(word_text.collect(), word_range);
|
||||
}
|
||||
}
|
||||
}
|
||||
query_ix = 0;
|
||||
@@ -4204,6 +4204,15 @@ impl BufferSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WordsQuery<'a> {
|
||||
/// Only returns words with all chars from the fuzzy string in them.
|
||||
pub fuzzy_contents: Option<&'a str>,
|
||||
/// Skips words that start with a digit.
|
||||
pub skip_digits: bool,
|
||||
/// Buffer offset range, to look for words.
|
||||
pub range: Range<usize>,
|
||||
}
|
||||
|
||||
fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
|
||||
indent_size_for_text(text.chars_at(Point::new(row, 0)))
|
||||
}
|
||||
|
||||
@@ -3145,7 +3145,11 @@ fn test_trailing_whitespace_ranges(mut rng: StdRng) {
|
||||
fn test_words_in_range(cx: &mut gpui::App) {
|
||||
init_settings(cx, |_| {});
|
||||
|
||||
let contents = r#"let word=öäpple.bar你 Öäpple word2-öÄpPlE-Pizza-word ÖÄPPLE word"#;
|
||||
// The first line are words excluded from the results with heuristics, we do not expect them in the test assertions.
|
||||
let contents = r#"
|
||||
0_isize 123 3.4 4
|
||||
let word=öäpple.bar你 Öäpple word2-öÄpPlE-Pizza-word ÖÄPPLE word
|
||||
"#;
|
||||
|
||||
let buffer = cx.new(|cx| {
|
||||
let buffer = Buffer::local(contents, cx).with_language(Arc::new(rust_lang()), cx);
|
||||
@@ -3159,7 +3163,11 @@ fn test_words_in_range(cx: &mut gpui::App) {
|
||||
assert_eq!(
|
||||
BTreeSet::from_iter(["Pizza".to_string()]),
|
||||
snapshot
|
||||
.words_in_range(Some("piz"), 0..snapshot.len())
|
||||
.words_in_range(WordsQuery {
|
||||
fuzzy_contents: Some("piz"),
|
||||
skip_digits: true,
|
||||
range: 0..snapshot.len(),
|
||||
})
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
@@ -3171,7 +3179,11 @@ fn test_words_in_range(cx: &mut gpui::App) {
|
||||
"ÖÄPPLE".to_string(),
|
||||
]),
|
||||
snapshot
|
||||
.words_in_range(Some("öp"), 0..snapshot.len())
|
||||
.words_in_range(WordsQuery {
|
||||
fuzzy_contents: Some("öp"),
|
||||
skip_digits: true,
|
||||
range: 0..snapshot.len(),
|
||||
})
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
@@ -3183,28 +3195,44 @@ fn test_words_in_range(cx: &mut gpui::App) {
|
||||
"öäpple".to_string(),
|
||||
]),
|
||||
snapshot
|
||||
.words_in_range(Some("öÄ"), 0..snapshot.len())
|
||||
.words_in_range(WordsQuery {
|
||||
fuzzy_contents: Some("öÄ"),
|
||||
skip_digits: true,
|
||||
range: 0..snapshot.len(),
|
||||
})
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
BTreeSet::default(),
|
||||
snapshot
|
||||
.words_in_range(Some("öÄ好"), 0..snapshot.len())
|
||||
.words_in_range(WordsQuery {
|
||||
fuzzy_contents: Some("öÄ好"),
|
||||
skip_digits: true,
|
||||
range: 0..snapshot.len(),
|
||||
})
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
BTreeSet::from_iter(["bar你".to_string(),]),
|
||||
snapshot
|
||||
.words_in_range(Some("你"), 0..snapshot.len())
|
||||
.words_in_range(WordsQuery {
|
||||
fuzzy_contents: Some("你"),
|
||||
skip_digits: true,
|
||||
range: 0..snapshot.len(),
|
||||
})
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
BTreeSet::default(),
|
||||
snapshot
|
||||
.words_in_range(Some(""), 0..snapshot.len())
|
||||
.words_in_range(WordsQuery {
|
||||
fuzzy_contents: Some(""),
|
||||
skip_digits: true,
|
||||
range: 0..snapshot.len(),
|
||||
},)
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
@@ -3221,7 +3249,36 @@ fn test_words_in_range(cx: &mut gpui::App) {
|
||||
"word2".to_string(),
|
||||
]),
|
||||
snapshot
|
||||
.words_in_range(None, 0..snapshot.len())
|
||||
.words_in_range(WordsQuery {
|
||||
fuzzy_contents: None,
|
||||
skip_digits: true,
|
||||
range: 0..snapshot.len(),
|
||||
})
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
BTreeSet::from_iter([
|
||||
"0_isize".to_string(),
|
||||
"123".to_string(),
|
||||
"3".to_string(),
|
||||
"4".to_string(),
|
||||
"bar你".to_string(),
|
||||
"öÄpPlE".to_string(),
|
||||
"Öäpple".to_string(),
|
||||
"ÖÄPPLE".to_string(),
|
||||
"öäpple".to_string(),
|
||||
"let".to_string(),
|
||||
"Pizza".to_string(),
|
||||
"word".to_string(),
|
||||
"word2".to_string(),
|
||||
]),
|
||||
snapshot
|
||||
.words_in_range(WordsQuery {
|
||||
fuzzy_contents: None,
|
||||
skip_digits: false,
|
||||
range: 0..snapshot.len(),
|
||||
})
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
|
||||
@@ -555,6 +555,23 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||
// By default all language servers are rooted at the root of the worktree.
|
||||
Some(Arc::from("".as_ref()))
|
||||
}
|
||||
|
||||
/// Method only implemented by the default JSON language server adapter.
|
||||
/// Used to provide dynamic reloading of the JSON schemas used to
|
||||
/// provide autocompletion and diagnostics in Zed setting and keybind
|
||||
/// files
|
||||
fn is_primary_zed_json_schema_adapter(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Method only implemented by the default JSON language server adapter.
|
||||
/// Used to clear the cache of JSON schemas that are used to provide
|
||||
/// autocompletion and diagnostics in Zed settings and keybinds files.
|
||||
/// Should not be called unless the callee is sure that
|
||||
/// `Self::is_primary_zed_json_schema_adapter` returns `true`
|
||||
async fn clear_zed_json_schema_cache(&self) {
|
||||
unreachable!("Not implemented for this adapter. This method should only be called on the default JSON language server adapter");
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>(
|
||||
|
||||
@@ -326,8 +326,8 @@ pub struct CompletionSettings {
|
||||
/// When fetching LSP completions, determines how long to wait for a response of a particular server.
|
||||
/// When set to 0, waits indefinitely.
|
||||
///
|
||||
/// Default: 500
|
||||
#[serde(default = "lsp_fetch_timeout_ms")]
|
||||
/// Default: 0
|
||||
#[serde(default = "default_lsp_fetch_timeout_ms")]
|
||||
pub lsp_fetch_timeout_ms: u64,
|
||||
}
|
||||
|
||||
@@ -335,12 +335,13 @@ pub struct CompletionSettings {
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WordsCompletionMode {
|
||||
/// Always fetch document's words for completions.
|
||||
/// Always fetch document's words for completions along with LSP completions.
|
||||
Enabled,
|
||||
/// Only if LSP response errors/times out/is empty,
|
||||
/// Only if LSP response errors or times out,
|
||||
/// use document's words to show completions.
|
||||
Fallback,
|
||||
/// Never fetch or complete document's words for completions.
|
||||
/// (Word-based completions can still be queried via a separate action)
|
||||
Disabled,
|
||||
}
|
||||
|
||||
@@ -348,8 +349,8 @@ fn default_words_completion_mode() -> WordsCompletionMode {
|
||||
WordsCompletionMode::Fallback
|
||||
}
|
||||
|
||||
fn lsp_fetch_timeout_ms() -> u64 {
|
||||
500
|
||||
fn default_lsp_fetch_timeout_ms() -> u64 {
|
||||
0
|
||||
}
|
||||
|
||||
/// The settings for a particular language.
|
||||
@@ -580,8 +581,6 @@ pub struct CopilotSettingsContent {
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct FeaturesContent {
|
||||
/// Whether the GitHub Copilot feature is enabled.
|
||||
pub copilot: Option<bool>,
|
||||
/// Determines which edit prediction provider to use.
|
||||
pub edit_prediction_provider: Option<EditPredictionProvider>,
|
||||
}
|
||||
@@ -1158,7 +1157,6 @@ impl settings::Settings for AllLanguageSettings {
|
||||
languages.insert(language_name.clone(), language_settings);
|
||||
}
|
||||
|
||||
let mut copilot_enabled = default_value.features.as_ref().and_then(|f| f.copilot);
|
||||
let mut edit_prediction_provider = default_value
|
||||
.features
|
||||
.as_ref()
|
||||
@@ -1205,9 +1203,6 @@ impl settings::Settings for AllLanguageSettings {
|
||||
}
|
||||
|
||||
for user_settings in sources.customizations() {
|
||||
if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) {
|
||||
copilot_enabled = Some(copilot);
|
||||
}
|
||||
if let Some(provider) = user_settings
|
||||
.features
|
||||
.as_ref()
|
||||
@@ -1282,8 +1277,6 @@ impl settings::Settings for AllLanguageSettings {
|
||||
edit_predictions: EditPredictionSettings {
|
||||
provider: if let Some(provider) = edit_prediction_provider {
|
||||
provider
|
||||
} else if copilot_enabled.unwrap_or(true) {
|
||||
EditPredictionProvider::Copilot
|
||||
} else {
|
||||
EditPredictionProvider::None
|
||||
},
|
||||
|
||||
@@ -17,9 +17,11 @@ use proto::Plan;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::ops::{Add, Sub};
|
||||
use std::{future::Future, sync::Arc};
|
||||
use thiserror::Error;
|
||||
use ui::IconName;
|
||||
use util::serde::is_default;
|
||||
|
||||
pub use crate::model::*;
|
||||
pub use crate::rate_limiter::*;
|
||||
@@ -59,6 +61,7 @@ pub enum LanguageModelCompletionEvent {
|
||||
Text(String),
|
||||
ToolUse(LanguageModelToolUse),
|
||||
StartMessage { message_id: String },
|
||||
UsageUpdate(TokenUsage),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
@@ -69,6 +72,46 @@ pub enum StopReason {
|
||||
ToolUse,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct TokenUsage {
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub input_tokens: u32,
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub output_tokens: u32,
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub cache_creation_input_tokens: u32,
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub cache_read_input_tokens: u32,
|
||||
}
|
||||
|
||||
impl Add<TokenUsage> for TokenUsage {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, other: Self) -> Self {
|
||||
Self {
|
||||
input_tokens: self.input_tokens + other.input_tokens,
|
||||
output_tokens: self.output_tokens + other.output_tokens,
|
||||
cache_creation_input_tokens: self.cache_creation_input_tokens
|
||||
+ other.cache_creation_input_tokens,
|
||||
cache_read_input_tokens: self.cache_read_input_tokens + other.cache_read_input_tokens,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<TokenUsage> for TokenUsage {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, other: Self) -> Self {
|
||||
Self {
|
||||
input_tokens: self.input_tokens - other.input_tokens,
|
||||
output_tokens: self.output_tokens - other.output_tokens,
|
||||
cache_creation_input_tokens: self.cache_creation_input_tokens
|
||||
- other.cache_creation_input_tokens,
|
||||
cache_read_input_tokens: self.cache_read_input_tokens - other.cache_read_input_tokens,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
|
||||
pub struct LanguageModelToolUseId(Arc<str>);
|
||||
|
||||
@@ -176,6 +219,7 @@ pub trait LanguageModel: Send + Sync {
|
||||
Ok(LanguageModelCompletionEvent::Text(text)) => Some(Ok(text)),
|
||||
Ok(LanguageModelCompletionEvent::Stop(_)) => None,
|
||||
Ok(LanguageModelCompletionEvent::ToolUse(_)) => None,
|
||||
Ok(LanguageModelCompletionEvent::UsageUpdate(_)) => None,
|
||||
Err(err) => Some(Err(err)),
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -33,6 +33,7 @@ gpui_tokio.workspace = true
|
||||
http_client.workspace = true
|
||||
language_model.workspace = true
|
||||
lmstudio = { workspace = true, features = ["schemars"] }
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
mistral = { workspace = true, features = ["schemars"] }
|
||||
ollama = { workspace = true, features = ["schemars"] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::ui::InstructionListItem;
|
||||
use crate::AllLanguageModelSettings;
|
||||
use anthropic::{AnthropicError, ContentDelta, Event, ResponseContent};
|
||||
use anthropic::{AnthropicError, ContentDelta, Event, ResponseContent, Usage};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use credentials_provider::CredentialsProvider;
|
||||
@@ -582,12 +582,16 @@ pub fn map_to_language_model_completion_events(
|
||||
struct State {
|
||||
events: Pin<Box<dyn Send + Stream<Item = Result<Event, AnthropicError>>>>,
|
||||
tool_uses_by_index: HashMap<usize, RawToolUse>,
|
||||
usage: Usage,
|
||||
stop_reason: StopReason,
|
||||
}
|
||||
|
||||
futures::stream::unfold(
|
||||
State {
|
||||
events,
|
||||
tool_uses_by_index: HashMap::default(),
|
||||
usage: Usage::default(),
|
||||
stop_reason: StopReason::EndTurn,
|
||||
},
|
||||
|mut state| async move {
|
||||
while let Some(event) = state.events.next().await {
|
||||
@@ -599,7 +603,7 @@ pub fn map_to_language_model_completion_events(
|
||||
} => match content_block {
|
||||
ResponseContent::Text { text } => {
|
||||
return Some((
|
||||
Some(Ok(LanguageModelCompletionEvent::Text(text))),
|
||||
vec![Ok(LanguageModelCompletionEvent::Text(text))],
|
||||
state,
|
||||
));
|
||||
}
|
||||
@@ -612,28 +616,25 @@ pub fn map_to_language_model_completion_events(
|
||||
input_json: String::new(),
|
||||
},
|
||||
);
|
||||
|
||||
return Some((None, state));
|
||||
}
|
||||
},
|
||||
Event::ContentBlockDelta { index, delta } => match delta {
|
||||
ContentDelta::TextDelta { text } => {
|
||||
return Some((
|
||||
Some(Ok(LanguageModelCompletionEvent::Text(text))),
|
||||
vec![Ok(LanguageModelCompletionEvent::Text(text))],
|
||||
state,
|
||||
));
|
||||
}
|
||||
ContentDelta::InputJsonDelta { partial_json } => {
|
||||
if let Some(tool_use) = state.tool_uses_by_index.get_mut(&index) {
|
||||
tool_use.input_json.push_str(&partial_json);
|
||||
return Some((None, state));
|
||||
}
|
||||
}
|
||||
},
|
||||
Event::ContentBlockStop { index } => {
|
||||
if let Some(tool_use) = state.tool_uses_by_index.remove(&index) {
|
||||
return Some((
|
||||
Some(maybe!({
|
||||
vec![maybe!({
|
||||
Ok(LanguageModelCompletionEvent::ToolUse(
|
||||
LanguageModelToolUse {
|
||||
id: tool_use.id.into(),
|
||||
@@ -650,44 +651,63 @@ pub fn map_to_language_model_completion_events(
|
||||
},
|
||||
},
|
||||
))
|
||||
})),
|
||||
})],
|
||||
state,
|
||||
));
|
||||
}
|
||||
}
|
||||
Event::MessageStart { message } => {
|
||||
update_usage(&mut state.usage, &message.usage);
|
||||
return Some((
|
||||
Some(Ok(LanguageModelCompletionEvent::StartMessage {
|
||||
message_id: message.id,
|
||||
})),
|
||||
vec![
|
||||
Ok(LanguageModelCompletionEvent::StartMessage {
|
||||
message_id: message.id,
|
||||
}),
|
||||
Ok(LanguageModelCompletionEvent::UsageUpdate(convert_usage(
|
||||
&state.usage,
|
||||
))),
|
||||
],
|
||||
state,
|
||||
))
|
||||
));
|
||||
}
|
||||
Event::MessageDelta { delta, .. } => {
|
||||
Event::MessageDelta { delta, usage } => {
|
||||
update_usage(&mut state.usage, &usage);
|
||||
if let Some(stop_reason) = delta.stop_reason.as_deref() {
|
||||
let stop_reason = match stop_reason {
|
||||
state.stop_reason = match stop_reason {
|
||||
"end_turn" => StopReason::EndTurn,
|
||||
"max_tokens" => StopReason::MaxTokens,
|
||||
"tool_use" => StopReason::ToolUse,
|
||||
_ => StopReason::EndTurn,
|
||||
_ => {
|
||||
log::error!(
|
||||
"Unexpected anthropic stop_reason: {stop_reason}"
|
||||
);
|
||||
StopReason::EndTurn
|
||||
}
|
||||
};
|
||||
|
||||
return Some((
|
||||
Some(Ok(LanguageModelCompletionEvent::Stop(stop_reason))),
|
||||
state,
|
||||
));
|
||||
}
|
||||
return Some((
|
||||
vec![Ok(LanguageModelCompletionEvent::UsageUpdate(
|
||||
convert_usage(&state.usage),
|
||||
))],
|
||||
state,
|
||||
));
|
||||
}
|
||||
Event::MessageStop => {
|
||||
return Some((
|
||||
vec![Ok(LanguageModelCompletionEvent::Stop(state.stop_reason))],
|
||||
state,
|
||||
));
|
||||
}
|
||||
Event::Error { error } => {
|
||||
return Some((
|
||||
Some(Err(anyhow!(AnthropicError::ApiError(error)))),
|
||||
vec![Err(anyhow!(AnthropicError::ApiError(error)))],
|
||||
state,
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Err(err) => {
|
||||
return Some((Some(Err(anyhow!(err))), state));
|
||||
return Some((vec![Err(anyhow!(err))], state));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -695,7 +715,32 @@ pub fn map_to_language_model_completion_events(
|
||||
None
|
||||
},
|
||||
)
|
||||
.filter_map(|event| async move { event })
|
||||
.flat_map(futures::stream::iter)
|
||||
}
|
||||
|
||||
/// Updates usage data by preferring counts from `new`.
|
||||
fn update_usage(usage: &mut Usage, new: &Usage) {
|
||||
if let Some(input_tokens) = new.input_tokens {
|
||||
usage.input_tokens = Some(input_tokens);
|
||||
}
|
||||
if let Some(output_tokens) = new.output_tokens {
|
||||
usage.output_tokens = Some(output_tokens);
|
||||
}
|
||||
if let Some(cache_creation_input_tokens) = new.cache_creation_input_tokens {
|
||||
usage.cache_creation_input_tokens = Some(cache_creation_input_tokens);
|
||||
}
|
||||
if let Some(cache_read_input_tokens) = new.cache_read_input_tokens {
|
||||
usage.cache_read_input_tokens = Some(cache_read_input_tokens);
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_usage(usage: &Usage) -> language_model::TokenUsage {
|
||||
language_model::TokenUsage {
|
||||
input_tokens: usage.input_tokens.unwrap_or(0),
|
||||
output_tokens: usage.output_tokens.unwrap_or(0),
|
||||
cache_creation_input_tokens: usage.cache_creation_input_tokens.unwrap_or(0),
|
||||
cache_read_input_tokens: usage.cache_read_input_tokens.unwrap_or(0),
|
||||
}
|
||||
}
|
||||
|
||||
struct ConfigurationView {
|
||||
|
||||
@@ -129,7 +129,7 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
|
||||
Status::Error(err) => anyhow!(format!("Received the following error while signing into Copilot: {err}")),
|
||||
Status::Starting { task: _ } => anyhow!("Copilot is still starting, please wait for Copilot to start then try again"),
|
||||
Status::Unauthorized => anyhow!("Unable to authorize with Copilot. Please make sure that you have an active Copilot and Copilot Chat subscription."),
|
||||
Status::SignedOut => anyhow!("You have signed out of Copilot. Please sign in to Copilot and try again."),
|
||||
Status::SignedOut {..} => anyhow!("You have signed out of Copilot. Please sign in to Copilot and try again."),
|
||||
Status::SigningIn { prompt: _ } => anyhow!("Still signing into Copilot..."),
|
||||
};
|
||||
|
||||
@@ -366,7 +366,6 @@ impl Render for ConfigurationView {
|
||||
|
||||
match &self.copilot_status {
|
||||
Some(status) => match status {
|
||||
Status::Disabled => v_flex().gap_6().p_4().child(Label::new(ERROR_LABEL)),
|
||||
Status::Starting { task: _ } => {
|
||||
const LABEL: &str = "Starting Copilot...";
|
||||
v_flex()
|
||||
@@ -376,7 +375,10 @@ impl Render for ConfigurationView {
|
||||
.child(Label::new(LABEL))
|
||||
.child(loading_icon)
|
||||
}
|
||||
Status::SigningIn { prompt: _ } => {
|
||||
Status::SigningIn { prompt: _ }
|
||||
| Status::SignedOut {
|
||||
awaiting_signing_in: true,
|
||||
} => {
|
||||
const LABEL: &str = "Signing in to Copilot...";
|
||||
v_flex()
|
||||
.gap_6()
|
||||
|
||||
@@ -15,6 +15,7 @@ use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
|
||||
use smol::{
|
||||
fs::{self},
|
||||
io::BufReader,
|
||||
lock::RwLock,
|
||||
};
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -22,7 +23,7 @@ use std::{
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::{Arc, OnceLock},
|
||||
sync::Arc,
|
||||
};
|
||||
use task::{TaskTemplate, TaskTemplates, VariableName};
|
||||
use util::{fs::remove_matching, maybe, merge_json_value_into, ResultExt};
|
||||
@@ -60,7 +61,7 @@ fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
pub struct JsonLspAdapter {
|
||||
node: NodeRuntime,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
workspace_config: OnceLock<Value>,
|
||||
workspace_config: RwLock<Option<Value>>,
|
||||
}
|
||||
|
||||
impl JsonLspAdapter {
|
||||
@@ -141,6 +142,20 @@ impl JsonLspAdapter {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_or_init_workspace_config(&self, cx: &mut AsyncApp) -> Result<Value> {
|
||||
{
|
||||
let reader = self.workspace_config.read().await;
|
||||
if let Some(config) = reader.as_ref() {
|
||||
return Ok(config.clone());
|
||||
}
|
||||
}
|
||||
let mut writer = self.workspace_config.write().await;
|
||||
let config =
|
||||
cx.update(|cx| Self::get_workspace_config(self.languages.language_names(), cx))?;
|
||||
writer.replace(config.clone());
|
||||
return Ok(config);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
@@ -251,11 +266,7 @@ impl LspAdapter for JsonLspAdapter {
|
||||
_: Arc<dyn LanguageToolchainStore>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
let mut config = cx.update(|cx| {
|
||||
self.workspace_config
|
||||
.get_or_init(|| Self::get_workspace_config(self.languages.language_names(), cx))
|
||||
.clone()
|
||||
})?;
|
||||
let mut config = self.get_or_init_workspace_config(cx).await?;
|
||||
|
||||
let project_options = cx.update(|cx| {
|
||||
language_server_settings(delegate.as_ref(), &self.name(), cx)
|
||||
@@ -277,6 +288,14 @@ impl LspAdapter for JsonLspAdapter {
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_primary_zed_json_schema_adapter(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn clear_zed_json_schema_cache(&self) {
|
||||
self.workspace_config.write().await.take();
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
|
||||
@@ -10,3 +10,8 @@ brackets = [
|
||||
]
|
||||
tab_size = 2
|
||||
prettier_parser_name = "json"
|
||||
scope_opt_in_language_servers = ["json-language-server"]
|
||||
|
||||
[overrides.string]
|
||||
word_characters = [":"]
|
||||
opt_into_language_servers = ["json-language-server"]
|
||||
|
||||
@@ -10,3 +10,8 @@ brackets = [
|
||||
]
|
||||
tab_size = 2
|
||||
prettier_parser_name = "jsonc"
|
||||
|
||||
scope_opt_in_language_servers = ["json-language-server"]
|
||||
[overrides.string]
|
||||
word_characters = [":"]
|
||||
opt_into_language_servers = ["json-language-server"]
|
||||
|
||||
@@ -68,7 +68,8 @@ pub struct MultiBuffer {
|
||||
/// Contains the state of the buffers being edited
|
||||
buffers: RefCell<HashMap<BufferId, BufferState>>,
|
||||
// only used by consumers using `set_excerpts_for_buffer`
|
||||
buffers_by_path: BTreeMap<PathKey, Vec<ExcerptId>>,
|
||||
excerpts_by_path: BTreeMap<PathKey, Vec<ExcerptId>>,
|
||||
paths_by_excerpt: HashMap<ExcerptId, PathKey>,
|
||||
diffs: HashMap<BufferId, DiffState>,
|
||||
// all_diff_hunks_expanded: bool,
|
||||
subscriptions: Topic,
|
||||
@@ -360,12 +361,19 @@ impl ExcerptBoundary {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ExpandInfo {
|
||||
pub direction: ExpandExcerptDirection,
|
||||
pub excerpt_id: ExcerptId,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct RowInfo {
|
||||
pub buffer_id: Option<BufferId>,
|
||||
pub buffer_row: Option<u32>,
|
||||
pub multibuffer_row: Option<MultiBufferRow>,
|
||||
pub diff_status: Option<buffer_diff::DiffHunkStatus>,
|
||||
pub expand_info: Option<ExpandInfo>,
|
||||
}
|
||||
|
||||
/// A slice into a [`Buffer`] that is being edited in a [`MultiBuffer`].
|
||||
@@ -438,6 +446,7 @@ pub struct DiffTransformSummary {
|
||||
pub struct MultiBufferRows<'a> {
|
||||
point: Point,
|
||||
is_empty: bool,
|
||||
is_singleton: bool,
|
||||
cursor: MultiBufferCursor<'a, Point>,
|
||||
}
|
||||
|
||||
@@ -569,7 +578,8 @@ impl MultiBuffer {
|
||||
singleton: false,
|
||||
capability,
|
||||
title: None,
|
||||
buffers_by_path: Default::default(),
|
||||
excerpts_by_path: Default::default(),
|
||||
paths_by_excerpt: Default::default(),
|
||||
buffer_changed_since_sync: Default::default(),
|
||||
history: History {
|
||||
next_transaction_id: clock::Lamport::default(),
|
||||
@@ -585,7 +595,8 @@ impl MultiBuffer {
|
||||
Self {
|
||||
snapshot: Default::default(),
|
||||
buffers: Default::default(),
|
||||
buffers_by_path: Default::default(),
|
||||
excerpts_by_path: Default::default(),
|
||||
paths_by_excerpt: Default::default(),
|
||||
diffs: HashMap::default(),
|
||||
subscriptions: Default::default(),
|
||||
singleton: false,
|
||||
@@ -630,7 +641,8 @@ impl MultiBuffer {
|
||||
Self {
|
||||
snapshot: RefCell::new(self.snapshot.borrow().clone()),
|
||||
buffers: RefCell::new(buffers),
|
||||
buffers_by_path: Default::default(),
|
||||
excerpts_by_path: Default::default(),
|
||||
paths_by_excerpt: Default::default(),
|
||||
diffs: diff_bases,
|
||||
subscriptions: Default::default(),
|
||||
singleton: self.singleton,
|
||||
@@ -1470,7 +1482,7 @@ impl MultiBuffer {
|
||||
}
|
||||
|
||||
pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option<Anchor> {
|
||||
let excerpt_id = self.buffers_by_path.get(path)?.first()?;
|
||||
let excerpt_id = self.excerpts_by_path.get(path)?.first()?;
|
||||
let snapshot = self.snapshot(cx);
|
||||
let excerpt = snapshot.excerpt(*excerpt_id)?;
|
||||
Some(Anchor::in_buffer(
|
||||
@@ -1481,7 +1493,93 @@ impl MultiBuffer {
|
||||
}
|
||||
|
||||
pub fn excerpt_paths(&self) -> impl Iterator<Item = &PathKey> {
|
||||
self.buffers_by_path.keys()
|
||||
self.excerpts_by_path.keys()
|
||||
}
|
||||
|
||||
fn expand_excerpts_with_paths(
|
||||
&mut self,
|
||||
ids: impl IntoIterator<Item = ExcerptId>,
|
||||
line_count: u32,
|
||||
direction: ExpandExcerptDirection,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let grouped = ids
|
||||
.into_iter()
|
||||
.chunk_by(|id| self.paths_by_excerpt.get(id).cloned())
|
||||
.into_iter()
|
||||
.flat_map(|(k, v)| Some((k?, v.into_iter().collect::<Vec<_>>())))
|
||||
.collect::<Vec<_>>();
|
||||
let snapshot = self.snapshot(cx);
|
||||
|
||||
for (path, ids) in grouped.into_iter() {
|
||||
let Some(excerpt_ids) = self.excerpts_by_path.get(&path) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let ids_to_expand = HashSet::from_iter(ids);
|
||||
let expanded_ranges = excerpt_ids.iter().filter_map(|excerpt_id| {
|
||||
let excerpt = snapshot.excerpt(*excerpt_id)?;
|
||||
|
||||
let mut context = excerpt.range.context.to_point(&excerpt.buffer);
|
||||
if ids_to_expand.contains(excerpt_id) {
|
||||
match direction {
|
||||
ExpandExcerptDirection::Up => {
|
||||
context.start.row = context.start.row.saturating_sub(line_count);
|
||||
context.start.column = 0;
|
||||
}
|
||||
ExpandExcerptDirection::Down => {
|
||||
context.end.row =
|
||||
(context.end.row + line_count).min(excerpt.buffer.max_point().row);
|
||||
context.end.column = excerpt.buffer.line_len(context.end.row);
|
||||
}
|
||||
ExpandExcerptDirection::UpAndDown => {
|
||||
context.start.row = context.start.row.saturating_sub(line_count);
|
||||
context.start.column = 0;
|
||||
context.end.row =
|
||||
(context.end.row + line_count).min(excerpt.buffer.max_point().row);
|
||||
context.end.column = excerpt.buffer.line_len(context.end.row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(ExcerptRange {
|
||||
context,
|
||||
primary: excerpt
|
||||
.range
|
||||
.primary
|
||||
.as_ref()
|
||||
.map(|range| range.to_point(&excerpt.buffer)),
|
||||
})
|
||||
});
|
||||
let mut merged_ranges: Vec<ExcerptRange<Point>> = Vec::new();
|
||||
for range in expanded_ranges {
|
||||
if let Some(last_range) = merged_ranges.last_mut() {
|
||||
if last_range.context.end >= range.context.start {
|
||||
last_range.context.end = range.context.end;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
merged_ranges.push(range)
|
||||
}
|
||||
let Some(excerpt_id) = excerpt_ids.first() else {
|
||||
continue;
|
||||
};
|
||||
let Some(buffer_id) = &snapshot.buffer_id_for_excerpt(*excerpt_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(buffer) = self
|
||||
.buffers
|
||||
.borrow()
|
||||
.get(buffer_id)
|
||||
.map(|b| b.buffer.clone())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
self.update_path_excerpts(path.clone(), buffer, &buffer_snapshot, merged_ranges, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets excerpts, returns `true` if at least one new excerpt was added.
|
||||
@@ -1495,15 +1593,30 @@ impl MultiBuffer {
|
||||
) -> bool {
|
||||
let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
let (new, _) = build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
|
||||
self.update_path_excerpts(path, buffer, &buffer_snapshot, new, cx)
|
||||
}
|
||||
|
||||
fn update_path_excerpts(
|
||||
&mut self,
|
||||
path: PathKey,
|
||||
buffer: Entity<Buffer>,
|
||||
buffer_snapshot: &BufferSnapshot,
|
||||
new: Vec<ExcerptRange<Point>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let mut insert_after = self
|
||||
.buffers_by_path
|
||||
.excerpts_by_path
|
||||
.range(..path.clone())
|
||||
.next_back()
|
||||
.map(|(_, value)| *value.last().unwrap())
|
||||
.unwrap_or(ExcerptId::min());
|
||||
let existing = self.buffers_by_path.get(&path).cloned().unwrap_or_default();
|
||||
|
||||
let (new, _) = build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
|
||||
let existing = self
|
||||
.excerpts_by_path
|
||||
.get(&path)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut new_iter = new.into_iter().peekable();
|
||||
let mut existing_iter = existing.into_iter().peekable();
|
||||
@@ -1586,20 +1699,23 @@ impl MultiBuffer {
|
||||
));
|
||||
self.remove_excerpts(to_remove, cx);
|
||||
if new_excerpt_ids.is_empty() {
|
||||
self.buffers_by_path.remove(&path);
|
||||
self.excerpts_by_path.remove(&path);
|
||||
} else {
|
||||
self.buffers_by_path.insert(path, new_excerpt_ids);
|
||||
for excerpt_id in &new_excerpt_ids {
|
||||
self.paths_by_excerpt.insert(*excerpt_id, path.clone());
|
||||
}
|
||||
self.excerpts_by_path.insert(path, new_excerpt_ids);
|
||||
}
|
||||
|
||||
added_a_new_excerpt
|
||||
}
|
||||
|
||||
pub fn paths(&self) -> impl Iterator<Item = PathKey> + '_ {
|
||||
self.buffers_by_path.keys().cloned()
|
||||
self.excerpts_by_path.keys().cloned()
|
||||
}
|
||||
|
||||
pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
|
||||
if let Some(to_remove) = self.buffers_by_path.remove(&path) {
|
||||
if let Some(to_remove) = self.excerpts_by_path.remove(&path) {
|
||||
self.remove_excerpts(to_remove, cx)
|
||||
}
|
||||
}
|
||||
@@ -2007,22 +2123,17 @@ impl MultiBuffer {
|
||||
cx: &App,
|
||||
) -> Option<(Entity<Buffer>, Point, ExcerptId)> {
|
||||
let snapshot = self.read(cx);
|
||||
let point = point.to_point(&snapshot);
|
||||
let mut cursor = snapshot.cursor::<Point>();
|
||||
cursor.seek(&point);
|
||||
|
||||
cursor.region().and_then(|region| {
|
||||
if !region.is_main_buffer {
|
||||
return None;
|
||||
}
|
||||
|
||||
let overshoot = point - region.range.start;
|
||||
let buffer_point = region.buffer_range.start + overshoot;
|
||||
let buffer = self.buffers.borrow()[®ion.buffer.remote_id()]
|
||||
let (buffer, point, is_main_buffer) =
|
||||
snapshot.point_to_buffer_point(point.to_point(&snapshot))?;
|
||||
Some((
|
||||
self.buffers
|
||||
.borrow()
|
||||
.get(&buffer.remote_id())?
|
||||
.buffer
|
||||
.clone();
|
||||
Some((buffer, buffer_point, region.excerpt.id))
|
||||
})
|
||||
.clone(),
|
||||
point,
|
||||
is_main_buffer,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn buffer_point_to_anchor(
|
||||
@@ -2076,6 +2187,7 @@ impl MultiBuffer {
|
||||
let mut removed_buffer_ids = Vec::new();
|
||||
|
||||
while let Some(excerpt_id) = excerpt_ids.next() {
|
||||
self.paths_by_excerpt.remove(&excerpt_id);
|
||||
// Seek to the next excerpt to remove, preserving any preceding excerpts.
|
||||
let locator = snapshot.excerpt_locator_for_id(excerpt_id);
|
||||
new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &());
|
||||
@@ -2640,6 +2752,10 @@ impl MultiBuffer {
|
||||
return;
|
||||
}
|
||||
self.sync(cx);
|
||||
if !self.excerpts_by_path.is_empty() {
|
||||
self.expand_excerpts_with_paths(ids, line_count, direction, cx);
|
||||
return;
|
||||
}
|
||||
let mut snapshot = self.snapshot.borrow_mut();
|
||||
|
||||
let ids = ids.into_iter().collect::<Vec<_>>();
|
||||
@@ -4039,6 +4155,7 @@ impl MultiBufferSnapshot {
|
||||
let mut result = MultiBufferRows {
|
||||
point: Point::new(0, 0),
|
||||
is_empty: self.excerpts.is_empty(),
|
||||
is_singleton: self.is_singleton(),
|
||||
cursor,
|
||||
};
|
||||
result.seek(start_row);
|
||||
@@ -4176,22 +4293,36 @@ impl MultiBufferSnapshot {
|
||||
let region = cursor.region()?;
|
||||
let overshoot = offset - region.range.start;
|
||||
let buffer_offset = region.buffer_range.start + overshoot;
|
||||
if buffer_offset > region.buffer.len() {
|
||||
if buffer_offset == region.buffer.len() + 1
|
||||
&& region.has_trailing_newline
|
||||
&& !region.is_main_buffer
|
||||
{
|
||||
return Some((&cursor.excerpt()?.buffer, cursor.main_buffer_position()?));
|
||||
} else if buffer_offset > region.buffer.len() {
|
||||
return None;
|
||||
}
|
||||
Some((region.buffer, buffer_offset))
|
||||
}
|
||||
|
||||
pub fn point_to_buffer_point(&self, point: Point) -> Option<(&BufferSnapshot, Point, bool)> {
|
||||
pub fn point_to_buffer_point(
|
||||
&self,
|
||||
point: Point,
|
||||
) -> Option<(&BufferSnapshot, Point, ExcerptId)> {
|
||||
let mut cursor = self.cursor::<Point>();
|
||||
cursor.seek(&point);
|
||||
let region = cursor.region()?;
|
||||
let overshoot = point - region.range.start;
|
||||
let buffer_point = region.buffer_range.start + overshoot;
|
||||
if buffer_point > region.buffer.max_point() {
|
||||
let excerpt = cursor.excerpt()?;
|
||||
if buffer_point == region.buffer.max_point() + Point::new(1, 0)
|
||||
&& region.has_trailing_newline
|
||||
&& !region.is_main_buffer
|
||||
{
|
||||
return Some((&excerpt.buffer, cursor.main_buffer_position()?, excerpt.id));
|
||||
} else if buffer_point > region.buffer.max_point() {
|
||||
return None;
|
||||
}
|
||||
Some((region.buffer, buffer_point, region.is_main_buffer))
|
||||
Some((region.buffer, buffer_point, excerpt.id))
|
||||
}
|
||||
|
||||
pub fn suggested_indents(
|
||||
@@ -4733,6 +4864,9 @@ impl MultiBufferSnapshot {
|
||||
.buffer
|
||||
.text_summary_for_range(region.buffer_range.start.key..buffer_point),
|
||||
);
|
||||
if point == region.range.end.key && region.has_trailing_newline {
|
||||
position.add_assign(&D::from_text_summary(&TextSummary::newline()));
|
||||
}
|
||||
return Some(position);
|
||||
} else {
|
||||
return Some(D::from_text_summary(&self.text_summary()));
|
||||
@@ -6204,6 +6338,22 @@ where
|
||||
self.cached_region.clone()
|
||||
}
|
||||
|
||||
fn is_at_start_of_excerpt(&mut self) -> bool {
|
||||
if self.diff_transforms.start().1 > *self.excerpts.start() {
|
||||
return false;
|
||||
} else if self.diff_transforms.start().1 < *self.excerpts.start() {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.diff_transforms.prev(&());
|
||||
let prev_transform = self.diff_transforms.item();
|
||||
self.diff_transforms.next(&());
|
||||
|
||||
prev_transform.map_or(true, |next_transform| {
|
||||
matches!(next_transform, DiffTransform::BufferContent { .. })
|
||||
})
|
||||
}
|
||||
|
||||
fn is_at_end_of_excerpt(&mut self) -> bool {
|
||||
if self.diff_transforms.end(&()).1 < self.excerpts.end(&()) {
|
||||
return false;
|
||||
@@ -7080,6 +7230,7 @@ impl Iterator for MultiBufferRows<'_> {
|
||||
buffer_row: Some(0),
|
||||
multibuffer_row: Some(MultiBufferRow(0)),
|
||||
diff_status: None,
|
||||
expand_info: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7091,7 +7242,6 @@ impl Iterator for MultiBufferRows<'_> {
|
||||
} else {
|
||||
if self.point == self.cursor.diff_transforms.end(&()).0 .0 {
|
||||
let multibuffer_row = MultiBufferRow(self.point.row);
|
||||
self.point += Point::new(1, 0);
|
||||
let last_excerpt = self
|
||||
.cursor
|
||||
.excerpts
|
||||
@@ -7103,11 +7253,43 @@ impl Iterator for MultiBufferRows<'_> {
|
||||
.end
|
||||
.to_point(&last_excerpt.buffer)
|
||||
.row;
|
||||
|
||||
let first_row = last_excerpt
|
||||
.range
|
||||
.context
|
||||
.start
|
||||
.to_point(&last_excerpt.buffer)
|
||||
.row;
|
||||
|
||||
let expand_info = if self.is_singleton {
|
||||
None
|
||||
} else {
|
||||
let needs_expand_up = first_row == last_row
|
||||
&& last_row > 0
|
||||
&& !region.diff_hunk_status.is_some_and(|d| d.is_deleted());
|
||||
let needs_expand_down = last_row < last_excerpt.buffer.max_point().row;
|
||||
|
||||
if needs_expand_up && needs_expand_down {
|
||||
Some(ExpandExcerptDirection::UpAndDown)
|
||||
} else if needs_expand_up {
|
||||
Some(ExpandExcerptDirection::Up)
|
||||
} else if needs_expand_down {
|
||||
Some(ExpandExcerptDirection::Down)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.map(|direction| ExpandInfo {
|
||||
direction,
|
||||
excerpt_id: last_excerpt.id,
|
||||
})
|
||||
};
|
||||
self.point += Point::new(1, 0);
|
||||
return Some(RowInfo {
|
||||
buffer_id: Some(last_excerpt.buffer_id),
|
||||
buffer_row: Some(last_row),
|
||||
multibuffer_row: Some(multibuffer_row),
|
||||
diff_status: None,
|
||||
expand_info,
|
||||
});
|
||||
} else {
|
||||
return None;
|
||||
@@ -7117,6 +7299,33 @@ impl Iterator for MultiBufferRows<'_> {
|
||||
|
||||
let overshoot = self.point - region.range.start;
|
||||
let buffer_point = region.buffer_range.start + overshoot;
|
||||
let expand_info = if self.is_singleton {
|
||||
None
|
||||
} else {
|
||||
let needs_expand_up = self.point.row == region.range.start.row
|
||||
&& self.cursor.is_at_start_of_excerpt()
|
||||
&& buffer_point.row > 0;
|
||||
let needs_expand_down = (region.excerpt.has_trailing_newline
|
||||
&& self.point.row + 1 == region.range.end.row
|
||||
|| !region.excerpt.has_trailing_newline && self.point.row == region.range.end.row)
|
||||
&& self.cursor.is_at_end_of_excerpt()
|
||||
&& buffer_point.row < region.buffer.max_point().row;
|
||||
|
||||
if needs_expand_up && needs_expand_down {
|
||||
Some(ExpandExcerptDirection::UpAndDown)
|
||||
} else if needs_expand_up {
|
||||
Some(ExpandExcerptDirection::Up)
|
||||
} else if needs_expand_down {
|
||||
Some(ExpandExcerptDirection::Down)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.map(|direction| ExpandInfo {
|
||||
direction,
|
||||
excerpt_id: region.excerpt.id,
|
||||
})
|
||||
};
|
||||
|
||||
let result = Some(RowInfo {
|
||||
buffer_id: Some(region.buffer.remote_id()),
|
||||
buffer_row: Some(buffer_point.row),
|
||||
@@ -7124,6 +7333,7 @@ impl Iterator for MultiBufferRows<'_> {
|
||||
diff_status: region
|
||||
.diff_hunk_status
|
||||
.filter(|_| self.point < region.range.end),
|
||||
expand_info,
|
||||
});
|
||||
self.point += Point::new(1, 0);
|
||||
result
|
||||
|
||||
@@ -29,7 +29,8 @@ fn test_empty_singleton(cx: &mut App) {
|
||||
buffer_id: Some(buffer_id),
|
||||
buffer_row: Some(0),
|
||||
multibuffer_row: Some(MultiBufferRow(0)),
|
||||
diff_status: None
|
||||
diff_status: None,
|
||||
expand_info: None,
|
||||
}]
|
||||
);
|
||||
}
|
||||
@@ -2118,6 +2119,7 @@ struct ReferenceRegion {
|
||||
range: Range<usize>,
|
||||
buffer_start: Option<Point>,
|
||||
status: Option<DiffHunkStatus>,
|
||||
excerpt_id: Option<ExcerptId>,
|
||||
}
|
||||
|
||||
impl ReferenceMultibuffer {
|
||||
@@ -2274,6 +2276,7 @@ impl ReferenceMultibuffer {
|
||||
range: len..text.len(),
|
||||
buffer_start: Some(buffer.offset_to_point(offset)),
|
||||
status: None,
|
||||
excerpt_id: Some(excerpt.id),
|
||||
});
|
||||
|
||||
// Add the deleted text for the hunk.
|
||||
@@ -2293,6 +2296,7 @@ impl ReferenceMultibuffer {
|
||||
base_buffer.offset_to_point(hunk.diff_base_byte_range.start),
|
||||
),
|
||||
status: Some(DiffHunkStatus::deleted(hunk.secondary_status)),
|
||||
excerpt_id: Some(excerpt.id),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2308,6 +2312,7 @@ impl ReferenceMultibuffer {
|
||||
range: len..text.len(),
|
||||
buffer_start: Some(buffer.offset_to_point(offset)),
|
||||
status: Some(DiffHunkStatus::added(hunk.secondary_status)),
|
||||
excerpt_id: Some(excerpt.id),
|
||||
});
|
||||
offset = hunk_range.end;
|
||||
}
|
||||
@@ -2322,6 +2327,7 @@ impl ReferenceMultibuffer {
|
||||
range: len..text.len(),
|
||||
buffer_start: Some(buffer.offset_to_point(offset)),
|
||||
status: None,
|
||||
excerpt_id: Some(excerpt.id),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2332,6 +2338,7 @@ impl ReferenceMultibuffer {
|
||||
range: 0..1,
|
||||
buffer_start: Some(Point::new(0, 0)),
|
||||
status: None,
|
||||
excerpt_id: None,
|
||||
});
|
||||
} else {
|
||||
text.pop();
|
||||
@@ -2345,12 +2352,58 @@ impl ReferenceMultibuffer {
|
||||
.map(|line| {
|
||||
let row_info = regions
|
||||
.iter()
|
||||
.find(|region| region.range.contains(&ix))
|
||||
.map_or(RowInfo::default(), |region| {
|
||||
.position(|region| region.range.contains(&ix))
|
||||
.map_or(RowInfo::default(), |region_ix| {
|
||||
let region = ®ions[region_ix];
|
||||
let buffer_row = region.buffer_start.map(|start_point| {
|
||||
start_point.row
|
||||
+ text[region.range.start..ix].matches('\n').count() as u32
|
||||
});
|
||||
let is_excerpt_start = region_ix == 0
|
||||
|| ®ions[region_ix - 1].excerpt_id != ®ion.excerpt_id
|
||||
|| regions[region_ix - 1].range.is_empty();
|
||||
let mut is_excerpt_end = region_ix == regions.len() - 1
|
||||
|| ®ions[region_ix + 1].excerpt_id != ®ion.excerpt_id;
|
||||
let is_start = !text[region.range.start..ix].contains('\n');
|
||||
let mut is_end = if region.range.end > text.len() {
|
||||
!text[ix..].contains('\n')
|
||||
} else {
|
||||
text[ix..region.range.end.min(text.len())]
|
||||
.matches('\n')
|
||||
.count()
|
||||
== 1
|
||||
};
|
||||
if region_ix < regions.len() - 1
|
||||
&& !text[ix..].contains("\n")
|
||||
&& region.status == Some(DiffHunkStatus::added_none())
|
||||
&& regions[region_ix + 1].excerpt_id == region.excerpt_id
|
||||
&& regions[region_ix + 1].range.start == text.len()
|
||||
{
|
||||
is_end = true;
|
||||
is_excerpt_end = true;
|
||||
}
|
||||
let mut expand_direction = None;
|
||||
if let Some(buffer) = &self
|
||||
.excerpts
|
||||
.iter()
|
||||
.find(|e| e.id == region.excerpt_id.unwrap())
|
||||
.map(|e| e.buffer.clone())
|
||||
{
|
||||
let needs_expand_up =
|
||||
is_excerpt_start && is_start && buffer_row.unwrap() > 0;
|
||||
let needs_expand_down = is_excerpt_end
|
||||
&& is_end
|
||||
&& buffer.read(cx).max_point().row > buffer_row.unwrap();
|
||||
expand_direction = if needs_expand_up && needs_expand_down {
|
||||
Some(ExpandExcerptDirection::UpAndDown)
|
||||
} else if needs_expand_up {
|
||||
Some(ExpandExcerptDirection::Up)
|
||||
} else if needs_expand_down {
|
||||
Some(ExpandExcerptDirection::Down)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
RowInfo {
|
||||
buffer_id: region.buffer_id,
|
||||
diff_status: region.status,
|
||||
@@ -2358,6 +2411,12 @@ impl ReferenceMultibuffer {
|
||||
multibuffer_row: Some(MultiBufferRow(
|
||||
text[..ix].matches('\n').count() as u32
|
||||
)),
|
||||
expand_info: expand_direction.zip(region.excerpt_id).map(
|
||||
|(direction, excerpt_id)| ExpandInfo {
|
||||
direction,
|
||||
excerpt_id,
|
||||
},
|
||||
),
|
||||
}
|
||||
});
|
||||
ix += line.len() + 1;
|
||||
@@ -3121,6 +3180,100 @@ fn test_summaries_for_anchors(cx: &mut TestAppContext) {
|
||||
assert_eq!(point_2, Point::new(3, 0));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_trailing_deletion_without_newline(cx: &mut TestAppContext) {
|
||||
let base_text_1 = "one\ntwo".to_owned();
|
||||
let text_1 = "one\n".to_owned();
|
||||
|
||||
let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
|
||||
let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(&base_text_1, &buffer_1, cx));
|
||||
cx.run_until_parked();
|
||||
|
||||
let multibuffer = cx.new(|cx| {
|
||||
let mut multibuffer = MultiBuffer::singleton(buffer_1.clone(), cx);
|
||||
multibuffer.add_diff(diff_1.clone(), cx);
|
||||
multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
|
||||
multibuffer
|
||||
});
|
||||
|
||||
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
|
||||
(multibuffer.snapshot(cx), multibuffer.subscribe())
|
||||
});
|
||||
|
||||
assert_new_snapshot(
|
||||
&multibuffer,
|
||||
&mut snapshot,
|
||||
&mut subscription,
|
||||
cx,
|
||||
indoc!(
|
||||
"
|
||||
one
|
||||
- two
|
||||
"
|
||||
),
|
||||
);
|
||||
|
||||
assert_eq!(snapshot.max_point(), Point::new(2, 0));
|
||||
assert_eq!(snapshot.len(), 8);
|
||||
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.dimensions_from_points::<Point>([Point::new(2, 0)])
|
||||
.collect::<Vec<_>>(),
|
||||
vec![Point::new(2, 0)]
|
||||
);
|
||||
|
||||
let (_, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
|
||||
assert_eq!(translated_offset, "one\n".len());
|
||||
let (_, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
|
||||
assert_eq!(translated_point, Point::new(1, 0));
|
||||
|
||||
// The same, for an excerpt that's not at the end of the multibuffer.
|
||||
|
||||
let text_2 = "foo\n".to_owned();
|
||||
let buffer_2 = cx.new(|cx| Buffer::local(&text_2, cx));
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[ExcerptRange {
|
||||
context: Point::new(0, 0)..Point::new(1, 0),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
assert_new_snapshot(
|
||||
&multibuffer,
|
||||
&mut snapshot,
|
||||
&mut subscription,
|
||||
cx,
|
||||
indoc!(
|
||||
"
|
||||
one
|
||||
- two
|
||||
|
||||
foo
|
||||
"
|
||||
),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.dimensions_from_points::<Point>([Point::new(2, 0)])
|
||||
.collect::<Vec<_>>(),
|
||||
vec![Point::new(2, 0)]
|
||||
);
|
||||
|
||||
let buffer_1_id = buffer_1.read_with(cx, |buffer_1, _| buffer_1.remote_id());
|
||||
let (buffer, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
|
||||
assert_eq!(buffer.remote_id(), buffer_1_id);
|
||||
assert_eq!(translated_offset, "one\n".len());
|
||||
let (buffer, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
|
||||
assert_eq!(buffer.remote_id(), buffer_1_id);
|
||||
assert_eq!(translated_point, Point::new(1, 0));
|
||||
}
|
||||
|
||||
fn format_diff(
|
||||
text: &str,
|
||||
row_infos: &Vec<RowInfo>,
|
||||
@@ -3379,16 +3532,12 @@ fn assert_position_translation(snapshot: &MultiBufferSnapshot) {
|
||||
}
|
||||
}
|
||||
|
||||
let point = snapshot.max_point();
|
||||
let Some((buffer, offset)) = snapshot.point_to_buffer_offset(point) else {
|
||||
return;
|
||||
};
|
||||
assert!(offset <= buffer.len(),);
|
||||
|
||||
let Some((buffer, point, _)) = snapshot.point_to_buffer_point(point) else {
|
||||
return;
|
||||
};
|
||||
assert!(point <= buffer.max_point(),);
|
||||
if let Some((buffer, offset)) = snapshot.point_to_buffer_offset(snapshot.max_point()) {
|
||||
assert!(offset <= buffer.len());
|
||||
}
|
||||
if let Some((buffer, point, _)) = snapshot.point_to_buffer_point(snapshot.max_point()) {
|
||||
assert!(point <= buffer.max_point());
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_line_indents(snapshot: &MultiBufferSnapshot) {
|
||||
|
||||
@@ -277,6 +277,7 @@ impl PickerDelegate for OutlineViewDelegate {
|
||||
cx: &mut Context<Picker<OutlineViewDelegate>>,
|
||||
) {
|
||||
self.prev_scroll_position.take();
|
||||
self.set_selected_index(self.selected_match_index, true, cx);
|
||||
|
||||
self.active_editor.update(cx, |active_editor, cx| {
|
||||
let highlight = active_editor
|
||||
@@ -381,12 +382,13 @@ mod tests {
|
||||
path!("/dir"),
|
||||
json!({
|
||||
"a.rs": indoc!{"
|
||||
struct SingleLine; // display line 0
|
||||
// display line 1
|
||||
struct MultiLine { // display line 2
|
||||
field_1: i32, // display line 3
|
||||
field_2: i32, // display line 4
|
||||
} // display line 5
|
||||
// display line 0
|
||||
struct SingleLine; // display line 1
|
||||
// display line 2
|
||||
struct MultiLine { // display line 3
|
||||
field_1: i32, // display line 4
|
||||
field_2: i32, // display line 5
|
||||
} // display line 6
|
||||
"}
|
||||
}),
|
||||
)
|
||||
@@ -439,23 +441,29 @@ mod tests {
|
||||
);
|
||||
assert_single_caret_at_row(&editor, 0, cx);
|
||||
|
||||
cx.dispatch_action(menu::Confirm);
|
||||
// Ensures that outline still goes to entry even if no queries have been made
|
||||
assert_single_caret_at_row(&editor, 1, cx);
|
||||
|
||||
let outline_view = open_outline_view(&workspace, cx);
|
||||
|
||||
cx.dispatch_action(menu::SelectNext);
|
||||
ensure_outline_view_contents(&outline_view, cx);
|
||||
assert_eq!(
|
||||
highlighted_display_rows(&editor, cx),
|
||||
vec![2, 3, 4, 5],
|
||||
vec![3, 4, 5, 6],
|
||||
"Second struct's rows should be highlighted"
|
||||
);
|
||||
assert_single_caret_at_row(&editor, 0, cx);
|
||||
assert_single_caret_at_row(&editor, 1, cx);
|
||||
|
||||
cx.dispatch_action(menu::SelectPrevious);
|
||||
ensure_outline_view_contents(&outline_view, cx);
|
||||
assert_eq!(
|
||||
highlighted_display_rows(&editor, cx),
|
||||
vec![0],
|
||||
vec![1],
|
||||
"First struct's row should be highlighted"
|
||||
);
|
||||
assert_single_caret_at_row(&editor, 0, cx);
|
||||
assert_single_caret_at_row(&editor, 1, cx);
|
||||
|
||||
cx.dispatch_action(menu::Cancel);
|
||||
ensure_outline_view_contents(&outline_view, cx);
|
||||
@@ -464,7 +472,7 @@ mod tests {
|
||||
Vec::<u32>::new(),
|
||||
"No rows should be highlighted after outline view is cancelled and closed"
|
||||
);
|
||||
assert_single_caret_at_row(&editor, 0, cx);
|
||||
assert_single_caret_at_row(&editor, 1, cx);
|
||||
|
||||
let outline_view = open_outline_view(&workspace, cx);
|
||||
ensure_outline_view_contents(&outline_view, cx);
|
||||
@@ -473,16 +481,16 @@ mod tests {
|
||||
Vec::<u32>::new(),
|
||||
"Reopened outline view should have no highlights"
|
||||
);
|
||||
assert_single_caret_at_row(&editor, 0, cx);
|
||||
assert_single_caret_at_row(&editor, 1, cx);
|
||||
|
||||
let expected_first_highlighted_row = 2;
|
||||
let expected_first_highlighted_row = 3;
|
||||
cx.dispatch_action(menu::SelectNext);
|
||||
ensure_outline_view_contents(&outline_view, cx);
|
||||
assert_eq!(
|
||||
highlighted_display_rows(&editor, cx),
|
||||
vec![expected_first_highlighted_row, 3, 4, 5]
|
||||
vec![expected_first_highlighted_row, 4, 5, 6]
|
||||
);
|
||||
assert_single_caret_at_row(&editor, 0, cx);
|
||||
assert_single_caret_at_row(&editor, 1, cx);
|
||||
cx.dispatch_action(menu::Confirm);
|
||||
ensure_outline_view_contents(&outline_view, cx);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1075,45 +1075,36 @@ impl OutlinePanel {
|
||||
});
|
||||
} else {
|
||||
let mut offset = Point::default();
|
||||
let show_excerpt_controls = active_editor
|
||||
.read(cx)
|
||||
.display_map
|
||||
.read(cx)
|
||||
.show_excerpt_controls();
|
||||
let expand_excerpt_control_height = 1.0;
|
||||
if let Some(buffer_id) = scroll_to_buffer {
|
||||
let current_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx);
|
||||
if current_folded {
|
||||
if show_excerpt_controls {
|
||||
let previous_buffer_id = self
|
||||
.fs_entries
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|entry| match entry {
|
||||
FsEntry::File(file) => Some(file.buffer_id),
|
||||
FsEntry::ExternalFile(external_file) => {
|
||||
Some(external_file.buffer_id)
|
||||
}
|
||||
FsEntry::Directory(..) => None,
|
||||
})
|
||||
.skip_while(|id| *id != buffer_id)
|
||||
.nth(1);
|
||||
if let Some(previous_buffer_id) = previous_buffer_id {
|
||||
if !active_editor
|
||||
.read(cx)
|
||||
.is_buffer_folded(previous_buffer_id, cx)
|
||||
{
|
||||
offset.y += expand_excerpt_control_height;
|
||||
let previous_buffer_id = self
|
||||
.fs_entries
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|entry| match entry {
|
||||
FsEntry::File(file) => Some(file.buffer_id),
|
||||
FsEntry::ExternalFile(external_file) => {
|
||||
Some(external_file.buffer_id)
|
||||
}
|
||||
FsEntry::Directory(..) => None,
|
||||
})
|
||||
.skip_while(|id| *id != buffer_id)
|
||||
.nth(1);
|
||||
if let Some(previous_buffer_id) = previous_buffer_id {
|
||||
if !active_editor
|
||||
.read(cx)
|
||||
.is_buffer_folded(previous_buffer_id, cx)
|
||||
{
|
||||
offset.y += expand_excerpt_control_height;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if multi_buffer_snapshot.as_singleton().is_none() {
|
||||
offset.y = -(active_editor.read(cx).file_header_size() as f32);
|
||||
}
|
||||
if show_excerpt_controls {
|
||||
offset.y -= expand_excerpt_control_height;
|
||||
}
|
||||
offset.y -= expand_excerpt_control_height;
|
||||
}
|
||||
}
|
||||
active_editor.update(cx, |editor, cx| {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user