Compare commits
156 Commits
v0.177.9
...
streaming-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44501581ee | ||
|
|
ae95142cc8 | ||
|
|
b1b8d596b9 | ||
|
|
6c025507b6 | ||
|
|
8f4b7aa5db | ||
|
|
3345666557 | ||
|
|
40c62cda5f | ||
|
|
349a48d937 | ||
|
|
a88af7351a | ||
|
|
efaf358876 | ||
|
|
06a226dc32 | ||
|
|
1763dd714b | ||
|
|
330e799293 | ||
|
|
51c900366d | ||
|
|
be75f17429 | ||
|
|
f373383fc1 | ||
|
|
263d9ff755 | ||
|
|
829ecda370 | ||
|
|
af5af9d7c5 | ||
|
|
97c0a0a86e | ||
|
|
7e964290bf | ||
|
|
b8a8b9c699 | ||
|
|
d1cec209d4 | ||
|
|
aceab76ae4 | ||
|
|
9c054f207e | ||
|
|
219d36f589 | ||
|
|
6fd9708eee | ||
|
|
99216acdec | ||
|
|
aef25a3bc3 | ||
|
|
9b07f36199 | ||
|
|
ff25fa24e7 | ||
|
|
05df3d1bd6 | ||
|
|
84f4d2630f | ||
|
|
b42930f5be | ||
|
|
cb2eef6fb6 | ||
|
|
73668c2c4b | ||
|
|
07f555ca3b | ||
|
|
b0e2b57462 | ||
|
|
d404d7964e | ||
|
|
69af9bedaf | ||
|
|
2ebbcf15ea | ||
|
|
631cab5e40 | ||
|
|
ba39a47c52 | ||
|
|
c34357e2ab | ||
|
|
6fdb666bb7 | ||
|
|
7a9e0b37ed | ||
|
|
d44ba92363 | ||
|
|
ca4d8fc900 | ||
|
|
352882af77 | ||
|
|
c5c4a6201b | ||
|
|
6327a5d665 | ||
|
|
57438d30e8 | ||
|
|
5c81dd7d39 | ||
|
|
aec4d5cb26 | ||
|
|
4c0750bd2f | ||
|
|
22b1a02e23 | ||
|
|
314ad5dd5f | ||
|
|
d3c68650c0 | ||
|
|
43339c6869 | ||
|
|
e505d6bf5b | ||
|
|
0200dda83d | ||
|
|
4db9ab15a7 | ||
|
|
5daadc0d30 | ||
|
|
431727fdd7 | ||
|
|
cee98f872a | ||
|
|
e99d68a66f | ||
|
|
6a3e8044b1 | ||
|
|
c2375a4164 | ||
|
|
9d54e63a11 | ||
|
|
2a919ad1d0 | ||
|
|
f13b2fd811 | ||
|
|
7c39153160 | ||
|
|
d0c2bef8c3 | ||
|
|
87b3fefdd1 | ||
|
|
66784c0b3f | ||
|
|
ad9c508a72 | ||
|
|
aaa506c061 | ||
|
|
a602c50a6c | ||
|
|
728c161e8d | ||
|
|
3975d8ea93 | ||
|
|
2d050a8130 | ||
|
|
e600e71c1c | ||
|
|
82d85fd2ed | ||
|
|
e061ebb46c | ||
|
|
387ee46c46 | ||
|
|
89d89b8b2d | ||
|
|
f07ae541ad | ||
|
|
ff0bb1f389 | ||
|
|
9c7eee24bc | ||
|
|
ec4719146a | ||
|
|
47f8f891c8 | ||
|
|
d9d3b8847b | ||
|
|
d7b90f4204 | ||
|
|
3e64f38ba0 | ||
|
|
82338e2c47 | ||
|
|
229e853874 | ||
|
|
ed13e05855 | ||
|
|
674fb7621f | ||
|
|
fe18c73a07 | ||
|
|
befacfe8c9 | ||
|
|
54f0a729c2 | ||
|
|
67f9b2b87f | ||
|
|
a4ec0af681 | ||
|
|
886d8c1cab | ||
|
|
ebc5c213a2 | ||
|
|
0a2d938ac5 | ||
|
|
fc01f496a9 | ||
|
|
db28b9bbde | ||
|
|
0453cb2b06 | ||
|
|
85211889e5 | ||
|
|
ad94642e83 | ||
|
|
f4899d92a4 | ||
|
|
6685d85f49 | ||
|
|
161f8a1dd2 | ||
|
|
6cdd7b7390 | ||
|
|
0ec15d6b02 | ||
|
|
909de2ca6f | ||
|
|
f31749c81b | ||
|
|
76a81607de | ||
|
|
7d22059a2f | ||
|
|
6b16a5555e | ||
|
|
7ba2b258de | ||
|
|
cbb535f5eb | ||
|
|
20fc753f2b | ||
|
|
042fc82e99 | ||
|
|
27781a8a60 | ||
|
|
33af6bce55 | ||
|
|
563baf682e | ||
|
|
11b79d0ab9 | ||
|
|
8c4da9fba0 | ||
|
|
1f7fa80166 | ||
|
|
2ac952ee6b | ||
|
|
495612be2e | ||
|
|
1086b282b8 | ||
|
|
ffe2bed1e2 | ||
|
|
88940732ca | ||
|
|
74fc52d5ce | ||
|
|
2a7a4a80c6 | ||
|
|
c03bf1af36 | ||
|
|
922aaa0534 | ||
|
|
fc5ff318e3 | ||
|
|
e7b3b8bf03 | ||
|
|
6faa7cd722 | ||
|
|
0776fa8f31 | ||
|
|
7321c814ce | ||
|
|
ac3cb3df05 | ||
|
|
0bd40da546 | ||
|
|
3bec4eb117 | ||
|
|
bf6cc2697a | ||
|
|
dc3158c8ce | ||
|
|
9e2b7bc5dc | ||
|
|
b774a4b8d1 | ||
|
|
16ab8701a2 | ||
|
|
b2add8c803 | ||
|
|
6635462f7b | ||
|
|
81ff6f7a3c |
187
.github/workflows/ci.yml
vendored
187
.github/workflows/ci.yml
vendored
@@ -23,47 +23,9 @@ env:
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
job_spec:
|
||||
name: Decide which jobs to run
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
outputs:
|
||||
run_tests: ${{ steps.filter.outputs.run_tests }}
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
# 350 is arbitrary; ~10days of history on main (5secs); full history is ~25secs
|
||||
fetch-depth: ${{ github.ref == 'refs/heads/main' && 2 || 350 }}
|
||||
- name: Fetch git history and generate output filters
|
||||
id: filter
|
||||
run: |
|
||||
if [ -z "$GITHUB_BASE_REF" ]; then
|
||||
echo "Not in a PR context (i.e., push to main/stable/preview)"
|
||||
COMPARE_REV=$(git rev-parse HEAD~1)
|
||||
else
|
||||
echo "In a PR context comparing to pull_request.base.ref"
|
||||
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
|
||||
echo "run_tests=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "run_tests=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep '^Cargo.lock') ]]; then
|
||||
echo "run_license=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "run_license=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
migration_checks:
|
||||
name: Check Postgres and Protobuf migrations, mergability
|
||||
needs: [job_spec]
|
||||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
needs.job_spec.outputs.run_tests == 'true'
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
timeout-minutes: 60
|
||||
runs-on:
|
||||
- self-hosted
|
||||
@@ -107,7 +69,6 @@ jobs:
|
||||
style:
|
||||
timeout-minutes: 60
|
||||
name: Check formatting and spelling
|
||||
needs: [job_spec]
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- buildjet-8vcpu-ubuntu-2204
|
||||
@@ -115,21 +76,6 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Prettier Check on /docs
|
||||
working-directory: ./docs
|
||||
run: |
|
||||
pnpm dlx prettier@${PRETTIER_VERSION} . --check || {
|
||||
echo "To fix, run from the root of the zed repo:"
|
||||
echo " cd docs && pnpm dlx prettier@${PRETTIER_VERSION} . --write && cd .."
|
||||
false
|
||||
}
|
||||
env:
|
||||
PRETTIER_VERSION: 3.5.0
|
||||
|
||||
# To support writing comments that they will certainly be revisited.
|
||||
- name: Check for todo! and FIXME comments
|
||||
run: script/check-todos
|
||||
@@ -145,10 +91,7 @@ jobs:
|
||||
macos_tests:
|
||||
timeout-minutes: 60
|
||||
name: (macOS) Run Clippy and tests
|
||||
needs: [job_spec]
|
||||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
needs.job_spec.outputs.run_tests == 'true'
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
@@ -180,9 +123,7 @@ jobs:
|
||||
- name: Check licenses
|
||||
run: |
|
||||
script/check-licenses
|
||||
if [[ "${{ needs.job_spec.outputs.run_license }}" == "true" ]]; then
|
||||
script/generate-licenses /tmp/zed_licenses_output
|
||||
fi
|
||||
script/generate-licenses /tmp/zed_licenses_output
|
||||
|
||||
- name: Check for new vulnerable dependencies
|
||||
if: github.event_name == 'pull_request'
|
||||
@@ -213,10 +154,7 @@ jobs:
|
||||
linux_tests:
|
||||
timeout-minutes: 60
|
||||
name: (Linux) Run Clippy and tests
|
||||
needs: [job_spec]
|
||||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
needs.job_spec.outputs.run_tests == 'true'
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
steps:
|
||||
@@ -265,12 +203,9 @@ jobs:
|
||||
build_remote_server:
|
||||
timeout-minutes: 60
|
||||
name: (Linux) Build Remote Server
|
||||
needs: [job_spec]
|
||||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
needs.job_spec.outputs.run_tests == 'true'
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- buildjet-8vcpu-ubuntu-2204
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
@@ -304,12 +239,21 @@ jobs:
|
||||
windows_clippy:
|
||||
timeout-minutes: 60
|
||||
name: (Windows) Run Clippy
|
||||
needs: [job_spec]
|
||||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
needs.job_spec.outputs.run_tests == 'true'
|
||||
runs-on: windows-2025-16
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: hosted-windows-2
|
||||
steps:
|
||||
# Temporarily Collect some metadata about the hardware behind our runners.
|
||||
- name: GHA Runner Info
|
||||
run: |
|
||||
Invoke-RestMethod -Headers @{"Metadata"="true"} -Method GET -Uri "http://169.254.169.254/metadata/instance/compute?api-version=2023-07-01" |
|
||||
ConvertTo-Json -Depth 10 |
|
||||
jq "{ vm_size: .vmSize, location: .location, os_disk_gb: (.storageProfile.osDisk.diskSizeGB | tonumber), rs_disk_gb: (.storageProfile.resourceDisk.size | tonumber / 1024) }"
|
||||
@{
|
||||
Cores = (Get-CimInstance Win32_Processor).NumberOfCores
|
||||
vCPUs = (Get-CimInstance Win32_Processor).NumberOfLogicalProcessors
|
||||
RamGb = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)
|
||||
cpuid = (Get-CimInstance Win32_Processor).Name.Trim()
|
||||
} | ConvertTo-Json
|
||||
# more info here:- https://github.com/rust-lang/cargo/issues/13020
|
||||
- name: Enable longer pathnames for git
|
||||
run: git config --system core.longpaths true
|
||||
@@ -362,13 +306,21 @@ jobs:
|
||||
windows_tests:
|
||||
timeout-minutes: 60
|
||||
name: (Windows) Run Tests
|
||||
needs: [job_spec]
|
||||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
needs.job_spec.outputs.run_tests == 'true'
|
||||
# Use bigger runners for PRs (speed); smaller for async (cost)
|
||||
runs-on: ${{ github.event_name == 'pull_request' && 'windows-2025-32' || 'windows-2025-16' }}
|
||||
if: ${{ github.repository_owner == 'zed-industries' && (github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'windows')) }}
|
||||
runs-on: hosted-windows-2
|
||||
steps:
|
||||
# Temporarily Collect some metadata about the hardware behind our runners.
|
||||
- name: GHA Runner Info
|
||||
run: |
|
||||
Invoke-RestMethod -Headers @{"Metadata"="true"} -Method GET -Uri "http://169.254.169.254/metadata/instance/compute?api-version=2023-07-01" |
|
||||
ConvertTo-Json -Depth 10 |
|
||||
jq "{ vm_size: .vmSize, location: .location, os_disk_gb: (.storageProfile.osDisk.diskSizeGB | tonumber), rs_disk_gb: (.storageProfile.resourceDisk.size | tonumber / 1024) }"
|
||||
@{
|
||||
Cores = (Get-CimInstance Win32_Processor).NumberOfCores
|
||||
vCPUs = (Get-CimInstance Win32_Processor).NumberOfLogicalProcessors
|
||||
RamGb = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)
|
||||
cpuid = (Get-CimInstance Win32_Processor).Name.Trim()
|
||||
} | ConvertTo-Json
|
||||
# more info here:- https://github.com/rust-lang/cargo/issues/13020
|
||||
- name: Enable longer pathnames for git
|
||||
run: git config --system core.longpaths true
|
||||
@@ -420,50 +372,13 @@ jobs:
|
||||
Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force
|
||||
}
|
||||
|
||||
tests_pass:
|
||||
name: Tests Pass
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- job_spec
|
||||
- style
|
||||
- migration_checks
|
||||
- linux_tests
|
||||
- build_remote_server
|
||||
- macos_tests
|
||||
- windows_clippy
|
||||
- windows_tests
|
||||
if: always()
|
||||
steps:
|
||||
- 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
|
||||
name: Create a macOS bundle
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
needs: [macos_tests]
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
@@ -553,9 +468,7 @@ jobs:
|
||||
name: Linux x86_x64 release bundle
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2004
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
@@ -572,7 +485,7 @@ jobs:
|
||||
run: ./script/linux && ./script/install-mold 2.34.0
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
|
||||
script/determine-release-channel
|
||||
@@ -582,18 +495,14 @@ jobs:
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
if: |
|
||||
github.ref == 'refs/heads/main'
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
|
||||
path: target/release/zed-*.tar.gz
|
||||
|
||||
- name: Upload Linux remote server to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
if: |
|
||||
github.ref == 'refs/heads/main'
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.gz
|
||||
path: target/zed-remote-server-linux-x86_64.gz
|
||||
@@ -614,9 +523,7 @@ jobs:
|
||||
name: Linux arm64 release bundle
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2204-arm
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
@@ -633,7 +540,7 @@ jobs:
|
||||
run: ./script/linux
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
|
||||
script/determine-release-channel
|
||||
@@ -643,18 +550,14 @@ jobs:
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
if: |
|
||||
github.ref == 'refs/heads/main'
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz
|
||||
path: target/release/zed-*.tar.gz
|
||||
|
||||
- name: Upload Linux remote server to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
|
||||
if: |
|
||||
github.ref == 'refs/heads/main'
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.gz
|
||||
path: target/zed-remote-server-linux-aarch64.gz
|
||||
@@ -672,9 +575,7 @@ jobs:
|
||||
|
||||
auto-release-preview:
|
||||
name: Auto release preview
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
&& endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') }}
|
||||
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64]
|
||||
runs-on:
|
||||
- self-hosted
|
||||
|
||||
39
.github/workflows/docs.yml
vendored
Normal file
39
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Docs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "docs/**"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check_formatting:
|
||||
name: "Check formatting"
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Prettier Check on /docs
|
||||
working-directory: ./docs
|
||||
run: |
|
||||
pnpm dlx prettier@${PRETTIER_VERSION} . --check || {
|
||||
echo "To fix, run from the root of the zed repo:"
|
||||
echo " cd docs && pnpm dlx prettier@${PRETTIER_VERSION} . --write && cd .."
|
||||
false
|
||||
}
|
||||
env:
|
||||
PRETTIER_VERSION: 3.5.0
|
||||
|
||||
- name: Check for Typos with Typos-CLI
|
||||
uses: crate-ci/typos@8e6a4285bcbde632c5d79900a7779746e8b7ea3f # v1.24.6
|
||||
with:
|
||||
config: ./typos.toml
|
||||
files: ./docs/
|
||||
1710
Cargo.lock
generated
1710
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@@ -118,6 +118,7 @@ members = [
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/schema_generator",
|
||||
"crates/scripting_tool",
|
||||
"crates/search",
|
||||
"crates/semantic_index",
|
||||
"crates/semantic_version",
|
||||
@@ -168,15 +169,10 @@ members = [
|
||||
# Extensions
|
||||
#
|
||||
|
||||
"extensions/csharp",
|
||||
"extensions/deno",
|
||||
"extensions/elixir",
|
||||
"extensions/emmet",
|
||||
"extensions/erlang",
|
||||
"extensions/glsl",
|
||||
"extensions/haskell",
|
||||
"extensions/html",
|
||||
"extensions/lua",
|
||||
"extensions/perplexity",
|
||||
"extensions/proto",
|
||||
"extensions/purescript",
|
||||
@@ -323,6 +319,7 @@ reqwest_client = { path = "crates/reqwest_client" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
scripting_tool = { path = "crates/scripting_tool" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_index = { path = "crates/semantic_index" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
@@ -455,6 +452,7 @@ livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "
|
||||
], default-features = false }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
mlua = { version = "0.10", features = ["lua54", "vendored"] }
|
||||
nanoid = "0.4"
|
||||
nbformat = { version = "0.10.0" }
|
||||
nix = "0.29"
|
||||
@@ -603,9 +601,11 @@ features = [
|
||||
version = "0.58"
|
||||
features = [
|
||||
"implement",
|
||||
"Foundation_Collections",
|
||||
"Foundation_Numerics",
|
||||
"Storage",
|
||||
"System_Threading",
|
||||
"UI_StartScreen",
|
||||
"UI_ViewManagement",
|
||||
"Wdk_System_SystemServices",
|
||||
"Win32_Globalization",
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<svg width="400" height="120" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="tilePattern" width="124" height="24" patternUnits="userSpaceOnUse">
|
||||
<svg width="124" height="24" viewBox="0 0 124 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.2">
|
||||
<path d="M16.666 12.0013L11.9993 16.668L7.33268 12.0013" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 7.33464L12 16.668" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29 8.33464C29.3682 8.33464 29.6667 8.03616 29.6667 7.66797C29.6667 7.29978 29.3682 7.0013 29 7.0013C28.6318 7.0013 28.3333 7.29978 28.3333 7.66797C28.3333 8.03616 28.6318 8.33464 29 8.33464ZM29 9.66797C30.1046 9.66797 31 8.77254 31 7.66797C31 6.5634 30.1046 5.66797 29 5.66797C27.8954 5.66797 27 6.5634 27 7.66797C27 8.77254 27.8954 9.66797 29 9.66797Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M35 8.33464C35.3682 8.33464 35.6667 8.03616 35.6667 7.66797C35.6667 7.29978 35.3682 7.0013 35 7.0013C34.6318 7.0013 34.3333 7.29978 34.3333 7.66797C34.3333 8.03616 34.6318 8.33464 35 8.33464ZM35 9.66797C36.1046 9.66797 37 8.77254 37 7.66797C37 6.5634 36.1046 5.66797 35 5.66797C33.8954 5.66797 33 6.5634 33 7.66797C33 8.77254 33.8954 9.66797 35 9.66797Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29 16.9987C29.3682 16.9987 29.6667 16.7002 29.6667 16.332C29.6667 15.9638 29.3682 15.6654 29 15.6654C28.6318 15.6654 28.3333 15.9638 28.3333 16.332C28.3333 16.7002 28.6318 16.9987 29 16.9987ZM29 18.332C30.1046 18.332 31 17.4366 31 16.332C31 15.2275 30.1046 14.332 29 14.332C27.8954 14.332 27 15.2275 27 16.332C27 17.4366 27.8954 18.332 29 18.332Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.334 9H29.6673V11.4615C30.2383 11.1443 31.0005 11 32.0007 11H33.6675C34.0356 11 34.334 10.7017 34.334 10.3333V9H35.6673V10.3333C35.6673 11.4378 34.7723 12.3333 33.6675 12.3333H32.0007C30.8614 12.3333 30.3692 12.5484 30.1298 12.7549C29.9016 12.9516 29.7857 13.2347 29.6673 13.742V15H28.334V9Z" fill="white"/>
|
||||
<path d="M48.668 8.66406H55.3346V15.3307" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M48.668 15.3307L55.3346 8.66406" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M76.5871 9.40624C76.8514 9.14195 77 8.78346 77 8.40965C77 8.03583 76.8516 7.67731 76.5873 7.41295C76.323 7.14859 75.9645 7.00005 75.5907 7C75.2169 6.99995 74.8584 7.14841 74.594 7.4127L67.921 14.0874C67.8049 14.2031 67.719 14.3456 67.671 14.5024L67.0105 16.6784C66.9975 16.7217 66.9966 16.7676 67.0076 16.8113C67.0187 16.8551 67.0414 16.895 67.0734 16.9269C67.1053 16.9588 67.1453 16.9815 67.1891 16.9925C67.2328 17.0035 67.2788 17.0024 67.322 16.9894L69.4985 16.3294C69.6551 16.2818 69.7976 16.1964 69.9135 16.0809L76.5871 9.40624Z" stroke="white" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M74 8L76 10" stroke="white" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M70.3877 7.53516V6.53516" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M73.5693 16.6992V17.6992" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M66.3877 10.5352H67.3877" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M77.5693 13.6992H76.5693" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M68.3877 8.53516L67.3877 7.53516" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M75.5693 15.6992L76.5693 16.6992" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M87.334 11.9987L92.0007 7.33203L96.6673 11.9987" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M92 16.6654V7.33203" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M117 12C117 10.6739 116.473 9.40215 115.536 8.46447C114.598 7.52678 113.326 7 112 7C110.602 7.00526 109.261 7.55068 108.256 8.52222L107 9.77778" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M107 7V9.77778H109.778" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M107 12C107 13.3261 107.527 14.5979 108.464 15.5355C109.402 16.4732 110.674 17 112 17C113.398 16.9947 114.739 16.4493 115.744 15.4778L117 14.2222" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M114.223 14.2188H117V16.9965" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
</pattern>
|
||||
<linearGradient id="fade" y2="1" x2="0">
|
||||
<stop offset="0" stop-color="white" stop-opacity=".52"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<mask id="fadeMask" maskContentUnits="objectBoundingBox">
|
||||
<rect width="1" height="1" fill="url(#fade)"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#tilePattern)" mask="url(#fadeMask)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB |
@@ -1,12 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2131_1193)">
|
||||
<circle cx="7" cy="7" r="6" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M6 10H7M8 10H7M7 10V7.1C7 7.04477 6.95523 7 6.9 7H6" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<circle cx="7" cy="4.5" r="1" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2131_1193">
|
||||
<rect width="14" height="14" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M7 11H8M8 11H9M8 11V8.1C8 8.04477 7.95523 8 7.9 8H7" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M8 6.5C8.55228 6.5 9 6.05228 9 5.5C9 4.94772 8.55228 4.5 8 4.5C7.44772 4.5 7 4.94772 7 5.5C7 6.05228 7.44772 6.5 8 6.5Z" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 479 B After Width: | Height: | Size: 524 B |
4
assets/icons/zed_predict_error.svg
Normal file
4
assets/icons/zed_predict_error.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.6" d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M14 8L10 12M14 12L10 8" stroke="black" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 296 B |
@@ -393,7 +393,6 @@
|
||||
"alt-shift-open": "projects::OpenRemote",
|
||||
"alt-ctrl-shift-o": "projects::OpenRemote",
|
||||
"alt-ctrl-shift-b": "branches::OpenRecent",
|
||||
"alt-shift-enter": "toast::RunAction",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"save": "workspace::Save",
|
||||
"ctrl-s": "workspace::Save",
|
||||
@@ -613,12 +612,29 @@
|
||||
"ctrl-alt-e": "assistant2::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel2 && prompt_editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "assistant2::NewPromptEditor",
|
||||
"cmd-alt-t": "assistant2::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"bindings": {
|
||||
"enter": "assistant2::Chat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextStrip",
|
||||
"bindings": {
|
||||
@@ -717,48 +733,28 @@
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-y": "git::StageFile",
|
||||
"alt-shift-y": "git::UnstageFile",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"space": "git::ToggleStaged",
|
||||
"ctrl-space": "git::StageAll",
|
||||
"ctrl-shift-space": "git::UnstageAll",
|
||||
"tab": "git_panel::FocusEditor",
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"alt-enter": "menu::SecondaryConfirm",
|
||||
"backspace": "git::RestoreFile"
|
||||
"alt-enter": "menu::SecondaryConfirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"alt-l": "git::GenerateCommitMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-g ctrl-g": "git::Fetch",
|
||||
"ctrl-g up": "git::Push",
|
||||
"ctrl-g down": "git::Pull",
|
||||
"ctrl-g shift-up": "git::ForcePush",
|
||||
"ctrl-g d": "git::Diff",
|
||||
"ctrl-g backspace": "git::RestoreTrackedFiles",
|
||||
"ctrl-g shift-backspace": "git::TrashUntrackedFiles",
|
||||
"ctrl-space": "git::StageAll",
|
||||
"ctrl-shift-space": "git::UnstageAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitDiff > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "git::Commit",
|
||||
"ctrl-space": "git::StageAll",
|
||||
"ctrl-shift-space": "git::UnstageAll"
|
||||
"ctrl-enter": "git::Commit"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -773,7 +769,6 @@
|
||||
"escape": "git_panel::FocusChanges",
|
||||
"tab": "git_panel::FocusChanges",
|
||||
"shift-tab": "git_panel::FocusChanges",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"alt-up": "git_panel::FocusChanges",
|
||||
"alt-l": "git::GenerateCommitMessage"
|
||||
|
||||
@@ -31,13 +31,13 @@
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"cmd-enter": "menu::SecondaryConfirm",
|
||||
"cmd-escape": "menu::Cancel",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"escape": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
"cmd-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"cmd-escape": "menu::Cancel",
|
||||
"cmd-o": "workspace::Open",
|
||||
"cmd-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||
"cmd-+": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||
@@ -108,8 +108,8 @@
|
||||
"cmd-right": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
"cmd-up": "editor::MoveToBeginning",
|
||||
"cmd-down": "editor::MoveToEnd",
|
||||
"cmd-up": "editor::MoveToStartOfExcerpt",
|
||||
"cmd-down": "editor::MoveToEndOfExcerpt",
|
||||
"cmd-home": "editor::MoveToBeginning", // Typed via `cmd-fn-left`
|
||||
"cmd-end": "editor::MoveToEnd", // Typed via `cmd-fn-right`
|
||||
"shift-up": "editor::SelectUp",
|
||||
@@ -124,8 +124,8 @@
|
||||
"alt-shift-right": "editor::SelectToNextWordEnd", // cursorWordRightSelect
|
||||
"ctrl-shift-up": "editor::SelectToStartOfParagraph",
|
||||
"ctrl-shift-down": "editor::SelectToEndOfParagraph",
|
||||
"cmd-shift-up": "editor::SelectToBeginning",
|
||||
"cmd-shift-down": "editor::SelectToEnd",
|
||||
"cmd-shift-up": "editor::SelectToStartOfExcerpt",
|
||||
"cmd-shift-down": "editor::SelectToEndOfExcerpt",
|
||||
"cmd-a": "editor::SelectAll",
|
||||
"cmd-l": "editor::SelectLine",
|
||||
"cmd-shift-i": "editor::Format",
|
||||
@@ -172,16 +172,6 @@
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && multibuffer",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-up": "editor::MoveToStartOfExcerpt",
|
||||
"cmd-down": "editor::MoveToStartOfNextExcerpt",
|
||||
"cmd-shift-up": "editor::SelectToStartOfExcerpt",
|
||||
"cmd-shift-down": "editor::SelectToStartOfNextExcerpt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && edit_prediction",
|
||||
"use_key_equivalents": true,
|
||||
@@ -266,6 +256,14 @@
|
||||
"cmd-alt-e": "assistant2::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel2 && prompt_editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "assistant2::NewPromptEditor",
|
||||
"cmd-alt-t": "assistant2::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
@@ -273,6 +271,15 @@
|
||||
"enter": "assistant2::Chat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextStrip",
|
||||
"use_key_equivalents": true,
|
||||
@@ -497,7 +504,6 @@
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd-k s": "workspace::SaveWithoutFormat",
|
||||
"alt-shift-enter": "toast::RunAction",
|
||||
"cmd-shift-s": "workspace::SaveAs",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
@@ -747,25 +753,28 @@
|
||||
"cmd-up": "menu::SelectFirst",
|
||||
"cmd-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"cmd-alt-y": "git::ToggleStaged",
|
||||
"space": "git::ToggleStaged",
|
||||
"cmd-y": "git::StageFile",
|
||||
"cmd-shift-y": "git::UnstageFile",
|
||||
"cmd-shift-space": "git::StageAll",
|
||||
"ctrl-shift-space": "git::UnstageAll",
|
||||
"alt-down": "git_panel::FocusEditor",
|
||||
"tab": "git_panel::FocusEditor",
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"cmd-enter": "git::Commit",
|
||||
"backspace": "git::RestoreFile"
|
||||
"cmd-enter": "git::Commit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitDiff > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "git::Commit",
|
||||
"cmd-ctrl-y": "git::StageAll",
|
||||
"cmd-ctrl-shift-y": "git::UnstageAll"
|
||||
"cmd-enter": "git::Commit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AskPass > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -781,27 +790,11 @@
|
||||
"alt-tab": "git::GenerateCommitMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-g ctrl-g": "git::Fetch",
|
||||
"ctrl-g up": "git::Push",
|
||||
"ctrl-g down": "git::Pull",
|
||||
"ctrl-g shift-up": "git::ForcePush",
|
||||
"ctrl-g d": "git::Diff",
|
||||
"ctrl-g backspace": "git::RestoreTrackedFiles",
|
||||
"ctrl-g shift-backspace": "git::TrashUntrackedFiles",
|
||||
"cmd-ctrl-y": "git::StageAll",
|
||||
"cmd-ctrl-shift-y": "git::UnstageAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"escape": "menu::Cancel",
|
||||
"cmd-enter": "git::Commit",
|
||||
"alt-tab": "git::GenerateCommitMessage"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"a": ["vim::PushObject", { "around": true }],
|
||||
"left": "vim::Left",
|
||||
"h": "vim::Left",
|
||||
"backspace": "vim::Backspace",
|
||||
"backspace": "vim::WrappingLeft",
|
||||
"down": "vim::Down",
|
||||
"ctrl-j": "vim::Down",
|
||||
"j": "vim::Down",
|
||||
@@ -20,7 +20,7 @@
|
||||
"k": "vim::Up",
|
||||
"right": "vim::Right",
|
||||
"l": "vim::Right",
|
||||
"space": "vim::Space",
|
||||
"space": "vim::WrappingRight",
|
||||
"end": "vim::EndOfLine",
|
||||
"$": "vim::EndOfLine",
|
||||
"^": "vim::FirstNonWhitespace",
|
||||
@@ -247,7 +247,8 @@
|
||||
"context": "VimControl && VimCount",
|
||||
"bindings": {
|
||||
"0": ["vim::Number", 0],
|
||||
":": "vim::CountCommand"
|
||||
":": "vim::CountCommand",
|
||||
"%": "vim::GoToPercentage"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -555,12 +555,6 @@
|
||||
//
|
||||
// Default: icon
|
||||
"status_style": "icon",
|
||||
// What branch name to use if init.defaultBranch
|
||||
// is not set
|
||||
//
|
||||
// Default: main
|
||||
"fallback_branch_name": "main",
|
||||
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the git panel.
|
||||
//
|
||||
@@ -726,8 +720,8 @@
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
// Whether to start a new line with a comment when a previous line is a comment as well.
|
||||
"extend_comment_on_newline": true,
|
||||
// Whether or not to ensure there's a single newline at the end of a buffer
|
||||
// when saving it.
|
||||
// Removes any lines containing only whitespace at the end of the file and
|
||||
// ensures just one newline at the end.
|
||||
"ensure_final_newline_on_save": true,
|
||||
// Whether or not to perform a buffer format before saving
|
||||
//
|
||||
@@ -843,7 +837,15 @@
|
||||
//
|
||||
// The minimum column number to show the inline blame information at
|
||||
// "min_column": 0
|
||||
}
|
||||
},
|
||||
// How git hunks are displayed visually in the editor.
|
||||
// This setting can take two values:
|
||||
//
|
||||
// 1. Show unstaged hunks with a transparent background (default):
|
||||
// "hunk_style": "transparent"
|
||||
// 2. Show unstaged hunks with a pattern background:
|
||||
// "hunk_style": "pattern"
|
||||
"hunk_style": "transparent"
|
||||
},
|
||||
// Configuration for how direnv configuration should be loaded. May take 2 values:
|
||||
// 1. Load direnv configuration using `direnv export json` directly.
|
||||
@@ -1173,6 +1175,7 @@
|
||||
"format_on_save": "off",
|
||||
"use_on_type_format": false,
|
||||
"allow_rewrap": "anywhere",
|
||||
"soft_wrap": "bounded",
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -1296,6 +1299,11 @@
|
||||
// "semi": false,
|
||||
// "singleQuote": true
|
||||
},
|
||||
// Settings for auto-closing of JSX tags.
|
||||
"jsx_tag_auto_close": {
|
||||
// // Whether to auto-close JSX tags.
|
||||
// "enabled": true
|
||||
},
|
||||
// LSP Specific settings.
|
||||
"lsp": {
|
||||
// Specify the LSP name as a key here.
|
||||
|
||||
@@ -6,7 +6,15 @@
|
||||
{
|
||||
"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",
|
||||
@@ -97,9 +105,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",
|
||||
@@ -386,7 +394,15 @@
|
||||
{
|
||||
"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",
|
||||
@@ -477,9 +493,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",
|
||||
@@ -766,7 +782,15 @@
|
||||
{
|
||||
"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",
|
||||
@@ -857,9 +881,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",
|
||||
@@ -1146,7 +1170,15 @@
|
||||
{
|
||||
"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",
|
||||
@@ -1237,9 +1269,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",
|
||||
@@ -1526,7 +1558,15 @@
|
||||
{
|
||||
"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",
|
||||
@@ -1617,9 +1657,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",
|
||||
@@ -1906,7 +1946,15 @@
|
||||
{
|
||||
"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",
|
||||
@@ -1997,9 +2045,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",
|
||||
|
||||
@@ -96,9 +96,9 @@
|
||||
"terminal.ansi.bright_white": "#dce0e5ff",
|
||||
"terminal.ansi.dim_white": "#575d65ff",
|
||||
"link_text.hover": "#74ade8ff",
|
||||
"version_control.added": "#27a657ff",
|
||||
"version_control.modified": "#d3b020ff",
|
||||
"version_control.deleted": "#e06c76ff",
|
||||
"version_control_added": "#a7c088ff",
|
||||
"version_control_modified": "#dec184ff",
|
||||
"version_control_deleted": "#d07277ff",
|
||||
"conflict": "#dec184ff",
|
||||
"conflict.background": "#dec1841a",
|
||||
"conflict.border": "#5d4c2fff",
|
||||
@@ -475,9 +475,9 @@
|
||||
"terminal.ansi.bright_white": "#242529ff",
|
||||
"terminal.ansi.dim_white": "#97979aff",
|
||||
"link_text.hover": "#5c78e2ff",
|
||||
"version_control.added": "#27a657ff",
|
||||
"version_control.modified": "#d3b020ff",
|
||||
"version_control.deleted": "#e06c76ff",
|
||||
"version_control_added": "#669f59ff",
|
||||
"version_control_modified": "#a48819ff",
|
||||
"version_control_deleted": "#d36151ff",
|
||||
"conflict": "#a48819ff",
|
||||
"conflict.background": "#faf2e6ff",
|
||||
"conflict.border": "#f4e7d1ff",
|
||||
|
||||
@@ -9,10 +9,7 @@ use gpui::{
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId};
|
||||
use lsp::LanguageServerName;
|
||||
use project::{
|
||||
EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
|
||||
ProjectEnvironmentEvent, WorktreeId,
|
||||
};
|
||||
use project::{EnvironmentErrorMessage, LanguageServerProgress, Project, WorktreeId};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
|
||||
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
@@ -76,22 +73,7 @@ impl ActivityIndicator {
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe(
|
||||
&project.read(cx).lsp_store(),
|
||||
|_, _, event, cx| match event {
|
||||
LspStoreEvent::LanguageServerUpdate { .. } => cx.notify(),
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
cx.subscribe(
|
||||
&project.read(cx).environment().clone(),
|
||||
|_, _, event, cx| match event {
|
||||
ProjectEnvironmentEvent::ErrorsUpdated => cx.notify(),
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
||||
|
||||
if let Some(auto_updater) = auto_updater.as_ref() {
|
||||
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
|
||||
@@ -222,7 +204,7 @@ impl ActivityIndicator {
|
||||
message: error.0.clone(),
|
||||
on_click: Some(Arc::new(move |this, window, cx| {
|
||||
this.project.update(cx, |project, cx| {
|
||||
project.remove_environment_error(worktree_id, cx);
|
||||
project.remove_environment_error(cx, worktree_id);
|
||||
});
|
||||
window.dispatch_action(Box::new(workspace::OpenLog), cx);
|
||||
})),
|
||||
|
||||
@@ -110,7 +110,7 @@ impl ConfigurationView {
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.when(configuration_view.is_none(), |this| {
|
||||
this.child(div().child(Label::new(format!(
|
||||
"No configuration view for {}",
|
||||
|
||||
@@ -38,7 +38,7 @@ use language_model::{
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::{CodeAction, ProjectTransaction};
|
||||
use project::{ActionVariant, CodeAction, ProjectTransaction};
|
||||
use prompt_store::PromptBuilder;
|
||||
use rope::Rope;
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
@@ -3569,10 +3569,10 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
Task::ready(Ok(vec![CodeAction {
|
||||
server_id: language::LanguageServerId(0),
|
||||
range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end),
|
||||
lsp_action: lsp::CodeAction {
|
||||
lsp_action: ActionVariant::Action(Box::new(lsp::CodeAction {
|
||||
title: "Fix with Assistant".into(),
|
||||
..Default::default()
|
||||
},
|
||||
})),
|
||||
}]))
|
||||
} else {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
|
||||
@@ -2,17 +2,19 @@ use std::sync::Arc;
|
||||
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
list, AbsoluteLength, AnyElement, App, DefiniteLength, EdgesRefinement, Empty, Entity, Length,
|
||||
ListAlignment, ListOffset, ListState, StyleRefinement, Subscription, TextStyleRefinement,
|
||||
UnderlineStyle, WeakEntity,
|
||||
list, AbsoluteLength, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
|
||||
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, StyleRefinement, Subscription,
|
||||
Task, TextStyleRefinement, UnderlineStyle, WeakEntity,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use settings::Settings as _;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, Disclosure};
|
||||
use ui::{prelude::*, Disclosure, KeyBinding};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
|
||||
@@ -26,14 +28,20 @@ pub struct ActiveThread {
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
thread: Entity<Thread>,
|
||||
save_thread_task: Option<Task<()>>,
|
||||
messages: Vec<MessageId>,
|
||||
list_state: ListState,
|
||||
rendered_messages_by_id: HashMap<MessageId, Entity<Markdown>>,
|
||||
editing_message: Option<(MessageId, EditMessageState)>,
|
||||
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
|
||||
last_error: Option<ThreadError>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
struct EditMessageState {
|
||||
editor: Entity<Editor>,
|
||||
}
|
||||
|
||||
impl ActiveThread {
|
||||
pub fn new(
|
||||
thread: Entity<Thread>,
|
||||
@@ -55,16 +63,18 @@ impl ActiveThread {
|
||||
tools,
|
||||
thread_store,
|
||||
thread: thread.clone(),
|
||||
save_thread_task: None,
|
||||
messages: Vec::new(),
|
||||
rendered_messages_by_id: HashMap::default(),
|
||||
expanded_tool_uses: HashMap::default(),
|
||||
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, _: &mut Window, cx: &mut App| {
|
||||
this.update(cx, |this, cx| this.render_message(ix, cx))
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
this.update(cx, |this, cx| this.render_message(ix, window, cx))
|
||||
.unwrap()
|
||||
}
|
||||
}),
|
||||
editing_message: None,
|
||||
last_error: None,
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
@@ -117,6 +127,44 @@ impl ActiveThread {
|
||||
self.messages.push(*id);
|
||||
self.list_state.splice(old_len..old_len, 1);
|
||||
|
||||
let markdown = self.render_markdown(text.into(), window, cx);
|
||||
self.rendered_messages_by_id.insert(*id, markdown);
|
||||
self.list_state.scroll_to(ListOffset {
|
||||
item_ix: old_len,
|
||||
offset_in_item: Pixels(0.0),
|
||||
});
|
||||
}
|
||||
|
||||
fn edited_message(
|
||||
&mut self,
|
||||
id: &MessageId,
|
||||
text: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
|
||||
return;
|
||||
};
|
||||
self.list_state.splice(index..index + 1, 1);
|
||||
let markdown = self.render_markdown(text.into(), window, cx);
|
||||
self.rendered_messages_by_id.insert(*id, markdown);
|
||||
}
|
||||
|
||||
fn deleted_message(&mut self, id: &MessageId) {
|
||||
let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
|
||||
return;
|
||||
};
|
||||
self.messages.remove(index);
|
||||
self.list_state.splice(index..index + 1, 0);
|
||||
self.rendered_messages_by_id.remove(id);
|
||||
}
|
||||
|
||||
fn render_markdown(
|
||||
&self,
|
||||
text: SharedString,
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<Markdown> {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let colors = cx.theme().colors();
|
||||
let ui_font_size = TextSize::Default.rems(cx);
|
||||
@@ -134,6 +182,8 @@ impl ActiveThread {
|
||||
base_text_style: text_style,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: cx.theme().players().local().selection,
|
||||
code_block_overflow_x_scroll: true,
|
||||
table_overflow_x_scroll: true,
|
||||
code_block: StyleRefinement {
|
||||
margin: EdgesRefinement {
|
||||
top: Some(Length::Definite(rems(0.).into())),
|
||||
@@ -180,20 +230,15 @@ impl ActiveThread {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let markdown = cx.new(|cx| {
|
||||
cx.new(|cx| {
|
||||
Markdown::new(
|
||||
text.into(),
|
||||
text,
|
||||
markdown_style,
|
||||
Some(self.language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.rendered_messages_by_id.insert(*id, markdown);
|
||||
self.list_state.scroll_to(ListOffset {
|
||||
item_ix: old_len,
|
||||
offset_in_item: Pixels(0.0),
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
@@ -208,11 +253,7 @@ impl ActiveThread {
|
||||
self.last_error = Some(error.clone());
|
||||
}
|
||||
ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
|
||||
self.thread_store
|
||||
.update(cx, |thread_store, cx| {
|
||||
thread_store.save_thread(&self.thread, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
self.save_thread(cx);
|
||||
}
|
||||
ThreadEvent::StreamedAssistantText(message_id, text) => {
|
||||
if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
|
||||
@@ -231,12 +272,25 @@ impl ActiveThread {
|
||||
self.push_message(message_id, message_text, window, cx);
|
||||
}
|
||||
|
||||
self.thread_store
|
||||
.update(cx, |thread_store, cx| {
|
||||
thread_store.save_thread(&self.thread, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
self.save_thread(cx);
|
||||
cx.notify();
|
||||
}
|
||||
ThreadEvent::MessageEdited(message_id) => {
|
||||
if let Some(message_text) = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.message(*message_id)
|
||||
.map(|message| message.text.clone())
|
||||
{
|
||||
self.edited_message(message_id, message_text, window, cx);
|
||||
}
|
||||
|
||||
self.save_thread(cx);
|
||||
cx.notify();
|
||||
}
|
||||
ThreadEvent::MessageDeleted(message_id) => {
|
||||
self.deleted_message(message_id);
|
||||
self.save_thread(cx);
|
||||
cx.notify();
|
||||
}
|
||||
ThreadEvent::UsePendingTools => {
|
||||
@@ -287,7 +341,133 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_message(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
/// Spawns a task to save the active thread.
|
||||
///
|
||||
/// Only one task to save the thread will be in flight at a time.
|
||||
fn save_thread(&mut self, cx: &mut Context<Self>) {
|
||||
let thread = self.thread.clone();
|
||||
self.save_thread_task = Some(cx.spawn(|this, mut cx| async move {
|
||||
let task = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.thread_store
|
||||
.update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
|
||||
})
|
||||
.ok();
|
||||
|
||||
if let Some(task) = task {
|
||||
task.await.log_err();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
fn start_editing_message(
|
||||
&mut self,
|
||||
message_id: MessageId,
|
||||
message_text: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let buffer = cx.new(|cx| {
|
||||
MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
|
||||
});
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
editor::EditorMode::AutoHeight { max_lines: 8 },
|
||||
buffer,
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.focus_handle(cx).focus(window);
|
||||
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
||||
editor
|
||||
});
|
||||
self.editing_message = Some((
|
||||
message_id,
|
||||
EditMessageState {
|
||||
editor: editor.clone(),
|
||||
},
|
||||
));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editing_message.take();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm_editing_message(
|
||||
&mut self,
|
||||
_: &menu::Confirm,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some((message_id, state)) = self.editing_message.take() else {
|
||||
return;
|
||||
};
|
||||
let edited_text = state.editor.read(cx).text(cx);
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.edit_message(message_id, Role::User, edited_text, cx);
|
||||
for message_id in self.messages_after(message_id) {
|
||||
thread.delete_message(*message_id, cx);
|
||||
}
|
||||
});
|
||||
|
||||
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
if provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| provider.must_accept_terms(cx))
|
||||
{
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let Some(model) = model_registry.active_model() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.send_to_model(model, RequestKind::Chat, false, cx)
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn last_user_message(&self, cx: &Context<Self>) -> Option<MessageId> {
|
||||
self.messages
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|message_id| {
|
||||
self.thread
|
||||
.read(cx)
|
||||
.message(**message_id)
|
||||
.map_or(false, |message| message.role == Role::User)
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
|
||||
self.messages
|
||||
.iter()
|
||||
.position(|id| *id == message_id)
|
||||
.map(|index| &self.messages[index + 1..])
|
||||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.cancel_editing_message(&menu::Cancel, window, cx);
|
||||
}
|
||||
|
||||
fn handle_regenerate_click(
|
||||
&mut self,
|
||||
_: &ClickEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.confirm_editing_message(&menu::Confirm, window, cx);
|
||||
}
|
||||
|
||||
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let message_id = self.messages[ix];
|
||||
let Some(message) = self.thread.read(cx).message(message_id) else {
|
||||
return Empty.into_any();
|
||||
@@ -306,8 +486,28 @@ impl ActiveThread {
|
||||
return Empty.into_any();
|
||||
}
|
||||
|
||||
let allow_editing_message =
|
||||
message.role == Role::User && self.last_user_message(cx) == Some(message_id);
|
||||
|
||||
let edit_message_editor = self
|
||||
.editing_message
|
||||
.as_ref()
|
||||
.filter(|(id, _)| *id == message_id)
|
||||
.map(|(_, state)| state.editor.clone());
|
||||
|
||||
let message_content = v_flex()
|
||||
.child(div().p_2p5().text_ui(cx).child(markdown.clone()))
|
||||
.child(
|
||||
if let Some(edit_message_editor) = edit_message_editor.clone() {
|
||||
div()
|
||||
.key_context("EditMessageEditor")
|
||||
.on_action(cx.listener(Self::cancel_editing_message))
|
||||
.on_action(cx.listener(Self::confirm_editing_message))
|
||||
.p_2p5()
|
||||
.child(edit_message_editor)
|
||||
} else {
|
||||
div().p_2p5().text_ui(cx).child(markdown.clone())
|
||||
},
|
||||
)
|
||||
.when_some(context, |parent, context| {
|
||||
if !context.is_empty() {
|
||||
parent.child(
|
||||
@@ -337,7 +537,8 @@ impl ActiveThread {
|
||||
.child(
|
||||
h_flex()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.bg(colors.editor_foreground.opacity(0.05))
|
||||
.border_b_1()
|
||||
.border_color(colors.border)
|
||||
@@ -356,6 +557,71 @@ impl ActiveThread {
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.when_some(
|
||||
edit_message_editor.clone(),
|
||||
|this, edit_message_editor| {
|
||||
let focus_handle = edit_message_editor.focus_handle(cx);
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("cancel-edit-message", "Cancel")
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(
|
||||
cx.listener(Self::handle_cancel_click),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new(
|
||||
"confirm-edit-message",
|
||||
"Regenerate",
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Confirm,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(
|
||||
cx.listener(Self::handle_regenerate_click),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.when(
|
||||
edit_message_editor.is_none() && allow_editing_message,
|
||||
|this| {
|
||||
this.child(
|
||||
Button::new("edit-message", "Edit")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener({
|
||||
let message_text = message.text.clone();
|
||||
move |this, _, window, cx| {
|
||||
this.start_editing_message(
|
||||
message_id,
|
||||
message_text.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(message_content),
|
||||
@@ -379,7 +645,7 @@ impl ActiveThread {
|
||||
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
|
||||
v_flex()
|
||||
.bg(colors.editor_background)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.child(message_content),
|
||||
),
|
||||
};
|
||||
@@ -408,7 +674,7 @@ impl ActiveThread {
|
||||
.pr_2()
|
||||
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
|
||||
.when(is_open, |element| element.border_b_1().rounded_t(px(6.)))
|
||||
.when(!is_open, |element| element.rounded(px(6.)))
|
||||
.when(!is_open, |element| element.rounded_md())
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
h_flex()
|
||||
|
||||
@@ -134,7 +134,7 @@ impl AssistantConfiguration {
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.map(|parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
None => parent.child(div().child(Label::new(format!(
|
||||
|
||||
@@ -15,7 +15,8 @@ use editor::Editor;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
prelude::*, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, FontWeight, Pixels, Subscription, Task, UpdateGlobal, WeakEntity,
|
||||
FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task, UpdateGlobal,
|
||||
WeakEntity,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
|
||||
@@ -993,12 +994,21 @@ impl AssistantPanel {
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn key_context(&self) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("AssistantPanel2");
|
||||
if matches!(self.active_view, ActiveView::PromptEditor) {
|
||||
key_context.add("prompt_editor");
|
||||
}
|
||||
key_context
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantPanel {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("AssistantPanel2")
|
||||
.key_context(self.key_context())
|
||||
.justify_between()
|
||||
.size_full()
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
@@ -1013,12 +1023,7 @@ impl Render for AssistantPanel {
|
||||
.map(|parent| match self.active_view {
|
||||
ActiveView::Thread => parent
|
||||
.child(self.render_active_thread_or_empty_state(window, cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(self.message_editor.clone()),
|
||||
)
|
||||
.child(h_flex().child(self.message_editor.clone()))
|
||||
.children(self.render_last_error(cx)),
|
||||
ActiveView::History => parent.child(self.history.clone()),
|
||||
ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
|
||||
|
||||
@@ -167,8 +167,8 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
Some("Enter the URL that you would like to fetch".into())
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
|
||||
"Enter the URL that you would like to fetch".into()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
|
||||
@@ -208,12 +208,18 @@ impl ContextStore {
|
||||
let mut text_tasks = Vec::new();
|
||||
this.update(&mut cx, |_, cx| {
|
||||
for (path, buffer_entity) in files.into_iter().zip(buffers) {
|
||||
let buffer_entity = buffer_entity?;
|
||||
let buffer = buffer_entity.read(cx);
|
||||
let (buffer_info, text_task) =
|
||||
collect_buffer_info_and_text(path, buffer_entity, buffer, cx.to_async());
|
||||
buffer_infos.push(buffer_info);
|
||||
text_tasks.push(text_task);
|
||||
// Skip all binary files and other non-UTF8 files
|
||||
if let Ok(buffer_entity) = buffer_entity {
|
||||
let buffer = buffer_entity.read(cx);
|
||||
let (buffer_info, text_task) = collect_buffer_info_and_text(
|
||||
path,
|
||||
buffer_entity,
|
||||
buffer,
|
||||
cx.to_async(),
|
||||
);
|
||||
buffer_infos.push(buffer_info);
|
||||
text_tasks.push(text_task);
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
@@ -27,6 +27,7 @@ use language::{Buffer, Point, Selection, TransactionId};
|
||||
use language_model::{report_assistant_event, LanguageModelRegistry};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::ActionVariant;
|
||||
use project::{CodeAction, ProjectTransaction};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -1727,10 +1728,10 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
Task::ready(Ok(vec![CodeAction {
|
||||
server_id: language::LanguageServerId(0),
|
||||
range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end),
|
||||
lsp_action: lsp::CodeAction {
|
||||
lsp_action: ActionVariant::Action(Box::new(lsp::CodeAction {
|
||||
title: "Fix with Assistant".into(),
|
||||
..Default::default()
|
||||
},
|
||||
})),
|
||||
}]))
|
||||
} else {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
|
||||
@@ -4,8 +4,8 @@ use editor::actions::MoveUp;
|
||||
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
pulsating_between, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription,
|
||||
TextStyle, WeakEntity,
|
||||
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
|
||||
WeakEntity,
|
||||
};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
@@ -16,7 +16,7 @@ use text::Bias;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Switch,
|
||||
TintColor, Tooltip,
|
||||
Tooltip,
|
||||
};
|
||||
use vim_mode_setting::VimModeSetting;
|
||||
use workspace::Workspace;
|
||||
@@ -298,166 +298,210 @@ impl Render for MessageEditor {
|
||||
let linux = platform == PlatformStyle::Linux;
|
||||
let windows = platform == PlatformStyle::Windows;
|
||||
let button_width = if linux || windows || vim_mode_enabled {
|
||||
px(92.)
|
||||
px(82.)
|
||||
} else {
|
||||
px(64.)
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.key_context("MessageEditor")
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}))
|
||||
.on_action(cx.listener(Self::toggle_context_picker))
|
||||
.on_action(cx.listener(Self::remove_all_context))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
.on_action(cx.listener(Self::toggle_chat_mode))
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.bg(bg_color)
|
||||
.child(self.context_strip.clone())
|
||||
.when(is_streaming_completion, |parent| {
|
||||
let focus_handle = self.editor.focus_handle(cx).clone();
|
||||
parent.child(
|
||||
h_flex().py_3().w_full().justify_center().child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.py_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_lg()
|
||||
.shadow_md()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(gpui::Transformation::rotate(
|
||||
gpui::percentage(delta),
|
||||
))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Generating…")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(ui::Divider::vertical())
|
||||
.child(
|
||||
Button::new("cancel-generation", "Cancel")
|
||||
.label_size(LabelSize::XSmall)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&editor::actions::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.))),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(
|
||||
&editor::actions::Cancel,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_5()
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: line_height.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: bg_color,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.key_context("MessageEditor")
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}))
|
||||
.on_action(cx.listener(Self::toggle_context_picker))
|
||||
.on_action(cx.listener(Self::remove_all_context))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
.on_action(cx.listener(Self::toggle_chat_mode))
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.bg(bg_color)
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(self.context_strip.clone())
|
||||
.child(
|
||||
PopoverMenu::new("inline-context-picker")
|
||||
.menu(move |window, cx| {
|
||||
inline_context_picker.update(cx, |this, cx| {
|
||||
this.init(window, cx);
|
||||
});
|
||||
v_flex()
|
||||
.gap_5()
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: line_height.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Some(inline_context_picker.clone())
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: bg_color,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.attach(gpui::Corner::TopLeft)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2) - px(4.0),
|
||||
})
|
||||
.with_handle(self.inline_context_picker_menu_handle.clone()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(
|
||||
Switch::new("use-tools", self.use_tools.into())
|
||||
.label("Tools")
|
||||
.on_click(cx.listener(|this, selection, _window, _cx| {
|
||||
this.use_tools = match selection {
|
||||
ToggleState::Selected => true,
|
||||
ToggleState::Unselected
|
||||
| ToggleState::Indeterminate => false,
|
||||
};
|
||||
}))
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ChatMode,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
PopoverMenu::new("inline-context-picker")
|
||||
.menu(move |window, cx| {
|
||||
inline_context_picker.update(cx, |this, cx| {
|
||||
this.init(window, cx);
|
||||
});
|
||||
|
||||
Some(inline_context_picker.clone())
|
||||
})
|
||||
.attach(gpui::Corner::TopLeft)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2)
|
||||
- px(4.0),
|
||||
})
|
||||
.with_handle(self.inline_context_picker_menu_handle.clone()),
|
||||
)
|
||||
.child(h_flex().gap_1().child(self.model_selector.clone()).child(
|
||||
if is_streaming_completion {
|
||||
ButtonLike::new("cancel-generation")
|
||||
.width(button_width.into())
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new("Cancel")
|
||||
.size(LabelSize::Small)
|
||||
.with_animation(
|
||||
"pulsating-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(
|
||||
0.4, 0.8,
|
||||
)),
|
||||
|label, delta| label.alpha(delta),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&editor::actions::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(
|
||||
&editor::actions::Cancel,
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(
|
||||
Switch::new("use-tools", self.use_tools.into())
|
||||
.label("Tools")
|
||||
.on_click(cx.listener(
|
||||
|this, selection, _window, _cx| {
|
||||
this.use_tools = match selection {
|
||||
ToggleState::Selected => true,
|
||||
ToggleState::Unselected
|
||||
| ToggleState::Indeterminate => false,
|
||||
};
|
||||
},
|
||||
))
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ChatMode,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
} else {
|
||||
ButtonLike::new("submit-message")
|
||||
.width(button_width.into())
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(is_editor_empty || !is_model_selected)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new("Submit")
|
||||
.size(LabelSize::Small)
|
||||
.color(submit_label_color),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex().gap_1().child(self.model_selector.clone()).child(
|
||||
ButtonLike::new("submit-message")
|
||||
.width(button_width.into())
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(
|
||||
is_editor_empty
|
||||
|| !is_model_selected
|
||||
|| is_streaming_completion,
|
||||
)
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&Chat,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(&Chat, window, cx);
|
||||
})
|
||||
.when(is_editor_empty, |button| {
|
||||
button
|
||||
.tooltip(Tooltip::text("Type a message to submit"))
|
||||
})
|
||||
.when(!is_model_selected, |button| {
|
||||
button.tooltip(Tooltip::text(
|
||||
"Select a model to continue",
|
||||
))
|
||||
})
|
||||
},
|
||||
)),
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new("Submit")
|
||||
.size(LabelSize::Small)
|
||||
.color(submit_label_color),
|
||||
)
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&Chat,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| {
|
||||
binding
|
||||
.when(vim_mode_enabled, |kb| {
|
||||
kb.size(rems_from_px(12.))
|
||||
})
|
||||
.into_any_element()
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(&Chat, window, cx);
|
||||
})
|
||||
.when(is_editor_empty, |button| {
|
||||
button.tooltip(Tooltip::text(
|
||||
"Type a message to submit",
|
||||
))
|
||||
})
|
||||
.when(is_streaming_completion, |button| {
|
||||
button.tooltip(Tooltip::text(
|
||||
"Cancel to submit a new message",
|
||||
))
|
||||
})
|
||||
.when(!is_model_selected, |button| {
|
||||
button.tooltip(Tooltip::text(
|
||||
"Select a model to continue",
|
||||
))
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@ use futures::StreamExt as _;
|
||||
use gpui::{App, Context, EventEmitter, SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolUseId,
|
||||
MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError, Role, StopReason,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
|
||||
Role, StopReason,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::{post_inc, TryFutureExt as _};
|
||||
@@ -88,7 +89,7 @@ impl Thread {
|
||||
completion_count: 0,
|
||||
pending_completions: Vec::new(),
|
||||
tools,
|
||||
tool_use: ToolUseState::default(),
|
||||
tool_use: ToolUseState::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +99,14 @@ impl Thread {
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let next_message_id = MessageId(saved.messages.len());
|
||||
let next_message_id = MessageId(
|
||||
saved
|
||||
.messages
|
||||
.last()
|
||||
.map(|message| message.id.0 + 1)
|
||||
.unwrap_or(0),
|
||||
);
|
||||
let tool_use = ToolUseState::from_saved_messages(&saved.messages);
|
||||
|
||||
Self {
|
||||
id,
|
||||
@@ -120,7 +128,7 @@ impl Thread {
|
||||
completion_count: 0,
|
||||
pending_completions: Vec::new(),
|
||||
tools,
|
||||
tool_use: ToolUseState::default(),
|
||||
tool_use,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +197,10 @@ impl Thread {
|
||||
self.tool_use.tool_uses_for_message(id)
|
||||
}
|
||||
|
||||
pub fn tool_results_for_message(&self, id: MessageId) -> Vec<&LanguageModelToolResult> {
|
||||
self.tool_use.tool_results_for_message(id)
|
||||
}
|
||||
|
||||
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
|
||||
self.tool_use.message_has_tool_results(message_id)
|
||||
}
|
||||
@@ -223,6 +235,34 @@ impl Thread {
|
||||
id
|
||||
}
|
||||
|
||||
pub fn edit_message(
|
||||
&mut self,
|
||||
id: MessageId,
|
||||
new_role: Role,
|
||||
new_text: String,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
|
||||
return false;
|
||||
};
|
||||
message.role = new_role;
|
||||
message.text = new_text;
|
||||
self.touch_updated_at();
|
||||
cx.emit(ThreadEvent::MessageEdited(id));
|
||||
true
|
||||
}
|
||||
|
||||
pub fn delete_message(&mut self, id: MessageId, cx: &mut Context<Self>) -> bool {
|
||||
let Some(index) = self.messages.iter().position(|message| message.id == id) else {
|
||||
return false;
|
||||
};
|
||||
self.messages.remove(index);
|
||||
self.context_by_message.remove(&id);
|
||||
self.touch_updated_at();
|
||||
cx.emit(ThreadEvent::MessageDeleted(id));
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns the representation of this [`Thread`] in a textual form.
|
||||
///
|
||||
/// This is the representation we use when attaching a thread as context to another thread.
|
||||
@@ -561,6 +601,8 @@ pub enum ThreadEvent {
|
||||
StreamedCompletion,
|
||||
StreamedAssistantText(MessageId, String),
|
||||
MessageAdded(MessageId),
|
||||
MessageEdited(MessageId),
|
||||
MessageDeleted(MessageId),
|
||||
SummaryChanged,
|
||||
UsePendingTools,
|
||||
ToolFinished {
|
||||
|
||||
@@ -12,9 +12,9 @@ use futures::FutureExt as _;
|
||||
use gpui::{
|
||||
prelude::*, App, BackgroundExecutor, Context, Entity, Global, ReadGlobal, SharedString, Task,
|
||||
};
|
||||
use heed::types::SerdeBincode;
|
||||
use heed::types::{SerdeBincode, SerdeJson};
|
||||
use heed::Database;
|
||||
use language_model::Role;
|
||||
use language_model::{LanguageModelToolUseId, Role};
|
||||
use project::Project;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::ResultExt as _;
|
||||
@@ -113,6 +113,24 @@ impl ThreadStore {
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
text: message.text.clone(),
|
||||
tool_uses: thread
|
||||
.tool_uses_for_message(message.id)
|
||||
.into_iter()
|
||||
.map(|tool_use| SavedToolUse {
|
||||
id: tool_use.id,
|
||||
name: tool_use.name,
|
||||
input: tool_use.input,
|
||||
})
|
||||
.collect(),
|
||||
tool_results: thread
|
||||
.tool_results_for_message(message.id)
|
||||
.into_iter()
|
||||
.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(),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
@@ -239,11 +257,29 @@ pub struct SavedThread {
|
||||
pub messages: Vec<SavedMessage>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
pub id: MessageId,
|
||||
pub role: Role,
|
||||
pub text: String,
|
||||
#[serde(default)]
|
||||
pub tool_uses: Vec<SavedToolUse>,
|
||||
#[serde(default)]
|
||||
pub tool_results: Vec<SavedToolResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SavedToolUse {
|
||||
pub id: LanguageModelToolUseId,
|
||||
pub name: SharedString,
|
||||
pub input: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SavedToolResult {
|
||||
pub tool_use_id: LanguageModelToolUseId,
|
||||
pub is_error: bool,
|
||||
pub content: Arc<str>,
|
||||
}
|
||||
|
||||
struct GlobalThreadsDatabase(
|
||||
@@ -255,7 +291,7 @@ impl Global for GlobalThreadsDatabase {}
|
||||
pub(crate) struct ThreadsDatabase {
|
||||
executor: BackgroundExecutor,
|
||||
env: heed::Env,
|
||||
threads: Database<SerdeBincode<ThreadId>, SerdeBincode<SavedThread>>,
|
||||
threads: Database<SerdeBincode<ThreadId>, SerdeJson<SavedThread>>,
|
||||
}
|
||||
|
||||
impl ThreadsDatabase {
|
||||
@@ -270,7 +306,7 @@ impl ThreadsDatabase {
|
||||
let database_future = executor
|
||||
.spawn({
|
||||
let executor = executor.clone();
|
||||
let database_path = paths::support_dir().join("threads/threads-db.0.mdb");
|
||||
let database_path = paths::support_dir().join("threads/threads-db.1.mdb");
|
||||
async move { ThreadsDatabase::new(database_path, executor) }
|
||||
})
|
||||
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
|
||||
|
||||
@@ -7,10 +7,11 @@ use futures::FutureExt as _;
|
||||
use gpui::{SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
|
||||
LanguageModelToolUseId, MessageContent,
|
||||
LanguageModelToolUseId, MessageContent, Role,
|
||||
};
|
||||
|
||||
use crate::thread::MessageId;
|
||||
use crate::thread_store::SavedMessage;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ToolUse {
|
||||
@@ -28,7 +29,6 @@ pub enum ToolUseStatus {
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ToolUseState {
|
||||
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
|
||||
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
|
||||
@@ -37,6 +37,65 @@ pub struct ToolUseState {
|
||||
}
|
||||
|
||||
impl ToolUseState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tool_uses_by_assistant_message: HashMap::default(),
|
||||
tool_uses_by_user_message: HashMap::default(),
|
||||
tool_results: HashMap::default(),
|
||||
pending_tool_uses_by_id: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_saved_messages(messages: &[SavedMessage]) -> Self {
|
||||
let mut this = Self::new();
|
||||
|
||||
for message in messages {
|
||||
match message.role {
|
||||
Role::Assistant => {
|
||||
if !message.tool_uses.is_empty() {
|
||||
this.tool_uses_by_assistant_message.insert(
|
||||
message.id,
|
||||
message
|
||||
.tool_uses
|
||||
.iter()
|
||||
.map(|tool_use| LanguageModelToolUse {
|
||||
id: tool_use.id.clone(),
|
||||
name: tool_use.name.clone().into(),
|
||||
input: tool_use.input.clone(),
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Role::User => {
|
||||
if !message.tool_results.is_empty() {
|
||||
let tool_uses_by_user_message = this
|
||||
.tool_uses_by_user_message
|
||||
.entry(message.id)
|
||||
.or_default();
|
||||
|
||||
for tool_result in &message.tool_results {
|
||||
let tool_use_id = tool_result.tool_use_id.clone();
|
||||
|
||||
tool_uses_by_user_message.push(tool_use_id.clone());
|
||||
this.tool_results.insert(
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
tool_use_id,
|
||||
is_error: tool_result.is_error,
|
||||
content: tool_result.content.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Role::System => {}
|
||||
}
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
|
||||
self.pending_tool_uses_by_id.values().collect()
|
||||
}
|
||||
@@ -84,6 +143,17 @@ impl ToolUseState {
|
||||
tool_uses
|
||||
}
|
||||
|
||||
pub fn tool_results_for_message(&self, message_id: MessageId) -> Vec<&LanguageModelToolResult> {
|
||||
let empty = Vec::new();
|
||||
|
||||
self.tool_uses_by_user_message
|
||||
.get(&message_id)
|
||||
.unwrap_or(&empty)
|
||||
.iter()
|
||||
.filter_map(|tool_use_id| self.tool_results.get(&tool_use_id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
|
||||
self.tool_uses_by_user_message
|
||||
.get(&message_id)
|
||||
|
||||
@@ -103,7 +103,7 @@ impl RenderOnce for ContextPill {
|
||||
.pl_1()
|
||||
.pb(px(1.))
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.gap_1()
|
||||
.child(self.icon().size(IconSize::XSmall).color(Color::Muted));
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ use ui::{
|
||||
Tooltip,
|
||||
};
|
||||
use util::{maybe, ResultExt};
|
||||
use workspace::searchable::SearchableItemHandle;
|
||||
use workspace::searchable::{Direction, SearchableItemHandle};
|
||||
use workspace::{
|
||||
item::{self, FollowableItem, Item, ItemHandle},
|
||||
notifications::NotificationId,
|
||||
@@ -1241,7 +1241,7 @@ impl ContextEditor {
|
||||
.child("Press")
|
||||
.child(
|
||||
h_flex()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.px_1()
|
||||
.mr_0p5()
|
||||
.border_1()
|
||||
@@ -2092,7 +2092,7 @@ impl ContextEditor {
|
||||
.ml(gutter_width)
|
||||
.pb_1()
|
||||
.w(max_width - gutter_width)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.border_1()
|
||||
.border_color(theme.colors().border_variant)
|
||||
.overflow_hidden()
|
||||
@@ -3106,12 +3106,13 @@ impl SearchableItem for ContextEditor {
|
||||
|
||||
fn active_match_index(
|
||||
&mut self,
|
||||
direction: Direction,
|
||||
matches: &[Self::Match],
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<usize> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.active_match_index(matches, window, cx)
|
||||
editor.active_match_index(direction, matches, window, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3421,7 +3422,7 @@ fn invoked_slash_command_fold_placeholder(
|
||||
.ml_6()
|
||||
.gap_2()
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.child(Label::new(format!("/{}", command.name.clone())))
|
||||
.map(|parent| match &command.status {
|
||||
InvokedSlashCommandStatus::Running(_) => {
|
||||
|
||||
@@ -104,53 +104,49 @@ impl ContextStore {
|
||||
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
|
||||
|
||||
let this =
|
||||
cx.new(|cx: &mut Context<Self>| {
|
||||
let context_server_factory_registry =
|
||||
ContextServerFactoryRegistry::default_global(cx);
|
||||
let context_server_manager = cx.new(|cx| {
|
||||
ContextServerManager::new(
|
||||
context_server_factory_registry,
|
||||
project.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let mut this = Self {
|
||||
contexts: Vec::new(),
|
||||
contexts_metadata: Vec::new(),
|
||||
context_server_manager,
|
||||
context_server_slash_command_ids: HashMap::default(),
|
||||
host_contexts: Vec::new(),
|
||||
fs,
|
||||
languages,
|
||||
slash_commands,
|
||||
telemetry,
|
||||
_watch_updates: cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
let this = cx.new(|cx: &mut Context<Self>| {
|
||||
let context_server_factory_registry =
|
||||
ContextServerFactoryRegistry::default_global(cx);
|
||||
let context_server_manager = cx.new(|cx| {
|
||||
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
|
||||
});
|
||||
let mut this = Self {
|
||||
contexts: Vec::new(),
|
||||
contexts_metadata: Vec::new(),
|
||||
context_server_manager,
|
||||
context_server_slash_command_ids: HashMap::default(),
|
||||
host_contexts: Vec::new(),
|
||||
fs,
|
||||
languages,
|
||||
slash_commands,
|
||||
telemetry,
|
||||
_watch_updates: cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
.log_err()
|
||||
}),
|
||||
client_subscription: None,
|
||||
_project_subscriptions: vec![
|
||||
cx.subscribe(&project, Self::handle_project_event)
|
||||
],
|
||||
project_is_shared: false,
|
||||
client: project.read(cx).client(),
|
||||
project: project.clone(),
|
||||
prompt_builder,
|
||||
};
|
||||
this.handle_project_shared(project.clone(), cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
this
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
}),
|
||||
client_subscription: None,
|
||||
_project_subscriptions: vec![
|
||||
cx.observe(&project, Self::handle_project_changed),
|
||||
cx.subscribe(&project, Self::handle_project_event),
|
||||
],
|
||||
project_is_shared: false,
|
||||
client: project.read(cx).client(),
|
||||
project: project.clone(),
|
||||
prompt_builder,
|
||||
};
|
||||
this.handle_project_changed(project.clone(), cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
this
|
||||
})?;
|
||||
|
||||
Ok(this)
|
||||
})
|
||||
@@ -292,7 +288,7 @@ impl ContextStore {
|
||||
})?
|
||||
}
|
||||
|
||||
fn handle_project_shared(&mut self, _: Entity<Project>, cx: &mut Context<Self>) {
|
||||
fn handle_project_changed(&mut self, _: Entity<Project>, cx: &mut Context<Self>) {
|
||||
let is_shared = self.project.read(cx).is_shared();
|
||||
let was_shared = mem::replace(&mut self.project_is_shared, is_shared);
|
||||
if is_shared == was_shared {
|
||||
@@ -322,14 +318,11 @@ impl ContextStore {
|
||||
|
||||
fn handle_project_event(
|
||||
&mut self,
|
||||
project: Entity<Project>,
|
||||
_: Entity<Project>,
|
||||
event: &project::Event,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
project::Event::RemoteIdChanged(_) => {
|
||||
self.handle_project_shared(project, cx);
|
||||
}
|
||||
project::Event::Reshared => {
|
||||
self.advertise_contexts(cx);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
gpui.workspace = true
|
||||
project.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
mod list_worktrees_tool;
|
||||
mod now_tool;
|
||||
mod read_file_tool;
|
||||
|
||||
use assistant_tool::ToolRegistry;
|
||||
use gpui::App;
|
||||
|
||||
use crate::list_worktrees_tool::ListWorktreesTool;
|
||||
use crate::now_tool::NowTool;
|
||||
use crate::read_file_tool::ReadFileTool;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
assistant_tool::init(cx);
|
||||
|
||||
let registry = ToolRegistry::global(cx);
|
||||
registry.register_tool(NowTool);
|
||||
registry.register_tool(ListWorktreesTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
}
|
||||
|
||||
84
crates/assistant_tools/src/list_worktrees_tool.rs
Normal file
84
crates/assistant_tools/src/list_worktrees_tool.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Task, WeakEntity, Window};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ListWorktreesToolInput {}
|
||||
|
||||
pub struct ListWorktreesTool;
|
||||
|
||||
impl Tool for ListWorktreesTool {
|
||||
fn name(&self) -> String {
|
||||
"list-worktrees".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Lists all worktrees in the current project. Use this tool when you need to find available worktrees and their IDs.".into()
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_input: serde_json::Value,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace dropped")));
|
||||
};
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
cx.update(|cx| {
|
||||
#[derive(Debug, Serialize)]
|
||||
struct WorktreeInfo {
|
||||
id: usize,
|
||||
root_name: String,
|
||||
root_dir: Option<String>,
|
||||
}
|
||||
|
||||
let worktrees = project.update(cx, |project, cx| {
|
||||
project
|
||||
.visible_worktrees(cx)
|
||||
.map(|worktree| {
|
||||
worktree.read_with(cx, |worktree, _cx| WorktreeInfo {
|
||||
id: worktree.id().to_usize(),
|
||||
root_dir: worktree
|
||||
.root_dir()
|
||||
.map(|root_dir| root_dir.to_string_lossy().to_string()),
|
||||
root_name: worktree.root_name().to_string(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
if worktrees.is_empty() {
|
||||
return Ok("No worktrees found in the current project.".to_string());
|
||||
}
|
||||
|
||||
let mut result = String::from("Worktrees in the current project:\n\n");
|
||||
for worktree in worktrees {
|
||||
result.push_str(&serde_json::to_string(&worktree)?);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
})?
|
||||
})
|
||||
}
|
||||
}
|
||||
69
crates/assistant_tools/src/read_file_tool.rs
Normal file
69
crates/assistant_tools/src/read_file_tool.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Task, WeakEntity, Window};
|
||||
use project::{ProjectPath, WorktreeId};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ReadFileToolInput {
|
||||
/// The ID of the worktree in which the file resides.
|
||||
pub worktree_id: usize,
|
||||
/// The path to the file to read.
|
||||
///
|
||||
/// This path is relative to the worktree root, it must not be an absolute path.
|
||||
pub path: Arc<Path>,
|
||||
}
|
||||
|
||||
pub struct ReadFileTool;
|
||||
|
||||
impl Tool for ReadFileTool {
|
||||
fn name(&self) -> String {
|
||||
"read-file".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Reads the content of a file specified by a worktree ID and path. Use this tool when you need to access the contents of a file in the project.".into()
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(ReadFileToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace dropped")));
|
||||
};
|
||||
|
||||
let input = match serde_json::from_value::<ReadFileToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: WorktreeId::from_usize(input.worktree_id),
|
||||
path: input.path,
|
||||
};
|
||||
cx.spawn(|cx| async move {
|
||||
let buffer = cx
|
||||
.update(|cx| {
|
||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||
})?
|
||||
.await?;
|
||||
|
||||
cx.update(|cx| buffer.read(cx).text())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ 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
|
||||
@@ -32,6 +31,7 @@ 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;
|
||||
use sum_tree::{SumTree, TreeMap};
|
||||
use text::ToOffset as _;
|
||||
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: SumTree<PendingHunk>,
|
||||
pending_hunks: TreeMap<usize, PendingHunk>,
|
||||
base_text: language::BufferSnapshot,
|
||||
base_text_exists: bool,
|
||||
}
|
||||
@@ -48,7 +48,7 @@ pub enum DiffHunkStatusKind {
|
||||
pub enum DiffHunkSecondaryStatus {
|
||||
HasSecondaryHunk,
|
||||
OverlapsWithSecondaryHunk,
|
||||
NoSecondaryHunk,
|
||||
None,
|
||||
SecondaryHunkAdditionPending,
|
||||
SecondaryHunkRemovalPending,
|
||||
}
|
||||
@@ -74,8 +74,6 @@ 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,
|
||||
}
|
||||
@@ -95,16 +93,6 @@ 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;
|
||||
|
||||
@@ -188,7 +176,6 @@ impl BufferDiffSnapshot {
|
||||
}
|
||||
|
||||
impl BufferDiffInner {
|
||||
/// Returns the new index text and new pending hunks.
|
||||
fn stage_or_unstage_hunks(
|
||||
&mut self,
|
||||
unstaged_diff: &Self,
|
||||
@@ -196,7 +183,7 @@ impl BufferDiffInner {
|
||||
hunks: &[DiffHunk],
|
||||
buffer: &text::BufferSnapshot,
|
||||
file_exists: bool,
|
||||
) -> (Option<Rope>, SumTree<PendingHunk>) {
|
||||
) -> (Option<Rope>, Vec<(usize, PendingHunk)>) {
|
||||
let head_text = self
|
||||
.base_text_exists
|
||||
.then(|| self.base_text.as_rope().clone());
|
||||
@@ -208,41 +195,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),
|
||||
(index_text, head_text) => {
|
||||
let (rope, new_status) = if stage {
|
||||
(_, head_text @ _) => {
|
||||
if stage {
|
||||
log::debug!("stage all");
|
||||
(
|
||||
return (
|
||||
file_exists.then(|| buffer.as_rope().clone()),
|
||||
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
|
||||
)
|
||||
vec![(
|
||||
0,
|
||||
PendingHunk {
|
||||
buffer_version: buffer.version().clone(),
|
||||
new_status: DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
|
||||
},
|
||||
)],
|
||||
);
|
||||
} else {
|
||||
log::debug!("unstage all");
|
||||
(
|
||||
return (
|
||||
head_text,
|
||||
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);
|
||||
vec![(
|
||||
0,
|
||||
PendingHunk {
|
||||
buffer_version: buffer.version().clone(),
|
||||
new_status: DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
|
||||
},
|
||||
)],
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
|
||||
unstaged_hunk_cursor.next(buffer);
|
||||
|
||||
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
|
||||
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;
|
||||
for DiffHunk {
|
||||
buffer_range,
|
||||
diff_base_byte_range,
|
||||
@@ -250,58 +237,12 @@ impl BufferDiffInner {
|
||||
..
|
||||
} in hunks.iter().cloned()
|
||||
{
|
||||
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)
|
||||
if (stage && secondary_status == DiffHunkSecondaryStatus::None)
|
||||
|| (!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() {
|
||||
@@ -353,15 +294,22 @@ 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);
|
||||
@@ -406,14 +354,12 @@ impl BufferDiffInner {
|
||||
});
|
||||
|
||||
let mut secondary_cursor = None;
|
||||
let mut pending_hunks_cursor = None;
|
||||
let mut pending_hunks = TreeMap::default();
|
||||
if let Some(secondary) = secondary.as_ref() {
|
||||
let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
|
||||
cursor.next(buffer);
|
||||
secondary_cursor = Some(cursor);
|
||||
let mut cursor = secondary.pending_hunks.cursor::<DiffHunkSummary>(buffer);
|
||||
cursor.next(buffer);
|
||||
pending_hunks_cursor = Some(cursor);
|
||||
pending_hunks = secondary.pending_hunks.clone();
|
||||
}
|
||||
|
||||
let max_point = buffer.max_point();
|
||||
@@ -432,33 +378,16 @@ impl BufferDiffInner {
|
||||
end_anchor = buffer.anchor_before(end_point);
|
||||
}
|
||||
|
||||
let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
|
||||
let mut secondary_status = DiffHunkSecondaryStatus::None;
|
||||
|
||||
let mut has_pending = false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,7 +449,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::NoSecondaryHunk,
|
||||
secondary_status: DiffHunkSecondaryStatus::None,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -795,7 +724,7 @@ impl BufferDiff {
|
||||
base_text,
|
||||
hunks,
|
||||
base_text_exists,
|
||||
pending_hunks: SumTree::new(&buffer),
|
||||
pending_hunks: TreeMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -811,8 +740,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,
|
||||
}
|
||||
})
|
||||
@@ -822,7 +751,7 @@ impl BufferDiff {
|
||||
BufferDiffInner {
|
||||
base_text: language::Buffer::build_empty_snapshot(cx),
|
||||
hunks: SumTree::new(buffer),
|
||||
pending_hunks: SumTree::new(buffer),
|
||||
pending_hunks: TreeMap::default(),
|
||||
base_text_exists: false,
|
||||
}
|
||||
}
|
||||
@@ -838,7 +767,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 = SumTree::from_summary(DiffHunkSummary::default());
|
||||
diff.inner.pending_hunks.clear();
|
||||
});
|
||||
cx.emit(BufferDiffEvent::DiffChanged {
|
||||
changed_range: Some(Anchor::MIN..Anchor::MAX),
|
||||
@@ -854,17 +783,18 @@ impl BufferDiff {
|
||||
file_exists: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Rope> {
|
||||
let (new_index_text, new_pending_hunks) = self.inner.stage_or_unstage_hunks(
|
||||
let (new_index_text, 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, _| {
|
||||
diff.inner.pending_hunks = new_pending_hunks;
|
||||
for (offset, pending_hunk) in pending_hunks {
|
||||
diff.inner.pending_hunks.insert(offset, pending_hunk);
|
||||
}
|
||||
});
|
||||
}
|
||||
cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
|
||||
@@ -908,8 +838,8 @@ impl BufferDiff {
|
||||
language: Option<Arc<Language>>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> anyhow::Result<BufferDiffSnapshot> {
|
||||
let inner = if base_text_changed || language_changed {
|
||||
) -> anyhow::Result<Option<Range<Anchor>>> {
|
||||
let snapshot = if base_text_changed || language_changed {
|
||||
cx.update(|cx| {
|
||||
Self::build(
|
||||
buffer.clone(),
|
||||
@@ -931,45 +861,18 @@ impl BufferDiff {
|
||||
})?
|
||||
.await
|
||||
};
|
||||
Ok(BufferDiffSnapshot {
|
||||
inner,
|
||||
secondary_diff: None,
|
||||
})
|
||||
|
||||
this.update(cx, |this, _| this.set_state(snapshot, &buffer))
|
||||
}
|
||||
|
||||
pub fn set_snapshot(
|
||||
pub fn update_diff_from(
|
||||
&mut self,
|
||||
buffer: &text::BufferSnapshot,
|
||||
new_snapshot: BufferDiffSnapshot,
|
||||
language_changed: bool,
|
||||
secondary_changed_range: Option<Range<Anchor>>,
|
||||
other: &Entity<Self>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Range<Anchor>> {
|
||||
let changed_range = self.set_state(new_snapshot.inner, buffer);
|
||||
if language_changed {
|
||||
cx.emit(BufferDiffEvent::LanguageChanged);
|
||||
}
|
||||
|
||||
let changed_range = match (secondary_changed_range, changed_range) {
|
||||
(None, None) => None,
|
||||
(Some(unstaged_range), None) => self.range_to_hunk_range(unstaged_range, &buffer, cx),
|
||||
(None, Some(uncommitted_range)) => Some(uncommitted_range),
|
||||
(Some(unstaged_range), Some(uncommitted_range)) => {
|
||||
let mut start = uncommitted_range.start;
|
||||
let mut end = uncommitted_range.end;
|
||||
if let Some(unstaged_range) = self.range_to_hunk_range(unstaged_range, &buffer, cx)
|
||||
{
|
||||
start = unstaged_range.start.min(&uncommitted_range.start, &buffer);
|
||||
end = unstaged_range.end.max(&uncommitted_range.end, &buffer);
|
||||
}
|
||||
Some(start..end)
|
||||
}
|
||||
};
|
||||
|
||||
cx.emit(BufferDiffEvent::DiffChanged {
|
||||
changed_range: changed_range.clone(),
|
||||
});
|
||||
changed_range
|
||||
let other = other.read(cx).inner.clone();
|
||||
self.set_state(other, buffer)
|
||||
}
|
||||
|
||||
fn set_state(
|
||||
@@ -987,9 +890,7 @@ impl BufferDiff {
|
||||
}
|
||||
_ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)),
|
||||
};
|
||||
|
||||
let pending_hunks = mem::replace(&mut self.inner.pending_hunks, SumTree::new(buffer));
|
||||
|
||||
let pending_hunks = mem::take(&mut self.inner.pending_hunks);
|
||||
self.inner = new_state;
|
||||
if !base_text_changed {
|
||||
self.inner.pending_hunks = pending_hunks;
|
||||
@@ -1222,21 +1123,21 @@ impl DiffHunkStatus {
|
||||
pub fn deleted_none() -> Self {
|
||||
Self {
|
||||
kind: DiffHunkStatusKind::Deleted,
|
||||
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
|
||||
secondary: DiffHunkSecondaryStatus::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn added_none() -> Self {
|
||||
Self {
|
||||
kind: DiffHunkStatusKind::Added,
|
||||
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
|
||||
secondary: DiffHunkSecondaryStatus::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modified_none() -> Self {
|
||||
Self {
|
||||
kind: DiffHunkStatusKind::Modified,
|
||||
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
|
||||
secondary: DiffHunkSecondaryStatus::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1244,14 +1145,13 @@ impl DiffHunkStatus {
|
||||
/// Range (crossing new lines), old, new
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[track_caller]
|
||||
pub fn assert_hunks<ExpectedText, HunkIter>(
|
||||
diff_hunks: HunkIter,
|
||||
pub fn assert_hunks<Iter>(
|
||||
diff_hunks: Iter,
|
||||
buffer: &text::BufferSnapshot,
|
||||
diff_base: &str,
|
||||
expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
|
||||
expected_hunks: &[(Range<u32>, &str, &str, DiffHunkStatus)],
|
||||
) where
|
||||
HunkIter: Iterator<Item = DiffHunk>,
|
||||
ExpectedText: AsRef<str>,
|
||||
Iter: Iterator<Item = DiffHunk>,
|
||||
{
|
||||
let actual_hunks = diff_hunks
|
||||
.map(|hunk| {
|
||||
@@ -1271,14 +1171,14 @@ pub fn assert_hunks<ExpectedText, HunkIter>(
|
||||
.map(|(r, old_text, new_text, status)| {
|
||||
(
|
||||
Point::new(r.start, 0)..Point::new(r.end, 0),
|
||||
old_text.as_ref(),
|
||||
new_text.as_ref().to_string(),
|
||||
*old_text,
|
||||
new_text.to_string(),
|
||||
*status,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
|
||||
assert_eq!(actual_hunks, expected_hunks);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1337,7 +1237,7 @@ mod tests {
|
||||
);
|
||||
|
||||
diff = cx.update(|cx| BufferDiff::build_empty(&buffer, cx));
|
||||
assert_hunks::<&str, _>(
|
||||
assert_hunks(
|
||||
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
@@ -1675,10 +1575,7 @@ mod tests {
|
||||
.hunks_intersecting_range(hunk_range.clone(), &buffer, &cx)
|
||||
.collect::<Vec<_>>();
|
||||
for hunk in &hunks {
|
||||
assert_ne!(
|
||||
hunk.secondary_status,
|
||||
DiffHunkSecondaryStatus::NoSecondaryHunk
|
||||
)
|
||||
assert_ne!(hunk.secondary_status, DiffHunkSecondaryStatus::None)
|
||||
}
|
||||
|
||||
let new_index_text = diff
|
||||
@@ -1957,10 +1854,10 @@ mod tests {
|
||||
let hunk_to_change = hunk.clone();
|
||||
let stage = match hunk.secondary_status {
|
||||
DiffHunkSecondaryStatus::HasSecondaryHunk => {
|
||||
hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
|
||||
hunk.secondary_status = DiffHunkSecondaryStatus::None;
|
||||
true
|
||||
}
|
||||
DiffHunkSecondaryStatus::NoSecondaryHunk => {
|
||||
DiffHunkSecondaryStatus::None => {
|
||||
hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
|
||||
false
|
||||
}
|
||||
|
||||
@@ -396,7 +396,6 @@ impl Server {
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Stage>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Unstage>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::Commit>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitInit>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetRemotes>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitShow>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitReset>)
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::{
|
||||
tests::{rust_lang, TestServer},
|
||||
};
|
||||
use call::ActiveCall;
|
||||
use collections::HashMap;
|
||||
use editor::{
|
||||
actions::{
|
||||
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, Redo, Rename,
|
||||
@@ -1982,6 +1983,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
blame_entry("3a3a3a", 2..3),
|
||||
blame_entry("4c4c4c", 3..4),
|
||||
],
|
||||
permalinks: HashMap::default(), // This field is deprecrated
|
||||
messages: [
|
||||
("1b1b1b", "message for idx-0"),
|
||||
("0d0d0d", "message for idx-1"),
|
||||
@@ -2038,7 +2040,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
// client_b now requests git blame for the open buffer
|
||||
editor_b.update_in(cx_b, |editor_b, window, cx| {
|
||||
assert!(editor_b.blame().is_none());
|
||||
editor_b.toggle_git_blame(&git::Blame {}, window, cx);
|
||||
editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, window, cx);
|
||||
});
|
||||
|
||||
cx_a.executor().run_until_parked();
|
||||
|
||||
@@ -271,7 +271,7 @@ impl TestServer {
|
||||
|
||||
let git_hosting_provider_registry = cx.update(GitHostingProviderRegistry::default_global);
|
||||
git_hosting_provider_registry
|
||||
.register_hosting_provider(Arc::new(git_hosting_providers::Github::new()));
|
||||
.register_hosting_provider(Arc::new(git_hosting_providers::Github));
|
||||
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
|
||||
@@ -323,7 +323,7 @@ impl ChatPanel {
|
||||
.my_0p5()
|
||||
.px_0p5()
|
||||
.gap_x_1()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted))
|
||||
.when(reply_to_message.is_none(), |el| {
|
||||
el.child(
|
||||
@@ -358,7 +358,7 @@ impl ChatPanel {
|
||||
.my_0p5()
|
||||
.px_0p5()
|
||||
.gap_x_1()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.overflow_hidden()
|
||||
.hover(|style| style.bg(cx.theme().colors().element_background))
|
||||
.child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted))
|
||||
@@ -476,7 +476,7 @@ impl ChatPanel {
|
||||
div()
|
||||
.group("")
|
||||
.bg(background)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.overflow_hidden()
|
||||
.px_1p5()
|
||||
.py_0p5()
|
||||
@@ -563,7 +563,7 @@ impl ChatPanel {
|
||||
.child(
|
||||
div()
|
||||
.px_1()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.text_ui_xs(cx)
|
||||
.bg(cx.theme().colors().background)
|
||||
.child("New messages"),
|
||||
@@ -589,7 +589,7 @@ impl ChatPanel {
|
||||
div()
|
||||
.w_6()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover).rounded_md())
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover).rounded_sm())
|
||||
.child(child)
|
||||
}
|
||||
|
||||
@@ -604,7 +604,7 @@ impl ChatPanel {
|
||||
.absolute()
|
||||
.right_2()
|
||||
.overflow_hidden()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.border_color(cx.theme().colors().element_selected)
|
||||
.border_1()
|
||||
.when(!self.has_open_menu(message_id), |el| {
|
||||
|
||||
@@ -531,7 +531,7 @@ impl Render for MessageEditor {
|
||||
.px_2()
|
||||
.py_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.child(EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
|
||||
@@ -300,7 +300,7 @@ impl NotificationPanel {
|
||||
.hover(|style| {
|
||||
style
|
||||
.bg(cx.theme().colors().element_selected)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
})
|
||||
.child(Label::new(relative_timestamp).color(Color::Muted))
|
||||
.tooltip(move |_, cx| {
|
||||
|
||||
@@ -306,7 +306,7 @@ impl ComponentPreview {
|
||||
v_flex()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.w_full()
|
||||
.gap_4()
|
||||
.py_4()
|
||||
@@ -341,17 +341,22 @@ impl ComponentPreview {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn test_status_toast(&self, cx: &mut Context<Self>) {
|
||||
fn test_status_toast(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let status_toast =
|
||||
StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
|
||||
let status_toast = StatusToast::new(
|
||||
"`zed/new-notification-system` created!",
|
||||
window,
|
||||
cx,
|
||||
|this, _, cx| {
|
||||
this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
|
||||
.action("Open Pull Request", |_, cx| {
|
||||
cx.open_url("https://github.com/")
|
||||
})
|
||||
});
|
||||
workspace.toggle_status_toast(status_toast, cx)
|
||||
.action(
|
||||
"Open Pull Request",
|
||||
cx.listener(|_, _, _, cx| cx.open_url("https://github.com/")),
|
||||
)
|
||||
},
|
||||
);
|
||||
workspace.toggle_status_toast(window, cx, status_toast)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -401,8 +406,8 @@ impl Render for ComponentPreview {
|
||||
div().w_full().pb_4().child(
|
||||
Button::new("toast-test", "Launch Toast")
|
||||
.on_click(cx.listener({
|
||||
move |this, _, _window, cx| {
|
||||
this.test_status_toast(cx);
|
||||
move |this, _, window, cx| {
|
||||
this.test_status_toast(window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -122,7 +122,7 @@ impl CopilotCodeVerification {
|
||||
.p_1()
|
||||
.border_1()
|
||||
.border_muted(cx)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.cursor_pointer()
|
||||
.justify_between()
|
||||
.on_mouse_down(gpui::MouseButton::Left, {
|
||||
|
||||
@@ -973,7 +973,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.px_1()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.bg(color.surface_background.opacity(0.5))
|
||||
.map(|stack| {
|
||||
stack.child(
|
||||
|
||||
@@ -94,6 +94,7 @@ ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
languages = {workspace = true, features = ["test-support"] }
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
multi_buffer = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -340,9 +340,7 @@ gpui::actions!(
|
||||
MoveToPreviousWordStart,
|
||||
MoveToStartOfParagraph,
|
||||
MoveToStartOfExcerpt,
|
||||
MoveToStartOfNextExcerpt,
|
||||
MoveToEndOfExcerpt,
|
||||
MoveToEndOfPreviousExcerpt,
|
||||
MoveUp,
|
||||
Newline,
|
||||
NewlineAbove,
|
||||
@@ -380,9 +378,7 @@ gpui::actions!(
|
||||
SelectAll,
|
||||
SelectAllMatches,
|
||||
SelectToStartOfExcerpt,
|
||||
SelectToStartOfNextExcerpt,
|
||||
SelectToEndOfExcerpt,
|
||||
SelectToEndOfPreviousExcerpt,
|
||||
SelectDown,
|
||||
SelectEnclosingSymbol,
|
||||
SelectLargerSyntaxNode,
|
||||
@@ -412,6 +408,7 @@ gpui::actions!(
|
||||
Tab,
|
||||
Backtab,
|
||||
ToggleAutoSignatureHelp,
|
||||
ToggleGitBlame,
|
||||
ToggleGitBlameInline,
|
||||
ToggleIndentGuides,
|
||||
ToggleInlayHints,
|
||||
|
||||
@@ -7,7 +7,6 @@ use std::time::Duration;
|
||||
|
||||
pub struct BlinkManager {
|
||||
blink_interval: Duration,
|
||||
|
||||
blink_epoch: usize,
|
||||
blinking_paused: bool,
|
||||
visible: bool,
|
||||
@@ -24,7 +23,6 @@ impl BlinkManager {
|
||||
|
||||
Self {
|
||||
blink_interval,
|
||||
|
||||
blink_epoch: 0,
|
||||
blinking_paused: false,
|
||||
visible: true,
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::lsp_ext::find_specific_language_server_in_selection;
|
||||
|
||||
use crate::{element::register_action, Editor, SwitchSourceHeader};
|
||||
|
||||
const CLANGD_SERVER_NAME: &str = "clangd";
|
||||
use project::lsp_store::clangd_ext::CLANGD_SERVER_NAME;
|
||||
|
||||
fn is_c_language(language: &Language) -> bool {
|
||||
return language.name() == "C++".into() || language.name() == "C".into();
|
||||
@@ -47,7 +47,7 @@ pub fn switch_source_header(
|
||||
project.request_lsp(
|
||||
buffer,
|
||||
project::LanguageServerToQuery::Other(server_to_query),
|
||||
project::lsp_ext_command::SwitchSourceHeader,
|
||||
project::lsp_store::lsp_ext_command::SwitchSourceHeader,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -534,7 +534,7 @@ impl CompletionsMenu {
|
||||
};
|
||||
let color_swatch = completion
|
||||
.color()
|
||||
.map(|color| div().size_4().bg(color).rounded_sm());
|
||||
.map(|color| div().size_4().bg(color).rounded_xs());
|
||||
|
||||
div().min_w(px(280.)).max_w(px(540.)).child(
|
||||
ListItem::new(mat.candidate_id)
|
||||
@@ -851,7 +851,7 @@ impl CodeActionsItem {
|
||||
|
||||
pub fn label(&self) -> String {
|
||||
match self {
|
||||
Self::CodeAction { action, .. } => action.lsp_action.title.clone(),
|
||||
Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
|
||||
Self::Task(_, task) => task.resolved_label.clone(),
|
||||
}
|
||||
}
|
||||
@@ -984,7 +984,7 @@ impl CodeActionsMenu {
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
|
||||
action.lsp_action.title.replace("\n", ""),
|
||||
action.lsp_action.title().replace("\n", ""),
|
||||
)
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
@@ -1029,7 +1029,7 @@ impl CodeActionsMenu {
|
||||
.max_by_key(|(_, action)| match action {
|
||||
CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
|
||||
CodeActionsItem::CodeAction { action, .. } => {
|
||||
action.lsp_action.title.chars().count()
|
||||
action.lsp_action.title().chars().count()
|
||||
}
|
||||
})
|
||||
.map(|(ix, _)| ix),
|
||||
|
||||
@@ -43,7 +43,7 @@ use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, Under
|
||||
pub use inlay_map::Inlay;
|
||||
use inlay_map::{InlayMap, InlaySnapshot};
|
||||
pub use inlay_map::{InlayOffset, InlayPoint};
|
||||
use invisibles::{is_invisible, replacement};
|
||||
pub use invisibles::{is_invisible, replacement};
|
||||
use language::{
|
||||
language_settings::language_settings, ChunkRenderer, OffsetUtf16, Point,
|
||||
Subscription as BufferSubscription,
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn is_invisible(c: char) -> bool {
|
||||
// ASCII control characters have fancy unicode glyphs, everything else
|
||||
// is replaced by a space - unless it is used in combining characters in
|
||||
// which case we need to leave it in the string.
|
||||
pub(crate) fn replacement(c: char) -> Option<&'static str> {
|
||||
pub fn replacement(c: char) -> Option<&'static str> {
|
||||
if c <= '\x1f' {
|
||||
Some(C0_SYMBOLS[c as usize])
|
||||
} else if c == '\x7f' {
|
||||
|
||||
@@ -28,6 +28,7 @@ mod hover_popover;
|
||||
mod indent_guides;
|
||||
mod inlay_hint_cache;
|
||||
pub mod items;
|
||||
mod jsx_tag_auto_close;
|
||||
mod linked_editing_ranges;
|
||||
mod lsp_ext;
|
||||
mod mouse_context_menu;
|
||||
@@ -85,8 +86,8 @@ use gpui::{
|
||||
ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler,
|
||||
EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global,
|
||||
HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad,
|
||||
ParentElement, Pixels, Render, SharedString, Size, Styled, StyledText, Subscription, Task,
|
||||
TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle,
|
||||
ParentElement, Pixels, Render, SharedString, Size, Stateful, Styled, StyledText, Subscription,
|
||||
Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle,
|
||||
WeakEntity, WeakFocusHandle, Window,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
@@ -605,6 +606,12 @@ pub trait Addon: 'static {
|
||||
fn to_any(&self) -> &dyn std::any::Any;
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum IsVimMode {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`].
|
||||
///
|
||||
/// See the [module level documentation](self) for more information.
|
||||
@@ -636,7 +643,6 @@ pub struct Editor {
|
||||
inline_diagnostics_enabled: bool,
|
||||
inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>,
|
||||
soft_wrap_mode_override: Option<language_settings::SoftWrap>,
|
||||
hard_wrap: Option<usize>,
|
||||
|
||||
// TODO: make this a access method
|
||||
pub project: Option<Entity<Project>>,
|
||||
@@ -720,6 +726,7 @@ pub struct Editor {
|
||||
use_autoclose: bool,
|
||||
use_auto_surround: bool,
|
||||
auto_replace_emoji_shortcode: bool,
|
||||
jsx_tag_auto_close_enabled_in_any_buffer: bool,
|
||||
show_git_blame_gutter: bool,
|
||||
show_git_blame_inline: bool,
|
||||
show_git_blame_inline_delay_task: Option<Task<()>>,
|
||||
@@ -1195,7 +1202,7 @@ impl Editor {
|
||||
.bg(cx.theme().colors().ghost_element_background)
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.rounded_sm()
|
||||
.rounded_xs()
|
||||
.size_full()
|
||||
.cursor_pointer()
|
||||
.child("⋯")
|
||||
@@ -1243,6 +1250,11 @@ impl Editor {
|
||||
let mut project_subscriptions = Vec::new();
|
||||
if mode == EditorMode::Full {
|
||||
if let Some(project) = project.as_ref() {
|
||||
if buffer.read(cx).is_singleton() {
|
||||
project_subscriptions.push(cx.observe_in(project, window, |_, _, _, cx| {
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
}));
|
||||
}
|
||||
project_subscriptions.push(cx.subscribe_in(
|
||||
project,
|
||||
window,
|
||||
@@ -1347,7 +1359,6 @@ impl Editor {
|
||||
inline_diagnostics_update: Task::ready(()),
|
||||
inline_diagnostics: Vec::new(),
|
||||
soft_wrap_mode_override,
|
||||
hard_wrap: None,
|
||||
completion_provider: project.clone().map(|project| Box::new(project) as _),
|
||||
semantics_provider: project.clone().map(|project| Rc::new(project) as _),
|
||||
collaboration_hub: project.clone().map(|project| Box::new(project) as _),
|
||||
@@ -1402,6 +1413,7 @@ impl Editor {
|
||||
use_autoclose: true,
|
||||
use_auto_surround: true,
|
||||
auto_replace_emoji_shortcode: false,
|
||||
jsx_tag_auto_close_enabled_in_any_buffer: false,
|
||||
leader_peer_id: None,
|
||||
remote_id: None,
|
||||
hover_state: Default::default(),
|
||||
@@ -1485,6 +1497,7 @@ impl Editor {
|
||||
|
||||
this.end_selection(window, cx);
|
||||
this.scroll_manager.show_scrollbar(window, cx);
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx);
|
||||
|
||||
if mode == EditorMode::Full {
|
||||
let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars();
|
||||
@@ -1564,16 +1577,13 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(singleton_buffer) = self.buffer.read(cx).as_singleton() {
|
||||
if let Some(extension) = singleton_buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.and_then(|file| file.path().extension()?.to_str())
|
||||
{
|
||||
key_context.set("extension", extension.to_string());
|
||||
}
|
||||
} else {
|
||||
key_context.add("multibuffer");
|
||||
if let Some(extension) = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).file()?.path().extension()?.to_str())
|
||||
{
|
||||
key_context.set("extension", extension.to_string());
|
||||
}
|
||||
|
||||
if has_active_edit_prediction {
|
||||
@@ -3096,6 +3106,9 @@ impl Editor {
|
||||
drop(snapshot);
|
||||
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
let initial_buffer_versions =
|
||||
jsx_tag_auto_close::construct_initial_buffer_versions_map(this, &edits, cx);
|
||||
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits, this.autoindent_mode.clone(), cx);
|
||||
});
|
||||
@@ -3180,22 +3193,10 @@ impl Editor {
|
||||
|
||||
let trigger_in_words =
|
||||
this.show_edit_predictions_in_menu() || !had_active_inline_completion;
|
||||
if this.hard_wrap.is_some() {
|
||||
let latest: Range<Point> = this.selections.newest(cx).range();
|
||||
if latest.is_empty()
|
||||
&& this
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.line_len(MultiBufferRow(latest.start.row))
|
||||
== latest.start.column
|
||||
{
|
||||
this.rewrap_impl(true, cx)
|
||||
}
|
||||
}
|
||||
this.trigger_completion_on_input(&text, trigger_in_words, window, cx);
|
||||
linked_editing_ranges::refresh_linked_ranges(this, window, cx);
|
||||
this.refresh_inline_completion(true, false, window, cx);
|
||||
jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6345,6 +6346,9 @@ impl Editor {
|
||||
|
||||
const BORDER_WIDTH: Pixels = px(1.);
|
||||
|
||||
let keybind = self.render_edit_prediction_accept_keybind(window, cx);
|
||||
let has_keybind = keybind.is_some();
|
||||
|
||||
let mut element = h_flex()
|
||||
.items_start()
|
||||
.child(
|
||||
@@ -6375,7 +6379,19 @@ impl Editor {
|
||||
.border(BORDER_WIDTH)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_r_lg()
|
||||
.children(self.render_edit_prediction_accept_keybind(window, cx)),
|
||||
.id("edit_prediction_diff_popover_keybind")
|
||||
.when(!has_keybind, |el| {
|
||||
let status_colors = cx.theme().status();
|
||||
|
||||
el.bg(status_colors.error_background)
|
||||
.border_color(status_colors.error.opacity(0.6))
|
||||
.child(Icon::new(IconName::Info).color(Color::Error))
|
||||
.cursor_default()
|
||||
.hoverable_tooltip(move |_window, cx| {
|
||||
cx.new(|_| MissingEditPredictionKeybindingTooltip).into()
|
||||
})
|
||||
})
|
||||
.children(keybind),
|
||||
)
|
||||
.into_any();
|
||||
|
||||
@@ -6473,7 +6489,11 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_edit_prediction_accept_keybind(&self, window: &mut Window, cx: &App) -> Option<Div> {
|
||||
fn render_edit_prediction_accept_keybind(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &App,
|
||||
) -> Option<AnyElement> {
|
||||
let accept_binding = self.accept_edit_prediction_keybind(window, cx);
|
||||
let accept_keystroke = accept_binding.keystroke()?;
|
||||
|
||||
@@ -6509,6 +6529,7 @@ impl Editor {
|
||||
.size(Some(IconSize::XSmall.rems().into())),
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -6518,21 +6539,52 @@ impl Editor {
|
||||
icon: Option<IconName>,
|
||||
window: &mut Window,
|
||||
cx: &App,
|
||||
) -> Option<Div> {
|
||||
) -> Option<Stateful<Div>> {
|
||||
let padding_right = if icon.is_some() { px(4.) } else { px(8.) };
|
||||
|
||||
let keybind = self.render_edit_prediction_accept_keybind(window, cx);
|
||||
let has_keybind = keybind.is_some();
|
||||
|
||||
let result = h_flex()
|
||||
.id("ep-line-popover")
|
||||
.py_0p5()
|
||||
.pl_1()
|
||||
.pr(padding_right)
|
||||
.gap_1()
|
||||
.rounded(px(6.))
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.bg(Self::edit_prediction_line_popover_bg_color(cx))
|
||||
.border_color(Self::edit_prediction_callout_popover_border_color(cx))
|
||||
.shadow_sm()
|
||||
.children(self.render_edit_prediction_accept_keybind(window, cx))
|
||||
.child(Label::new(label).size(LabelSize::Small))
|
||||
.when(!has_keybind, |el| {
|
||||
let status_colors = cx.theme().status();
|
||||
|
||||
el.bg(status_colors.error_background)
|
||||
.border_color(status_colors.error.opacity(0.6))
|
||||
.pl_2()
|
||||
.child(Icon::new(IconName::ZedPredictError).color(Color::Error))
|
||||
.cursor_default()
|
||||
.hoverable_tooltip(move |_window, cx| {
|
||||
cx.new(|_| MissingEditPredictionKeybindingTooltip).into()
|
||||
})
|
||||
})
|
||||
.children(keybind)
|
||||
.child(
|
||||
Label::new(label)
|
||||
.size(LabelSize::Small)
|
||||
.when(!has_keybind, |el| {
|
||||
el.color(cx.theme().status().error.into()).strikethrough()
|
||||
}),
|
||||
)
|
||||
.when(!has_keybind, |el| {
|
||||
el.child(
|
||||
h_flex().ml_1().child(
|
||||
Icon::new(IconName::Info)
|
||||
.size(IconSize::Small)
|
||||
.color(cx.theme().status().error.into()),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when_some(icon, |element, icon| {
|
||||
element.child(
|
||||
div()
|
||||
@@ -6629,6 +6681,9 @@ impl Editor {
|
||||
.elevation_2(cx)
|
||||
.border(BORDER_WIDTH)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.when(accept_keystroke.is_none(), |el| {
|
||||
el.border_color(cx.theme().status().error)
|
||||
})
|
||||
.rounded(RADIUS)
|
||||
.rounded_tl(px(0.))
|
||||
.overflow_hidden()
|
||||
@@ -6657,16 +6712,37 @@ impl Editor {
|
||||
el.child(
|
||||
Label::new("Hold")
|
||||
.size(LabelSize::Small)
|
||||
.when(accept_keystroke.is_none(), |el| {
|
||||
el.strikethrough()
|
||||
})
|
||||
.line_height_style(LineHeightStyle::UiLabel),
|
||||
)
|
||||
})
|
||||
.child(h_flex().children(ui::render_modifiers(
|
||||
&accept_keystroke?.modifiers,
|
||||
PlatformStyle::platform(),
|
||||
Some(Color::Default),
|
||||
Some(IconSize::XSmall.rems().into()),
|
||||
false,
|
||||
))),
|
||||
.id("edit_prediction_cursor_popover_keybind")
|
||||
.when(accept_keystroke.is_none(), |el| {
|
||||
let status_colors = cx.theme().status();
|
||||
|
||||
el.bg(status_colors.error_background)
|
||||
.border_color(status_colors.error.opacity(0.6))
|
||||
.child(Icon::new(IconName::Info).color(Color::Error))
|
||||
.cursor_default()
|
||||
.hoverable_tooltip(move |_window, cx| {
|
||||
cx.new(|_| MissingEditPredictionKeybindingTooltip)
|
||||
.into()
|
||||
})
|
||||
})
|
||||
.when_some(
|
||||
accept_keystroke.as_ref(),
|
||||
|el, accept_keystroke| {
|
||||
el.child(h_flex().children(ui::render_modifiers(
|
||||
&accept_keystroke.modifiers,
|
||||
PlatformStyle::platform(),
|
||||
Some(Color::Default),
|
||||
Some(IconSize::XSmall.rems().into()),
|
||||
false,
|
||||
)))
|
||||
},
|
||||
),
|
||||
)
|
||||
.into_any(),
|
||||
);
|
||||
@@ -8439,10 +8515,10 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn rewrap(&mut self, _: &Rewrap, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.rewrap_impl(false, cx)
|
||||
self.rewrap_impl(IsVimMode::No, cx)
|
||||
}
|
||||
|
||||
pub fn rewrap_impl(&mut self, override_language_settings: bool, cx: &mut Context<Self>) {
|
||||
pub fn rewrap_impl(&mut self, is_vim_mode: IsVimMode, cx: &mut Context<Self>) {
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
let mut selections = selections.iter().peekable();
|
||||
@@ -8516,9 +8592,7 @@ impl Editor {
|
||||
RewrapBehavior::Anywhere => true,
|
||||
};
|
||||
|
||||
let should_rewrap = override_language_settings
|
||||
|| allow_rewrap_based_on_language
|
||||
|| self.hard_wrap.is_some();
|
||||
let should_rewrap = is_vim_mode == IsVimMode::Yes || allow_rewrap_based_on_language;
|
||||
if !should_rewrap {
|
||||
continue;
|
||||
}
|
||||
@@ -8566,11 +8640,9 @@ impl Editor {
|
||||
continue;
|
||||
};
|
||||
|
||||
let wrap_column = self.hard_wrap.unwrap_or_else(|| {
|
||||
buffer
|
||||
.language_settings_at(Point::new(start_row, 0), cx)
|
||||
.preferred_line_length as usize
|
||||
});
|
||||
let wrap_column = buffer
|
||||
.language_settings_at(Point::new(start_row, 0), cx)
|
||||
.preferred_line_length as usize;
|
||||
let wrapped_text = wrap_with_prefix(
|
||||
line_prefix,
|
||||
lines_without_prefixes.join(" "),
|
||||
@@ -8581,7 +8653,7 @@ impl Editor {
|
||||
// TODO: should always use char-based diff while still supporting cursor behavior that
|
||||
// matches vim.
|
||||
let mut diff_options = DiffOptions::default();
|
||||
if override_language_settings {
|
||||
if is_vim_mode == IsVimMode::Yes {
|
||||
diff_options.max_word_diff_len = 0;
|
||||
diff_options.max_word_diff_line_count = 0;
|
||||
} else {
|
||||
@@ -9777,31 +9849,6 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_start_of_next_excerpt(
|
||||
&mut self,
|
||||
_: &MoveToStartOfNextExcerpt,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
selection.collapse_to(
|
||||
movement::start_of_excerpt(
|
||||
map,
|
||||
selection.head(),
|
||||
workspace::searchable::Direction::Next,
|
||||
),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_end_of_excerpt(
|
||||
&mut self,
|
||||
_: &MoveToEndOfExcerpt,
|
||||
@@ -9827,31 +9874,6 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_end_of_previous_excerpt(
|
||||
&mut self,
|
||||
_: &MoveToEndOfPreviousExcerpt,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
selection.collapse_to(
|
||||
movement::end_of_excerpt(
|
||||
map,
|
||||
selection.head(),
|
||||
workspace::searchable::Direction::Prev,
|
||||
),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_to_start_of_excerpt(
|
||||
&mut self,
|
||||
_: &SelectToStartOfExcerpt,
|
||||
@@ -9873,27 +9895,6 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_to_start_of_next_excerpt(
|
||||
&mut self,
|
||||
_: &SelectToStartOfNextExcerpt,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(
|
||||
movement::start_of_excerpt(map, head, workspace::searchable::Direction::Next),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_to_end_of_excerpt(
|
||||
&mut self,
|
||||
_: &SelectToEndOfExcerpt,
|
||||
@@ -9915,27 +9916,6 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_to_end_of_previous_excerpt(
|
||||
&mut self,
|
||||
_: &SelectToEndOfPreviousExcerpt,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(
|
||||
movement::end_of_excerpt(map, head, workspace::searchable::Direction::Prev),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_beginning(
|
||||
&mut self,
|
||||
_: &MoveToBeginning,
|
||||
@@ -11568,7 +11548,7 @@ impl Editor {
|
||||
fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
self.go_to_hunk_before_or_after_position(
|
||||
self.go_to_hunk_after_or_before_position(
|
||||
&snapshot,
|
||||
selection.head(),
|
||||
Direction::Next,
|
||||
@@ -11577,7 +11557,7 @@ impl Editor {
|
||||
);
|
||||
}
|
||||
|
||||
fn go_to_hunk_before_or_after_position(
|
||||
fn go_to_hunk_after_or_before_position(
|
||||
&mut self,
|
||||
snapshot: &EditorSnapshot,
|
||||
position: Point,
|
||||
@@ -11628,7 +11608,7 @@ impl Editor {
|
||||
) {
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
self.go_to_hunk_before_or_after_position(
|
||||
self.go_to_hunk_after_or_before_position(
|
||||
&snapshot,
|
||||
selection.head(),
|
||||
Direction::Prev,
|
||||
@@ -13790,38 +13770,23 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let position = self.selections.newest::<Point>(cx).head();
|
||||
let mut row = snapshot
|
||||
.buffer_snapshot
|
||||
.diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point())
|
||||
.find(|hunk| hunk.row_range.start.0 > position.row)
|
||||
.map(|hunk| hunk.row_range.start);
|
||||
let newest_range = self.selections.newest::<Point>(cx).range();
|
||||
|
||||
let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded();
|
||||
// Outside of the project diff editor, wrap around to the beginning.
|
||||
if !all_diff_hunks_expanded {
|
||||
row = row.or_else(|| {
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.diff_hunks_in_range(Point::zero()..position)
|
||||
.find(|hunk| hunk.row_range.end.0 < position.row)
|
||||
.map(|hunk| hunk.row_range.start)
|
||||
let run_twice = snapshot
|
||||
.hunks_for_ranges([newest_range])
|
||||
.first()
|
||||
.is_some_and(|hunk| {
|
||||
let next_line = Point::new(hunk.row_range.end.0 + 1, 0);
|
||||
self.hunk_after_position(&snapshot, next_line)
|
||||
.is_some_and(|other| other.row_range == hunk.row_range)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(row) = row {
|
||||
let destination = Point::new(row.0, 0);
|
||||
let autoscroll = Autoscroll::center();
|
||||
|
||||
self.unfold_ranges(&[destination..destination], false, false, cx);
|
||||
self.change_selections(Some(autoscroll), window, cx, |s| {
|
||||
s.select_ranges([destination..destination]);
|
||||
});
|
||||
} else if all_diff_hunks_expanded {
|
||||
window.dispatch_action(::git::ExpandCommitEditor.boxed_clone(), cx);
|
||||
if run_twice {
|
||||
self.go_to_next_hunk(&GoToHunk, window, cx);
|
||||
}
|
||||
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
|
||||
self.go_to_next_hunk(&GoToHunk, window, cx);
|
||||
}
|
||||
|
||||
fn do_stage_or_unstage(
|
||||
@@ -14151,11 +14116,6 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_hard_wrap(&mut self, hard_wrap: Option<usize>, cx: &mut Context<Self>) {
|
||||
self.hard_wrap = hard_wrap;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_text_style_refinement(&mut self, style: TextStyleRefinement) {
|
||||
self.text_style_refinement = Some(style);
|
||||
}
|
||||
@@ -14431,7 +14391,7 @@ impl Editor {
|
||||
|
||||
pub fn toggle_git_blame(
|
||||
&mut self,
|
||||
_: &::git::Blame,
|
||||
_: &ToggleGitBlame,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -14927,14 +14887,14 @@ impl Editor {
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> BTreeMap<DisplayRow, LineHighlight> {
|
||||
) -> BTreeMap<DisplayRow, Background> {
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let mut used_highlight_orders = HashMap::default();
|
||||
self.highlighted_rows
|
||||
.iter()
|
||||
.flat_map(|(_, highlighted_rows)| highlighted_rows.iter())
|
||||
.fold(
|
||||
BTreeMap::<DisplayRow, LineHighlight>::new(),
|
||||
BTreeMap::<DisplayRow, Background>::new(),
|
||||
|mut unique_rows, highlight| {
|
||||
let start = highlight.range.start.to_display_point(&snapshot);
|
||||
let end = highlight.range.end.to_display_point(&snapshot);
|
||||
@@ -15447,6 +15407,7 @@ impl Editor {
|
||||
let buffer = self.buffer.read(cx);
|
||||
self.registered_buffers
|
||||
.retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some());
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
|
||||
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
|
||||
}
|
||||
multi_buffer::Event::ExcerptsEdited {
|
||||
@@ -15466,6 +15427,7 @@ impl Editor {
|
||||
}
|
||||
multi_buffer::Event::Reparsed(buffer_id) => {
|
||||
self.tasks_update_task = Some(self.refresh_runnables(window, cx));
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
|
||||
|
||||
cx.emit(EditorEvent::Reparsed(*buffer_id));
|
||||
}
|
||||
@@ -15474,14 +15436,20 @@ impl Editor {
|
||||
}
|
||||
multi_buffer::Event::LanguageChanged(buffer_id) => {
|
||||
linked_editing_ranges::refresh_linked_ranges(self, window, cx);
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
|
||||
cx.emit(EditorEvent::Reparsed(*buffer_id));
|
||||
cx.notify();
|
||||
}
|
||||
multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged),
|
||||
multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved),
|
||||
multi_buffer::Event::FileHandleChanged
|
||||
| multi_buffer::Event::Reloaded
|
||||
| multi_buffer::Event::BufferDiffChanged => cx.emit(EditorEvent::TitleChanged),
|
||||
multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => {
|
||||
cx.emit(EditorEvent::TitleChanged)
|
||||
}
|
||||
// multi_buffer::Event::DiffBaseChanged => {
|
||||
// self.scrollbar_marker_state.dirty = true;
|
||||
// cx.emit(EditorEvent::DiffBaseChanged);
|
||||
// cx.notify();
|
||||
// }
|
||||
multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed),
|
||||
multi_buffer::Event::DiagnosticsUpdated => {
|
||||
self.refresh_active_diagnostics(cx);
|
||||
@@ -18435,26 +18403,36 @@ fn all_edits_insertions_or_deletions(
|
||||
all_insertions || all_deletions
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct LineHighlight {
|
||||
pub background: Background,
|
||||
pub border: Option<gpui::Hsla>,
|
||||
}
|
||||
struct MissingEditPredictionKeybindingTooltip;
|
||||
|
||||
impl From<Hsla> for LineHighlight {
|
||||
fn from(hsla: Hsla) -> Self {
|
||||
Self {
|
||||
background: hsla.into(),
|
||||
border: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Background> for LineHighlight {
|
||||
fn from(background: Background) -> Self {
|
||||
Self {
|
||||
background,
|
||||
border: None,
|
||||
}
|
||||
impl Render for MissingEditPredictionKeybindingTooltip {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
|
||||
ui::tooltip_container(window, cx, |container, _, cx| {
|
||||
container
|
||||
.flex_shrink_0()
|
||||
.max_w_80()
|
||||
.min_h(rems_from_px(124.))
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.text_ui_sm(cx)
|
||||
.child(Label::new("Conflict with Accept Keybinding"))
|
||||
.child("Your keymap currently overrides the default accept keybinding. To continue, assign one keybinding for the `editor::AcceptEditPrediction` action.")
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.pb_1()
|
||||
.gap_1()
|
||||
.items_end()
|
||||
.w_full()
|
||||
.child(Button::new("open-keymap", "Assign Keybinding").size(ButtonSize::Compact).on_click(|_ev, window, cx| {
|
||||
window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx)
|
||||
}))
|
||||
.child(Button::new("see-docs", "See Docs").size(ButtonSize::Compact).on_click(|_ev, _window, cx| {
|
||||
cx.open_url("https://zed.dev/docs/completions#edit-predictions-missing-keybinding");
|
||||
})),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4737,31 +4737,6 @@ async fn test_rewrap(cx: &mut TestAppContext) {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_hard_wrap(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
editor.set_hard_wrap(Some(14), cx);
|
||||
});
|
||||
|
||||
cx.set_state(indoc!(
|
||||
"
|
||||
one two three ˇ
|
||||
"
|
||||
));
|
||||
cx.simulate_input("four");
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.assert_editor_state(indoc!(
|
||||
"
|
||||
one two three
|
||||
fourˇ
|
||||
"
|
||||
));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_clipboard(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -16832,6 +16807,245 @@ async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
|
||||
"});
|
||||
}
|
||||
|
||||
mod autoclose_tags {
|
||||
use super::*;
|
||||
use language::language_settings::JsxTagAutoCloseSettings;
|
||||
use languages::language;
|
||||
|
||||
async fn test_setup(cx: &mut TestAppContext) -> EditorTestContext {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true });
|
||||
});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
let language = language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into());
|
||||
|
||||
buffer.set_language(Some(language), cx)
|
||||
});
|
||||
|
||||
cx
|
||||
}
|
||||
|
||||
macro_rules! check {
|
||||
($name:ident, $initial:literal + $input:literal => $expected:expr) => {
|
||||
#[gpui::test]
|
||||
async fn $name(cx: &mut TestAppContext) {
|
||||
let mut cx = test_setup(cx).await;
|
||||
cx.set_state($initial);
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input($input, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state($expected);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
check!(
|
||||
test_basic,
|
||||
"<divˇ" + ">" => "<div>ˇ</div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_basic_nested,
|
||||
"<div><divˇ</div>" + ">" => "<div><div>ˇ</div></div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_basic_ignore_already_closed,
|
||||
"<div><divˇ</div></div>" + ">" => "<div><div>ˇ</div></div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_doesnt_autoclose_closing_tag,
|
||||
"</divˇ" + ">" => "</div>ˇ"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_jsx_attr,
|
||||
"<div attr={</div>}ˇ" + ">" => "<div attr={</div>}>ˇ</div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_ignores_closing_tags_in_expr_block,
|
||||
"<div><divˇ{</div>}</div>" + ">" => "<div><div>ˇ</div>{</div>}</div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_doesnt_autoclose_on_gt_in_expr,
|
||||
"<div attr={1 ˇ" + ">" => "<div attr={1 >ˇ"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_ignores_closing_tags_with_different_tag_names,
|
||||
"<div><divˇ</div></span>" + ">" => "<div><div>ˇ</div></div></span>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_autocloses_in_jsx_expression,
|
||||
"<div>{<divˇ}</div>" + ">" => "<div>{<div>ˇ</div>}</div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_doesnt_autoclose_already_closed_in_jsx_expression,
|
||||
"<div>{<divˇ</div>}</div>" + ">" => "<div>{<div>ˇ</div>}</div>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_autocloses_fragment,
|
||||
"<ˇ" + ">" => "<>ˇ</>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_does_not_include_type_argument_in_autoclose_tag_name,
|
||||
"<Component<T> attr={boolean_value}ˇ" + ">" => "<Component<T> attr={boolean_value}>ˇ</Component>"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_does_not_autoclose_doctype,
|
||||
"<!DOCTYPE htmlˇ" + ">" => "<!DOCTYPE html>ˇ"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_does_not_autoclose_comment,
|
||||
"<!-- comment --ˇ" + ">" => "<!-- comment -->ˇ"
|
||||
);
|
||||
|
||||
check!(
|
||||
test_multi_cursor_autoclose_same_tag,
|
||||
r#"
|
||||
<divˇ
|
||||
<divˇ
|
||||
"#
|
||||
+ ">" =>
|
||||
r#"
|
||||
<div>ˇ</div>
|
||||
<div>ˇ</div>
|
||||
"#
|
||||
);
|
||||
|
||||
check!(
|
||||
test_multi_cursor_autoclose_different_tags,
|
||||
r#"
|
||||
<divˇ
|
||||
<spanˇ
|
||||
"#
|
||||
+ ">" =>
|
||||
r#"
|
||||
<div>ˇ</div>
|
||||
<span>ˇ</span>
|
||||
"#
|
||||
);
|
||||
|
||||
check!(
|
||||
test_multi_cursor_autoclose_some_dont_autoclose_others,
|
||||
r#"
|
||||
<divˇ
|
||||
<div /ˇ
|
||||
<spanˇ</span>
|
||||
<!DOCTYPE htmlˇ
|
||||
</headˇ
|
||||
<Component<T>ˇ
|
||||
ˇ
|
||||
"#
|
||||
+ ">" =>
|
||||
r#"
|
||||
<div>ˇ</div>
|
||||
<div />ˇ
|
||||
<span>ˇ</span>
|
||||
<!DOCTYPE html>ˇ
|
||||
</head>ˇ
|
||||
<Component<T>>ˇ</Component>
|
||||
>ˇ
|
||||
"#
|
||||
);
|
||||
|
||||
check!(
|
||||
test_doesnt_mess_up_trailing_text,
|
||||
"<divˇfoobar" + ">" => "<div>ˇ</div>foobar"
|
||||
);
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multibuffer(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true });
|
||||
});
|
||||
|
||||
let buffer_a = cx.new(|cx| {
|
||||
let mut buf = language::Buffer::local("<div", cx);
|
||||
buf.set_language(
|
||||
Some(language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into())),
|
||||
cx,
|
||||
);
|
||||
buf
|
||||
});
|
||||
let buffer_b = cx.new(|cx| {
|
||||
let mut buf = language::Buffer::local("<pre", cx);
|
||||
buf.set_language(
|
||||
Some(language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into())),
|
||||
cx,
|
||||
);
|
||||
buf
|
||||
});
|
||||
let buffer_c = cx.new(|cx| {
|
||||
let buf = language::Buffer::local("<span", cx);
|
||||
buf
|
||||
});
|
||||
let buffer = cx.new(|cx| {
|
||||
let mut buf = MultiBuffer::new(language::Capability::ReadWrite);
|
||||
buf.push_excerpts(
|
||||
buffer_a,
|
||||
[ExcerptRange {
|
||||
context: text::Anchor::MIN..text::Anchor::MAX,
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
buf.push_excerpts(
|
||||
buffer_b,
|
||||
[ExcerptRange {
|
||||
context: text::Anchor::MIN..text::Anchor::MAX,
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
buf.push_excerpts(
|
||||
buffer_c,
|
||||
[ExcerptRange {
|
||||
context: text::Anchor::MIN..text::Anchor::MAX,
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
buf
|
||||
});
|
||||
let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
|
||||
|
||||
let mut cx = EditorTestContext::for_editor(editor, cx).await;
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |selections| {
|
||||
selections.select(vec![
|
||||
Selection::from_offset(4),
|
||||
Selection::from_offset(9),
|
||||
Selection::from_offset(15),
|
||||
])
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input(">", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.assert_editor_state("<div>ˇ</div>\n<pre>ˇ</pre>\n<span>ˇ");
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
|
||||
point..point
|
||||
|
||||
@@ -20,10 +20,10 @@ use crate::{
|
||||
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,
|
||||
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, 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};
|
||||
@@ -32,14 +32,15 @@ use collections::{BTreeMap, HashMap, HashSet};
|
||||
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,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
|
||||
StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement, Window,
|
||||
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, pattern_slash,
|
||||
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, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine,
|
||||
SharedString, Size, StatefulInteractiveElement, Style, Styled, Subscription, TextRun,
|
||||
TextStyleRefinement, Window,
|
||||
};
|
||||
use inline_completion::Direction;
|
||||
use itertools::Itertools;
|
||||
@@ -55,7 +56,7 @@ use multi_buffer::{
|
||||
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
|
||||
RowInfo,
|
||||
};
|
||||
use project::project_settings::{self, GitGutterSetting, ProjectSettings};
|
||||
use project::project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings};
|
||||
use settings::Settings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::{
|
||||
@@ -281,9 +282,7 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::move_to_beginning);
|
||||
register_action(editor, window, Editor::move_to_end);
|
||||
register_action(editor, window, Editor::move_to_start_of_excerpt);
|
||||
register_action(editor, window, Editor::move_to_start_of_next_excerpt);
|
||||
register_action(editor, window, Editor::move_to_end_of_excerpt);
|
||||
register_action(editor, window, Editor::move_to_end_of_previous_excerpt);
|
||||
register_action(editor, window, Editor::select_up);
|
||||
register_action(editor, window, Editor::select_down);
|
||||
register_action(editor, window, Editor::select_left);
|
||||
@@ -297,9 +296,7 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::select_to_start_of_paragraph);
|
||||
register_action(editor, window, Editor::select_to_end_of_paragraph);
|
||||
register_action(editor, window, Editor::select_to_start_of_excerpt);
|
||||
register_action(editor, window, Editor::select_to_start_of_next_excerpt);
|
||||
register_action(editor, window, Editor::select_to_end_of_excerpt);
|
||||
register_action(editor, window, Editor::select_to_end_of_previous_excerpt);
|
||||
register_action(editor, window, Editor::select_to_beginning);
|
||||
register_action(editor, window, Editor::select_to_end);
|
||||
register_action(editor, window, Editor::select_all);
|
||||
@@ -1694,7 +1691,7 @@ impl EditorElement {
|
||||
let pos_y = content_origin.y
|
||||
+ line_height * (row.0 as f32 - scroll_pixel_position.y / line_height);
|
||||
|
||||
let window_ix = row.0.saturating_sub(start_row.0) as usize;
|
||||
let window_ix = row.minus(start_row) as usize;
|
||||
let pos_x = {
|
||||
let crease_trailer_layout = &crease_trailers[window_ix];
|
||||
let line_layout = &line_layouts[window_ix];
|
||||
@@ -1727,7 +1724,7 @@ impl EditorElement {
|
||||
.h(line_height)
|
||||
.w_full()
|
||||
.px_1()
|
||||
.rounded_sm()
|
||||
.rounded_xs()
|
||||
.opacity(opacity)
|
||||
.bg(severity_to_color(&diagnostic_to_render.severity)
|
||||
.color(cx)
|
||||
@@ -2720,7 +2717,7 @@ impl EditorElement {
|
||||
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
|
||||
.pl_0p5()
|
||||
.pr_5()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.shadow_md()
|
||||
.border_1()
|
||||
.map(|div| {
|
||||
@@ -2744,7 +2741,7 @@ impl EditorElement {
|
||||
header.child(
|
||||
div()
|
||||
.hover(|style| style.bg(colors.element_selected))
|
||||
.rounded_sm()
|
||||
.rounded_xs()
|
||||
.child(
|
||||
ButtonLike::new("toggle-buffer-fold")
|
||||
.style(ui::ButtonStyle::Transparent)
|
||||
@@ -4135,74 +4132,46 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
let mut paint_highlight = |highlight_row_start: DisplayRow,
|
||||
highlight_row_end: DisplayRow,
|
||||
highlight: crate::LineHighlight,
|
||||
edges| {
|
||||
let origin = point(
|
||||
layout.hitbox.origin.x,
|
||||
layout.hitbox.origin.y
|
||||
+ (highlight_row_start.as_f32() - scroll_top)
|
||||
* layout.position_map.line_height,
|
||||
);
|
||||
let size = size(
|
||||
layout.hitbox.size.width,
|
||||
layout.position_map.line_height
|
||||
* highlight_row_end.next_row().minus(highlight_row_start) as f32,
|
||||
);
|
||||
let mut quad = fill(Bounds { origin, size }, highlight.background);
|
||||
if let Some(border_color) = highlight.border {
|
||||
quad.border_color = border_color;
|
||||
quad.border_widths = edges
|
||||
}
|
||||
window.paint_quad(quad);
|
||||
};
|
||||
let mut paint_highlight =
|
||||
|highlight_row_start: DisplayRow, highlight_row_end: DisplayRow, color| {
|
||||
let origin = point(
|
||||
layout.hitbox.origin.x,
|
||||
layout.hitbox.origin.y
|
||||
+ (highlight_row_start.as_f32() - scroll_top)
|
||||
* layout.position_map.line_height,
|
||||
);
|
||||
let size = size(
|
||||
layout.hitbox.size.width,
|
||||
layout.position_map.line_height
|
||||
* highlight_row_end.next_row().minus(highlight_row_start) as f32,
|
||||
);
|
||||
window.paint_quad(fill(Bounds { origin, size }, color));
|
||||
};
|
||||
|
||||
let mut current_paint: Option<(LineHighlight, Range<DisplayRow>, Edges<Pixels>)> =
|
||||
None;
|
||||
let mut current_paint: Option<(gpui::Background, Range<DisplayRow>)> = None;
|
||||
for (&new_row, &new_background) in &layout.highlighted_rows {
|
||||
match &mut current_paint {
|
||||
Some((current_background, current_range, mut edges)) => {
|
||||
Some((current_background, current_range)) => {
|
||||
let current_background = *current_background;
|
||||
let new_range_started = current_background != new_background
|
||||
|| current_range.end.next_row() != new_row;
|
||||
if new_range_started {
|
||||
if current_range.end.next_row() == new_row {
|
||||
edges.bottom = px(0.);
|
||||
};
|
||||
paint_highlight(
|
||||
current_range.start,
|
||||
current_range.end,
|
||||
current_background,
|
||||
edges,
|
||||
);
|
||||
let edges = Edges {
|
||||
top: if current_range.end.next_row() != new_row {
|
||||
px(1.)
|
||||
} else {
|
||||
px(0.)
|
||||
},
|
||||
bottom: px(1.),
|
||||
..Default::default()
|
||||
};
|
||||
current_paint = Some((new_background, new_row..new_row, edges));
|
||||
current_paint = Some((new_background, new_row..new_row));
|
||||
continue;
|
||||
} else {
|
||||
current_range.end = current_range.end.next_row();
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let edges = Edges {
|
||||
top: px(1.),
|
||||
bottom: px(1.),
|
||||
..Default::default()
|
||||
};
|
||||
current_paint = Some((new_background, new_row..new_row, edges))
|
||||
}
|
||||
None => current_paint = Some((new_background, new_row..new_row)),
|
||||
};
|
||||
}
|
||||
if let Some((color, range, edges)) = current_paint {
|
||||
paint_highlight(range.start, range.end, color, edges);
|
||||
if let Some((color, range)) = current_paint {
|
||||
paint_highlight(range.start, range.end, color);
|
||||
}
|
||||
|
||||
let scroll_left =
|
||||
@@ -4378,6 +4347,8 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
fn paint_gutter_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
|
||||
let is_light = cx.theme().appearance().is_light();
|
||||
|
||||
if layout.display_hunks.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -4438,7 +4409,14 @@ impl EditorElement {
|
||||
}),
|
||||
};
|
||||
|
||||
if let Some((hunk_bounds, background_color, corner_radii, _)) = hunk_to_paint {
|
||||
if let Some((hunk_bounds, mut background_color, corner_radii, secondary_status)) =
|
||||
hunk_to_paint
|
||||
{
|
||||
if secondary_status.has_secondary_hunk() {
|
||||
background_color =
|
||||
background_color.opacity(if is_light { 0.2 } else { 0.32 });
|
||||
}
|
||||
|
||||
// Flatten the background color with the editor color to prevent
|
||||
// elements below transparent hunks from showing through
|
||||
let flattened_background_color = cx
|
||||
@@ -6756,6 +6734,10 @@ impl Element for EditorElement {
|
||||
.update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
|
||||
|
||||
let is_light = cx.theme().appearance().is_light();
|
||||
let use_pattern = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.hunk_style
|
||||
.map_or(false, |style| matches!(style, GitHunkStyleSetting::Pattern));
|
||||
|
||||
for (ix, row_info) in row_infos.iter().enumerate() {
|
||||
let Some(diff_status) = row_info.diff_status else {
|
||||
@@ -6776,27 +6758,25 @@ impl Element for EditorElement {
|
||||
let unstaged = diff_status.has_secondary_hunk();
|
||||
let hunk_opacity = if is_light { 0.16 } else { 0.12 };
|
||||
|
||||
let staged_highlight = LineHighlight {
|
||||
background: (background_color.opacity(if is_light {
|
||||
let staged_background =
|
||||
solid_background(background_color.opacity(hunk_opacity));
|
||||
let unstaged_background = if use_pattern {
|
||||
pattern_slash(
|
||||
background_color.opacity(hunk_opacity),
|
||||
window.rem_size().0 * 1.125, // ~18 by default
|
||||
)
|
||||
} else {
|
||||
solid_background(background_color.opacity(if is_light {
|
||||
0.08
|
||||
} else {
|
||||
0.06
|
||||
0.04
|
||||
}))
|
||||
.into(),
|
||||
border: Some(if is_light {
|
||||
background_color.opacity(0.48)
|
||||
} else {
|
||||
background_color.opacity(0.36)
|
||||
}),
|
||||
};
|
||||
|
||||
let unstaged_highlight =
|
||||
solid_background(background_color.opacity(hunk_opacity)).into();
|
||||
|
||||
let background = if unstaged {
|
||||
unstaged_highlight
|
||||
unstaged_background
|
||||
} else {
|
||||
staged_highlight
|
||||
staged_background
|
||||
};
|
||||
|
||||
highlighted_rows
|
||||
@@ -7645,7 +7625,7 @@ pub struct EditorLayout {
|
||||
indent_guides: Option<Vec<IndentGuideLayout>>,
|
||||
visible_display_row_range: Range<DisplayRow>,
|
||||
active_rows: BTreeMap<DisplayRow, bool>,
|
||||
highlighted_rows: BTreeMap<DisplayRow, LineHighlight>,
|
||||
highlighted_rows: BTreeMap<DisplayRow, gpui::Background>,
|
||||
line_elements: SmallVec<[AnyElement; 1]>,
|
||||
line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
|
||||
display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
|
||||
@@ -8817,16 +8797,14 @@ fn diff_hunk_controls(
|
||||
.h(line_height)
|
||||
.mr_1()
|
||||
.gap_1()
|
||||
.px_0p5()
|
||||
.px_1()
|
||||
.pb_1()
|
||||
.border_x_1()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_b_lg()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.gap_1()
|
||||
.occlude()
|
||||
.shadow_md()
|
||||
.child(if status.has_secondary_hunk() {
|
||||
Button::new(("stage", row as u64), "Stage")
|
||||
.alpha(if status.is_pending() { 0.66 } else { 1.0 })
|
||||
@@ -8883,7 +8861,7 @@ fn diff_hunk_controls(
|
||||
})
|
||||
})
|
||||
.child(
|
||||
Button::new("restore", "Restore")
|
||||
Button::new("discard", "Restore")
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |window, cx| {
|
||||
@@ -8935,7 +8913,7 @@ fn diff_hunk_controls(
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let position =
|
||||
hunk_range.end.to_point(&snapshot.buffer_snapshot);
|
||||
editor.go_to_hunk_before_or_after_position(
|
||||
editor.go_to_hunk_after_or_before_position(
|
||||
&snapshot,
|
||||
position,
|
||||
Direction::Next,
|
||||
@@ -8971,7 +8949,7 @@ fn diff_hunk_controls(
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let point =
|
||||
hunk_range.start.to_point(&snapshot.buffer_snapshot);
|
||||
editor.go_to_hunk_before_or_after_position(
|
||||
editor.go_to_hunk_after_or_before_position(
|
||||
&snapshot,
|
||||
point,
|
||||
Direction::Prev,
|
||||
|
||||
@@ -370,6 +370,7 @@ impl GitBlame {
|
||||
async move {
|
||||
let Some(Blame {
|
||||
entries,
|
||||
permalinks,
|
||||
messages,
|
||||
remote_url,
|
||||
}) = blame.await?
|
||||
@@ -378,8 +379,13 @@ impl GitBlame {
|
||||
};
|
||||
|
||||
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
|
||||
let commit_details =
|
||||
parse_commit_messages(messages, remote_url, provider_registry).await;
|
||||
let commit_details = parse_commit_messages(
|
||||
messages,
|
||||
remote_url,
|
||||
&permalinks,
|
||||
provider_registry,
|
||||
)
|
||||
.await;
|
||||
|
||||
anyhow::Ok(Some((entries, commit_details)))
|
||||
}
|
||||
@@ -471,6 +477,7 @@ fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree
|
||||
async fn parse_commit_messages(
|
||||
messages: impl IntoIterator<Item = (Oid, String)>,
|
||||
remote_url: Option<String>,
|
||||
deprecated_permalinks: &HashMap<Oid, Url>,
|
||||
provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
) -> HashMap<Oid, ParsedCommitMessage> {
|
||||
let mut commit_details = HashMap::default();
|
||||
@@ -488,7 +495,11 @@ async fn parse_commit_messages(
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
|
||||
// now do the parsing. This is here for backwards compatibility, so that
|
||||
// when an old peer sends a client no `parsed_remote_url` but `deprecated_permalinks`,
|
||||
// we fall back to that.
|
||||
deprecated_permalinks.get(&oid).cloned()
|
||||
};
|
||||
|
||||
let remote = parsed_remote_url
|
||||
|
||||
@@ -241,8 +241,10 @@ impl Editor {
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
return self.navigate_to_hover_links(None, links, modifiers.alt, window, cx);
|
||||
let navigate_task =
|
||||
self.navigate_to_hover_links(None, links, modifiers.alt, window, cx);
|
||||
self.select(SelectPhase::End, window, cx);
|
||||
return navigate_task;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +260,7 @@ impl Editor {
|
||||
cx,
|
||||
);
|
||||
|
||||
if point.as_valid().is_some() {
|
||||
let navigate_task = if point.as_valid().is_some() {
|
||||
if modifiers.shift {
|
||||
self.go_to_type_definition(&GoToTypeDefinition, window, cx)
|
||||
} else {
|
||||
@@ -266,7 +268,9 @@ impl Editor {
|
||||
}
|
||||
} else {
|
||||
Task::ready(Ok(Navigated::No))
|
||||
}
|
||||
};
|
||||
self.select(SelectPhase::End, window, cx);
|
||||
return navigate_task;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -618,12 +618,12 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: { cx.theme().players().local().selection },
|
||||
|
||||
heading: StyleRefinement::default()
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.text_base()
|
||||
.mt(rems(1.))
|
||||
.mb_0(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -622,11 +622,8 @@ impl Item for Editor {
|
||||
ItemSettings::get_global(cx)
|
||||
.file_icons
|
||||
.then(|| {
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).project_path(cx))
|
||||
.and_then(|path| FileIcons::get_icon(path.path.as_ref(), cx))
|
||||
path_for_buffer(&self.buffer, 0, true, cx)
|
||||
.and_then(|path| FileIcons::get_icon(path.as_ref(), cx))
|
||||
})
|
||||
.flatten()
|
||||
.map(Icon::from_path)
|
||||
@@ -1604,11 +1601,13 @@ impl SearchableItem for Editor {
|
||||
|
||||
fn active_match_index(
|
||||
&mut self,
|
||||
direction: Direction,
|
||||
matches: &[Range<Anchor>],
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<usize> {
|
||||
active_match_index(
|
||||
direction,
|
||||
matches,
|
||||
&self.selections.newest_anchor().head(),
|
||||
&self.buffer().read(cx).snapshot(cx),
|
||||
@@ -1621,6 +1620,7 @@ impl SearchableItem for Editor {
|
||||
}
|
||||
|
||||
pub fn active_match_index(
|
||||
direction: Direction,
|
||||
ranges: &[Range<Anchor>],
|
||||
cursor: &Anchor,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
@@ -1628,7 +1628,7 @@ pub fn active_match_index(
|
||||
if ranges.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match ranges.binary_search_by(|probe| {
|
||||
let r = ranges.binary_search_by(|probe| {
|
||||
if probe.end.cmp(cursor, buffer).is_lt() {
|
||||
Ordering::Less
|
||||
} else if probe.start.cmp(cursor, buffer).is_gt() {
|
||||
@@ -1636,8 +1636,15 @@ pub fn active_match_index(
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
}) {
|
||||
Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
|
||||
});
|
||||
match direction {
|
||||
Direction::Prev => match r {
|
||||
Ok(i) => Some(i),
|
||||
Err(i) => Some(i.saturating_sub(1)),
|
||||
},
|
||||
Direction::Next => match r {
|
||||
Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
616
crates/editor/src/jsx_tag_auto_close.rs
Normal file
616
crates/editor/src/jsx_tag_auto_close.rs
Normal file
@@ -0,0 +1,616 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::{Context, Entity, Window};
|
||||
use multi_buffer::{MultiBuffer, ToOffset};
|
||||
use std::ops::Range;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use language::{BufferSnapshot, JsxTagAutoCloseConfig, Node};
|
||||
use text::{Anchor, OffsetRangeExt as _};
|
||||
|
||||
use crate::Editor;
|
||||
|
||||
pub struct JsxTagCompletionState {
|
||||
edit_index: usize,
|
||||
open_tag_range: Range<usize>,
|
||||
}
|
||||
|
||||
/// Index of the named child within an open or close tag
|
||||
/// that corresponds to the tag name
|
||||
/// Note that this is not configurable, i.e. we assume the first
|
||||
/// named child of a tag node is the tag name
|
||||
const TS_NODE_TAG_NAME_CHILD_INDEX: usize = 0;
|
||||
|
||||
/// Maximum number of parent elements to walk back when checking if an open tag
|
||||
/// is already closed.
|
||||
///
|
||||
/// See the comment in `generate_auto_close_edits` for more details
|
||||
const ALREADY_CLOSED_PARENT_ELEMENT_WALK_BACK_LIMIT: usize = 2;
|
||||
|
||||
pub(crate) fn should_auto_close(
|
||||
buffer: &BufferSnapshot,
|
||||
edited_ranges: &[Range<usize>],
|
||||
config: &JsxTagAutoCloseConfig,
|
||||
) -> Option<Vec<JsxTagCompletionState>> {
|
||||
let mut to_auto_edit = vec![];
|
||||
for (index, edited_range) in edited_ranges.iter().enumerate() {
|
||||
let text = buffer
|
||||
.text_for_range(edited_range.clone())
|
||||
.collect::<String>();
|
||||
if !text.ends_with(">") {
|
||||
continue;
|
||||
}
|
||||
let Some(layer) = buffer.smallest_syntax_layer_containing(edited_range.clone()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(node) = layer
|
||||
.node()
|
||||
.named_descendant_for_byte_range(edited_range.start, edited_range.end)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let mut jsx_open_tag_node = node;
|
||||
if node.grammar_name() != config.open_tag_node_name {
|
||||
if let Some(parent) = node.parent() {
|
||||
if parent.grammar_name() == config.open_tag_node_name {
|
||||
jsx_open_tag_node = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
if jsx_open_tag_node.grammar_name() != config.open_tag_node_name {
|
||||
continue;
|
||||
}
|
||||
|
||||
let first_two_chars: Option<[char; 2]> = {
|
||||
let mut chars = buffer
|
||||
.text_for_range(jsx_open_tag_node.byte_range())
|
||||
.flat_map(|chunk| chunk.chars());
|
||||
if let (Some(c1), Some(c2)) = (chars.next(), chars.next()) {
|
||||
Some([c1, c2])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(chars) = first_two_chars {
|
||||
if chars[0] != '<' {
|
||||
continue;
|
||||
}
|
||||
if chars[1] == '!' || chars[1] == '/' {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
to_auto_edit.push(JsxTagCompletionState {
|
||||
edit_index: index,
|
||||
open_tag_range: jsx_open_tag_node.byte_range(),
|
||||
});
|
||||
}
|
||||
if to_auto_edit.is_empty() {
|
||||
return None;
|
||||
} else {
|
||||
return Some(to_auto_edit);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate_auto_close_edits(
|
||||
buffer: &BufferSnapshot,
|
||||
ranges: &[Range<usize>],
|
||||
config: &JsxTagAutoCloseConfig,
|
||||
state: Vec<JsxTagCompletionState>,
|
||||
) -> Result<Vec<(Range<Anchor>, String)>> {
|
||||
let mut edits = Vec::with_capacity(state.len());
|
||||
for auto_edit in state {
|
||||
let edited_range = ranges[auto_edit.edit_index].clone();
|
||||
let Some(layer) = buffer.smallest_syntax_layer_containing(edited_range.clone()) else {
|
||||
continue;
|
||||
};
|
||||
let layer_root_node = layer.node();
|
||||
let Some(open_tag) = layer_root_node.descendant_for_byte_range(
|
||||
auto_edit.open_tag_range.start,
|
||||
auto_edit.open_tag_range.end,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
assert!(open_tag.kind() == config.open_tag_node_name);
|
||||
let tag_name = open_tag
|
||||
.named_child(TS_NODE_TAG_NAME_CHILD_INDEX)
|
||||
.filter(|node| node.kind() == config.tag_name_node_name)
|
||||
.map_or("".to_string(), |node| {
|
||||
buffer.text_for_range(node.byte_range()).collect::<String>()
|
||||
});
|
||||
|
||||
/*
|
||||
* Naive check to see if the tag is already closed
|
||||
* Essentially all we do is count the number of open and close tags
|
||||
* with the same tag name as the open tag just entered by the user
|
||||
* The search is limited to some scope determined by
|
||||
* `ALREADY_CLOSED_PARENT_ELEMENT_WALK_BACK_LIMIT`
|
||||
*
|
||||
* The limit is preferable to walking up the tree until we find a non-tag node,
|
||||
* and then checking the entire tree, as this is unnecessarily expensive, and
|
||||
* risks false positives
|
||||
* eg. a `</div>` tag without a corresponding opening tag exists 25 lines away
|
||||
* and the user typed in `<div>`, intuitively we still want to auto-close it because
|
||||
* the other `</div>` tag is almost certainly not supposed to be the closing tag for the
|
||||
* current element
|
||||
*
|
||||
* We have to walk up the tree some amount because tree-sitters error correction is not
|
||||
* designed to handle this case, and usually does not represent the tree structure
|
||||
* in the way we might expect,
|
||||
*
|
||||
* We half to walk up the tree until we hit an element with a different open tag name (`doing_deep_search == true`)
|
||||
* because tree-sitter may pair the new open tag with the root of the tree's closing tag leaving the
|
||||
* root's opening tag unclosed.
|
||||
* e.g
|
||||
* ```
|
||||
* <div>
|
||||
* <div>|cursor here|
|
||||
* </div>
|
||||
* ```
|
||||
* in Astro/vue/svelte tree-sitter represented the tree as
|
||||
* (
|
||||
* (jsx_element
|
||||
* (jsx_opening_element
|
||||
* "<div>")
|
||||
* )
|
||||
* (jsx_element
|
||||
* (jsx_opening_element
|
||||
* "<div>") // <- cursor is here
|
||||
* (jsx_closing_element
|
||||
* "</div>")
|
||||
* )
|
||||
* )
|
||||
* so if we only walked to the first `jsx_element` node,
|
||||
* we would mistakenly identify the div entered by the
|
||||
* user as already being closed, despite this clearly
|
||||
* being false
|
||||
*
|
||||
* The errors with the tree-sitter tree caused by error correction,
|
||||
* are also why the naive algorithm was chosen, as the alternative
|
||||
* approach would be to maintain or construct a full parse tree (like tree-sitter)
|
||||
* that better represents errors in a way that we can simply check
|
||||
* the enclosing scope of the entered tag for a closing tag
|
||||
* This is far more complex and expensive, and was deemed impractical
|
||||
* given that the naive algorithm is sufficient in the majority of cases.
|
||||
*/
|
||||
{
|
||||
let tag_node_name_equals = |node: &Node, tag_name_node_name: &str, name: &str| {
|
||||
let is_empty = name.len() == 0;
|
||||
if let Some(node_name) = node.named_child(TS_NODE_TAG_NAME_CHILD_INDEX) {
|
||||
if node_name.kind() != tag_name_node_name {
|
||||
return is_empty;
|
||||
}
|
||||
let range = node_name.byte_range();
|
||||
return buffer.text_for_range(range).equals_str(name);
|
||||
}
|
||||
return is_empty;
|
||||
};
|
||||
|
||||
let tree_root_node = {
|
||||
let mut ancestors = Vec::with_capacity(
|
||||
// estimate of max, not based on any data,
|
||||
// but trying to avoid excessive reallocation
|
||||
16,
|
||||
);
|
||||
ancestors.push(layer_root_node);
|
||||
let mut cur = layer_root_node;
|
||||
// walk down the tree until we hit the open tag
|
||||
// note: this is what node.parent() does internally
|
||||
while let Some(descendant) = cur.child_with_descendant(open_tag) {
|
||||
if descendant == open_tag {
|
||||
break;
|
||||
}
|
||||
ancestors.push(descendant);
|
||||
cur = descendant;
|
||||
}
|
||||
|
||||
assert!(ancestors.len() > 0);
|
||||
|
||||
let mut tree_root_node = open_tag;
|
||||
|
||||
let mut parent_element_node_count = 0;
|
||||
let mut doing_deep_search = false;
|
||||
|
||||
for &ancestor in ancestors.iter().rev() {
|
||||
tree_root_node = ancestor;
|
||||
let is_element = ancestor.kind() == config.jsx_element_node_name;
|
||||
let is_error = ancestor.is_error();
|
||||
if is_error || !is_element {
|
||||
break;
|
||||
}
|
||||
if is_element {
|
||||
let is_first = parent_element_node_count == 0;
|
||||
if !is_first {
|
||||
let has_open_tag_with_same_tag_name = ancestor
|
||||
.named_child(0)
|
||||
.filter(|n| n.kind() == config.open_tag_node_name)
|
||||
.map_or(false, |element_open_tag_node| {
|
||||
tag_node_name_equals(
|
||||
&element_open_tag_node,
|
||||
&config.tag_name_node_name,
|
||||
&tag_name,
|
||||
)
|
||||
});
|
||||
if has_open_tag_with_same_tag_name {
|
||||
doing_deep_search = true;
|
||||
} else if doing_deep_search {
|
||||
break;
|
||||
}
|
||||
}
|
||||
parent_element_node_count += 1;
|
||||
if !doing_deep_search
|
||||
&& parent_element_node_count
|
||||
>= ALREADY_CLOSED_PARENT_ELEMENT_WALK_BACK_LIMIT
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
tree_root_node
|
||||
};
|
||||
|
||||
let mut unclosed_open_tag_count: i32 = 0;
|
||||
|
||||
let mut cursor = layer_root_node.walk();
|
||||
|
||||
let mut stack = Vec::with_capacity(tree_root_node.descendant_count());
|
||||
stack.extend(tree_root_node.children(&mut cursor));
|
||||
|
||||
let mut has_erroneous_close_tag = false;
|
||||
let mut erroneous_close_tag_node_name = "";
|
||||
let mut erroneous_close_tag_name_node_name = "";
|
||||
if let Some(name) = config.erroneous_close_tag_node_name.as_deref() {
|
||||
has_erroneous_close_tag = true;
|
||||
erroneous_close_tag_node_name = name;
|
||||
erroneous_close_tag_name_node_name = config
|
||||
.erroneous_close_tag_name_node_name
|
||||
.as_deref()
|
||||
.unwrap_or(&config.tag_name_node_name);
|
||||
}
|
||||
|
||||
let is_after_open_tag = |node: &Node| {
|
||||
return node.start_byte() < open_tag.start_byte()
|
||||
&& node.end_byte() < open_tag.start_byte();
|
||||
};
|
||||
|
||||
// perf: use cursor for more efficient traversal
|
||||
// if child -> go to child
|
||||
// else if next sibling -> go to next sibling
|
||||
// else -> go to parent
|
||||
// if parent == tree_root_node -> break
|
||||
while let Some(node) = stack.pop() {
|
||||
let kind = node.kind();
|
||||
if kind == config.open_tag_node_name {
|
||||
if tag_node_name_equals(&node, &config.tag_name_node_name, &tag_name) {
|
||||
unclosed_open_tag_count += 1;
|
||||
}
|
||||
} else if kind == config.close_tag_node_name {
|
||||
if tag_node_name_equals(&node, &config.tag_name_node_name, &tag_name) {
|
||||
unclosed_open_tag_count -= 1;
|
||||
}
|
||||
} else if has_erroneous_close_tag && kind == erroneous_close_tag_node_name {
|
||||
if tag_node_name_equals(&node, erroneous_close_tag_name_node_name, &tag_name) {
|
||||
if !is_after_open_tag(&node) {
|
||||
unclosed_open_tag_count -= 1;
|
||||
}
|
||||
}
|
||||
} else if kind == config.jsx_element_node_name {
|
||||
// perf: filter only open,close,element,erroneous nodes
|
||||
stack.extend(node.children(&mut cursor));
|
||||
}
|
||||
}
|
||||
|
||||
if unclosed_open_tag_count <= 0 {
|
||||
// skip if already closed
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let edit_anchor = buffer.anchor_after(edited_range.end);
|
||||
let edit_range = edit_anchor..edit_anchor;
|
||||
edits.push((edit_range, format!("</{}>", tag_name)));
|
||||
}
|
||||
return Ok(edits);
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_enabled_in_any_buffer(
|
||||
editor: &mut Editor,
|
||||
multi_buffer: &Entity<MultiBuffer>,
|
||||
cx: &Context<Editor>,
|
||||
) {
|
||||
editor.jsx_tag_auto_close_enabled_in_any_buffer = {
|
||||
let multi_buffer = multi_buffer.read(cx);
|
||||
let mut found_enabled = false;
|
||||
multi_buffer.for_each_buffer(|buffer| {
|
||||
let buffer = buffer.read(cx);
|
||||
let snapshot = buffer.snapshot();
|
||||
for syntax_layer in snapshot.syntax_layers() {
|
||||
let language = syntax_layer.language;
|
||||
if language.config().jsx_tag_auto_close.is_none() {
|
||||
continue;
|
||||
}
|
||||
let language_settings = language::language_settings::language_settings(
|
||||
Some(language.name()),
|
||||
snapshot.file(),
|
||||
cx,
|
||||
);
|
||||
if language_settings.jsx_tag_auto_close.enabled {
|
||||
found_enabled = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
found_enabled
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) type InitialBufferVersionsMap = HashMap<language::BufferId, clock::Global>;
|
||||
|
||||
pub(crate) fn construct_initial_buffer_versions_map<
|
||||
D: ToOffset + Copy,
|
||||
_S: Into<std::sync::Arc<str>>,
|
||||
>(
|
||||
editor: &Editor,
|
||||
edits: &[(Range<D>, _S)],
|
||||
cx: &Context<Editor>,
|
||||
) -> InitialBufferVersionsMap {
|
||||
let mut initial_buffer_versions = InitialBufferVersionsMap::default();
|
||||
|
||||
if !editor.jsx_tag_auto_close_enabled_in_any_buffer {
|
||||
return initial_buffer_versions;
|
||||
}
|
||||
|
||||
for (edit_range, _) in edits {
|
||||
let edit_range_buffer = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.excerpt_containing(edit_range.end, cx)
|
||||
.map(|e| e.1);
|
||||
if let Some(buffer) = edit_range_buffer {
|
||||
let (buffer_id, buffer_version) =
|
||||
buffer.read_with(cx, |buffer, _| (buffer.remote_id(), buffer.version.clone()));
|
||||
initial_buffer_versions.insert(buffer_id, buffer_version);
|
||||
}
|
||||
}
|
||||
return initial_buffer_versions;
|
||||
}
|
||||
|
||||
pub(crate) fn handle_from(
|
||||
editor: &Editor,
|
||||
initial_buffer_versions: InitialBufferVersionsMap,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
if !editor.jsx_tag_auto_close_enabled_in_any_buffer {
|
||||
return;
|
||||
}
|
||||
|
||||
struct JsxAutoCloseEditContext {
|
||||
buffer: Entity<language::Buffer>,
|
||||
config: language::JsxTagAutoCloseConfig,
|
||||
edits: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
let mut edit_contexts =
|
||||
HashMap::<(language::BufferId, language::LanguageId), JsxAutoCloseEditContext>::default();
|
||||
|
||||
for (buffer_id, buffer_version_initial) in initial_buffer_versions {
|
||||
let Some(buffer) = editor.buffer.read(cx).buffer(buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
for edit in buffer.read(cx).edits_since(&buffer_version_initial) {
|
||||
let Some(language) = snapshot.language_at(edit.new.end) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(config) = language.config().jsx_tag_auto_close.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let language_settings = snapshot.settings_at(edit.new.end, cx);
|
||||
if !language_settings.jsx_tag_auto_close.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
edit_contexts
|
||||
.entry((snapshot.remote_id(), language.id()))
|
||||
.or_insert_with(|| JsxAutoCloseEditContext {
|
||||
buffer: buffer.clone(),
|
||||
config: config.clone(),
|
||||
edits: vec![],
|
||||
})
|
||||
.edits
|
||||
.push(edit.new);
|
||||
}
|
||||
}
|
||||
|
||||
for ((buffer_id, _), auto_close_context) in edit_contexts {
|
||||
let JsxAutoCloseEditContext {
|
||||
buffer,
|
||||
config: jsx_tag_auto_close_config,
|
||||
edits: edited_ranges,
|
||||
} = auto_close_context;
|
||||
|
||||
let (buffer_version_initial, mut buffer_parse_status_rx) =
|
||||
buffer.read_with(cx, |buffer, _| (buffer.version(), buffer.parse_status()));
|
||||
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
let Some(buffer_parse_status) = buffer_parse_status_rx.recv().await.ok() else {
|
||||
return Some(());
|
||||
};
|
||||
if buffer_parse_status == language::ParseStatus::Parsing {
|
||||
let Some(language::ParseStatus::Idle) = buffer_parse_status_rx.recv().await.ok()
|
||||
else {
|
||||
return Some(());
|
||||
};
|
||||
}
|
||||
|
||||
let buffer_snapshot = buffer.read_with(&cx, |buf, _| buf.snapshot()).ok()?;
|
||||
|
||||
let Some(edit_behavior_state) =
|
||||
should_auto_close(&buffer_snapshot, &edited_ranges, &jsx_tag_auto_close_config)
|
||||
else {
|
||||
return Some(());
|
||||
};
|
||||
|
||||
let ensure_no_edits_since_start = || -> Option<()> {
|
||||
// <div>wef,wefwef
|
||||
let has_edits_since_start = this
|
||||
.read_with(&cx, |this, cx| {
|
||||
this.buffer.read_with(cx, |buffer, cx| {
|
||||
buffer.buffer(buffer_id).map_or(true, |buffer| {
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
buffer.has_edits_since(&buffer_version_initial)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
if has_edits_since_start {
|
||||
Err(anyhow!(
|
||||
"Auto-close Operation Failed - Buffer has edits since start"
|
||||
))
|
||||
.log_err()?;
|
||||
}
|
||||
|
||||
Some(())
|
||||
};
|
||||
|
||||
ensure_no_edits_since_start()?;
|
||||
|
||||
let edits = cx
|
||||
.background_executor()
|
||||
.spawn({
|
||||
let buffer_snapshot = buffer_snapshot.clone();
|
||||
async move {
|
||||
generate_auto_close_edits(
|
||||
&buffer_snapshot,
|
||||
&edited_ranges,
|
||||
&jsx_tag_auto_close_config,
|
||||
edit_behavior_state,
|
||||
)
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let edits = edits
|
||||
.context("Auto-close Operation Failed - Failed to compute edits")
|
||||
.log_err()?;
|
||||
|
||||
if edits.is_empty() {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
// check again after awaiting background task before applying edits
|
||||
ensure_no_edits_since_start()?;
|
||||
|
||||
let multi_buffer_snapshot = this
|
||||
.read_with(&cx, |this, cx| {
|
||||
this.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx))
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let mut base_selections = Vec::new();
|
||||
let mut buffer_selection_map = HashMap::default();
|
||||
|
||||
{
|
||||
let selections = this
|
||||
.read_with(&cx, |this, _| this.selections.disjoint_anchors().clone())
|
||||
.ok()?;
|
||||
for selection in selections.iter() {
|
||||
let Some(selection_buffer_offset_head) =
|
||||
multi_buffer_snapshot.point_to_buffer_offset(selection.head())
|
||||
else {
|
||||
base_selections.push(selection.clone());
|
||||
continue;
|
||||
};
|
||||
let Some(selection_buffer_offset_tail) =
|
||||
multi_buffer_snapshot.point_to_buffer_offset(selection.tail())
|
||||
else {
|
||||
base_selections.push(selection.clone());
|
||||
continue;
|
||||
};
|
||||
|
||||
let is_entirely_in_buffer = selection_buffer_offset_head.0.remote_id()
|
||||
== buffer_id
|
||||
&& selection_buffer_offset_tail.0.remote_id() == buffer_id;
|
||||
if !is_entirely_in_buffer {
|
||||
base_selections.push(selection.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
let selection_buffer_offset_head = selection_buffer_offset_head.1;
|
||||
let selection_buffer_offset_tail = selection_buffer_offset_tail.1;
|
||||
buffer_selection_map.insert(
|
||||
(selection_buffer_offset_head, selection_buffer_offset_tail),
|
||||
(selection.clone(), None),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut any_selections_need_update = false;
|
||||
for edit in &edits {
|
||||
let edit_range_offset = edit.0.to_offset(&buffer_snapshot);
|
||||
if edit_range_offset.start != edit_range_offset.end {
|
||||
continue;
|
||||
}
|
||||
if let Some(selection) =
|
||||
buffer_selection_map.get_mut(&(edit_range_offset.start, edit_range_offset.end))
|
||||
{
|
||||
if selection.0.head().bias() != text::Bias::Right
|
||||
|| selection.0.tail().bias() != text::Bias::Right
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if selection.1.is_none() {
|
||||
any_selections_need_update = true;
|
||||
selection.1 = Some(
|
||||
selection
|
||||
.0
|
||||
.clone()
|
||||
.map(|anchor| multi_buffer_snapshot.anchor_before(anchor)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| {
|
||||
buffer.edit(edits, None, cx);
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
if any_selections_need_update {
|
||||
let multi_buffer_snapshot = this
|
||||
.read_with(&cx, |this, cx| {
|
||||
this.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx))
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
base_selections.extend(buffer_selection_map.values().map(|selection| {
|
||||
match &selection.1 {
|
||||
Some(left_biased_selection) => left_biased_selection.clone(),
|
||||
None => selection.0.clone(),
|
||||
}
|
||||
}));
|
||||
|
||||
let base_selections = base_selections
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
selection.map(|anchor| anchor.to_offset(&multi_buffer_snapshot))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
this.update_in(&mut cx, |this, window, cx| {
|
||||
this.change_selections_inner(None, false, window, cx, |s| {
|
||||
s.select(base_selections);
|
||||
});
|
||||
})
|
||||
.ok()?;
|
||||
}
|
||||
|
||||
Some(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
@@ -448,9 +448,7 @@ pub fn end_of_excerpt(
|
||||
if start.row() > DisplayRow(0) {
|
||||
*start.row_mut() -= 1;
|
||||
}
|
||||
start = map.clip_point(start, Bias::Left);
|
||||
*start.column_mut() = 0;
|
||||
start
|
||||
map.clip_point(start, Bias::Left)
|
||||
}
|
||||
Direction::Next => {
|
||||
let mut end = excerpt.end_anchor().to_display_point(&map);
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::Context as _;
|
||||
use gpui::{App, AppContext as _, Context, Entity, Window};
|
||||
use language::{Capability, Language};
|
||||
use multi_buffer::MultiBuffer;
|
||||
use project::lsp_ext_command::ExpandMacro;
|
||||
use project::lsp_store::{lsp_ext_command::ExpandMacro, rust_analyzer_ext::RUST_ANALYZER_NAME};
|
||||
use text::ToPointUtf16;
|
||||
|
||||
use crate::{
|
||||
@@ -12,8 +12,6 @@ use crate::{
|
||||
ExpandMacroRecursively, OpenDocs,
|
||||
};
|
||||
|
||||
const RUST_ANALYZER_NAME: &str = "rust-analyzer";
|
||||
|
||||
fn is_rust_language(language: &Language) -> bool {
|
||||
language.name() == "Rust".into()
|
||||
}
|
||||
@@ -131,7 +129,7 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu
|
||||
project.request_lsp(
|
||||
buffer,
|
||||
project::LanguageServerToQuery::Other(server_to_query),
|
||||
project::lsp_ext_command::OpenDocs { position },
|
||||
project::lsp_store::lsp_ext_command::OpenDocs { position },
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ collections.workspace = true
|
||||
env_logger.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
|
||||
@@ -5,6 +5,7 @@ use client::{Client, UserStore};
|
||||
use clock::RealSystemClock;
|
||||
use collections::BTreeMap;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use git::GitHostingProviderRegistry;
|
||||
use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, Entity};
|
||||
use http_client::{HttpClient, Method};
|
||||
use language::LanguageRegistry;
|
||||
@@ -273,7 +274,8 @@ async fn run_evaluation(
|
||||
let repos_dir = Path::new(EVAL_REPOS_DIR);
|
||||
let db_path = Path::new(EVAL_DB_PATH);
|
||||
let api_key = std::env::var("OPENAI_API_KEY").unwrap();
|
||||
let fs = Arc::new(RealFs::new(None)) as Arc<dyn Fs>;
|
||||
let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new());
|
||||
let fs = Arc::new(RealFs::new(git_hosting_provider_registry, None)) as Arc<dyn Fs>;
|
||||
let clock = Arc::new(RealSystemClock);
|
||||
let client = cx
|
||||
.update(|cx| {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use fs::Fs;
|
||||
use language::LanguageName;
|
||||
@@ -85,6 +85,61 @@ pub struct ExtensionManifest {
|
||||
pub indexed_docs_providers: BTreeMap<Arc<str>, IndexedDocsProviderEntry>,
|
||||
#[serde(default)]
|
||||
pub snippets: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub capabilities: Vec<ExtensionCapability>,
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
pub fn allow_exec(
|
||||
&self,
|
||||
desired_command: &str,
|
||||
desired_args: &[impl AsRef<str> + std::fmt::Debug],
|
||||
) -> Result<()> {
|
||||
let is_allowed = self.capabilities.iter().any(|capability| match capability {
|
||||
ExtensionCapability::ProcessExec { command, args } if command == desired_command => {
|
||||
for (ix, arg) in args.iter().enumerate() {
|
||||
if arg == "**" {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ix >= desired_args.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if arg != "*" && arg != desired_args[ix].as_ref() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if args.len() < desired_args.len() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
||||
if !is_allowed {
|
||||
bail!(
|
||||
"capability for process:exec {desired_command} {desired_args:?} was not listed in the extension manifest",
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A capability for an extension.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum ExtensionCapability {
|
||||
#[serde(rename = "process:exec")]
|
||||
ProcessExec {
|
||||
/// The command to execute.
|
||||
command: String,
|
||||
/// The arguments to pass to the command. Use `*` for a single wildcard argument.
|
||||
/// If the last element is `**`, then any trailing arguments are allowed.
|
||||
args: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
@@ -218,5 +273,104 @@ fn manifest_from_old_manifest(
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn extension_manifest() -> ExtensionManifest {
|
||||
ExtensionManifest {
|
||||
id: "test".into(),
|
||||
name: "Test".to_string(),
|
||||
version: "1.0.0".into(),
|
||||
schema_version: SchemaVersion::ZERO,
|
||||
description: None,
|
||||
repository: None,
|
||||
authors: vec![],
|
||||
lib: Default::default(),
|
||||
themes: vec![],
|
||||
icon_themes: vec![],
|
||||
languages: vec![],
|
||||
grammars: BTreeMap::default(),
|
||||
language_servers: BTreeMap::default(),
|
||||
context_servers: BTreeMap::default(),
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_exact_match() {
|
||||
let manifest = ExtensionManifest {
|
||||
capabilities: vec![ExtensionCapability::ProcessExec {
|
||||
command: "ls".to_string(),
|
||||
args: vec!["-la".to_string()],
|
||||
}],
|
||||
..extension_manifest()
|
||||
};
|
||||
|
||||
assert!(manifest.allow_exec("ls", &["-la"]).is_ok());
|
||||
assert!(manifest.allow_exec("ls", &["-l"]).is_err());
|
||||
assert!(manifest.allow_exec("pwd", &[] as &[&str]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_wildcard_arg() {
|
||||
let manifest = ExtensionManifest {
|
||||
capabilities: vec![ExtensionCapability::ProcessExec {
|
||||
command: "git".to_string(),
|
||||
args: vec!["*".to_string()],
|
||||
}],
|
||||
..extension_manifest()
|
||||
};
|
||||
|
||||
assert!(manifest.allow_exec("git", &["status"]).is_ok());
|
||||
assert!(manifest.allow_exec("git", &["commit"]).is_ok());
|
||||
assert!(manifest.allow_exec("git", &["status", "-s"]).is_err()); // too many args
|
||||
assert!(manifest.allow_exec("npm", &["install"]).is_err()); // wrong command
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_double_wildcard() {
|
||||
let manifest = ExtensionManifest {
|
||||
capabilities: vec![ExtensionCapability::ProcessExec {
|
||||
command: "cargo".to_string(),
|
||||
args: vec!["test".to_string(), "**".to_string()],
|
||||
}],
|
||||
..extension_manifest()
|
||||
};
|
||||
|
||||
assert!(manifest.allow_exec("cargo", &["test"]).is_ok());
|
||||
assert!(manifest.allow_exec("cargo", &["test", "--all"]).is_ok());
|
||||
assert!(manifest
|
||||
.allow_exec("cargo", &["test", "--all", "--no-fail-fast"])
|
||||
.is_ok());
|
||||
assert!(manifest.allow_exec("cargo", &["build"]).is_err()); // wrong first arg
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_mixed_wildcards() {
|
||||
let manifest = ExtensionManifest {
|
||||
capabilities: vec![ExtensionCapability::ProcessExec {
|
||||
command: "docker".to_string(),
|
||||
args: vec!["run".to_string(), "*".to_string(), "**".to_string()],
|
||||
}],
|
||||
..extension_manifest()
|
||||
};
|
||||
|
||||
assert!(manifest.allow_exec("docker", &["run", "nginx"]).is_ok());
|
||||
assert!(manifest.allow_exec("docker", &["run"]).is_err());
|
||||
assert!(manifest
|
||||
.allow_exec("docker", &["run", "ubuntu", "bash"])
|
||||
.is_ok());
|
||||
assert!(manifest
|
||||
.allow_exec("docker", &["run", "alpine", "sh", "-c", "echo hello"])
|
||||
.is_ok());
|
||||
assert!(manifest.allow_exec("docker", &["ps"]).is_err()); // wrong first arg
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +163,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: Vec::new(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
@@ -191,6 +192,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: Vec::new(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
@@ -356,6 +358,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: Vec::new(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
|
||||
@@ -592,6 +592,8 @@ impl process::Host for WasmState {
|
||||
command: process::Command,
|
||||
) -> wasmtime::Result<Result<process::Output, String>> {
|
||||
maybe!(async {
|
||||
self.manifest.allow_exec(&command.command, &command.args)?;
|
||||
|
||||
let output = util::command::new_smol_command(command.command.as_str())
|
||||
.args(&command.args)
|
||||
.envs(command.env)
|
||||
|
||||
@@ -40,7 +40,7 @@ impl RenderOnce for ExtensionCard {
|
||||
.bg(cx.theme().colors().elevated_surface_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.children(self.children)
|
||||
.when(self.overridden_by_dev_extension, |card| {
|
||||
card.child(
|
||||
|
||||
@@ -632,7 +632,7 @@ impl ExtensionsPage {
|
||||
.px_0p5()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.child(
|
||||
Label::new(label).size(LabelSize::XSmall),
|
||||
)
|
||||
|
||||
@@ -80,6 +80,11 @@ impl FeatureFlag for PredictEditsNonEagerModeFeatureFlag {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GitUiFeatureFlag;
|
||||
impl FeatureFlag for GitUiFeatureFlag {
|
||||
const NAME: &'static str = "git-ui";
|
||||
}
|
||||
|
||||
pub struct Remoting {}
|
||||
impl FeatureFlag for Remoting {
|
||||
const NAME: &'static str = "remoting";
|
||||
|
||||
@@ -470,7 +470,7 @@ impl Render for FeedbackModal {
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.p_2()
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(self.feedback_editor.clone()),
|
||||
)
|
||||
@@ -482,7 +482,7 @@ impl Render for FeedbackModal {
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.p_2()
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.border_color(if self.valid_email_address() {
|
||||
cx.theme().colors().border
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#[cfg(test)]
|
||||
mod file_finder_tests;
|
||||
#[cfg(test)]
|
||||
mod open_path_prompt_tests;
|
||||
|
||||
pub mod file_finder_settings;
|
||||
mod new_path_prompt;
|
||||
|
||||
@@ -436,8 +436,8 @@ impl PickerDelegate for NewPathDelegate {
|
||||
)
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
Some("Type a path...".into())
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
|
||||
"Type a path...".into()
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
|
||||
@@ -3,7 +3,7 @@ use fuzzy::StringMatchCandidate;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::DirectoryLister;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
path::{Path, PathBuf, MAIN_SEPARATOR_STR},
|
||||
sync::{
|
||||
atomic::{self, AtomicBool},
|
||||
Arc,
|
||||
@@ -38,14 +38,38 @@ impl OpenPathDelegate {
|
||||
should_dismiss: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn collect_match_candidates(&self) -> Vec<String> {
|
||||
if let Some(state) = self.directory_state.as_ref() {
|
||||
self.matches
|
||||
.iter()
|
||||
.filter_map(|&index| {
|
||||
state
|
||||
.match_candidates
|
||||
.get(index)
|
||||
.map(|candidate| candidate.path.string.clone())
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DirectoryState {
|
||||
path: String,
|
||||
match_candidates: Vec<StringMatchCandidate>,
|
||||
match_candidates: Vec<CandidateInfo>,
|
||||
error: Option<SharedString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CandidateInfo {
|
||||
path: StringMatchCandidate,
|
||||
is_dir: bool,
|
||||
}
|
||||
|
||||
impl OpenPathPrompt {
|
||||
pub(crate) fn register(
|
||||
workspace: &mut Workspace,
|
||||
@@ -93,8 +117,6 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// todo(windows)
|
||||
// Is this method woring correctly on Windows? This method uses `/` for path separator.
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
@@ -102,13 +124,26 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let lister = self.lister.clone();
|
||||
let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
|
||||
(query[..index].to_string(), query[index + 1..].to_string())
|
||||
let query_path = Path::new(&query);
|
||||
let last_item = query_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(&last_item) {
|
||||
(dir.to_string(), last_item)
|
||||
} else {
|
||||
(query, String::new())
|
||||
};
|
||||
if dir == "" {
|
||||
dir = "/".to_string();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
dir = "/".to_string();
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
dir = "C:\\".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
let query = if self
|
||||
@@ -134,12 +169,16 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.delegate.directory_state = Some(match paths {
|
||||
Ok(mut paths) => {
|
||||
paths.sort_by(|a, b| compare_paths((a, true), (b, true)));
|
||||
paths.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
|
||||
let match_candidates = paths
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, path)| {
|
||||
StringMatchCandidate::new(ix, &path.to_string_lossy())
|
||||
.map(|(ix, item)| CandidateInfo {
|
||||
path: StringMatchCandidate::new(
|
||||
ix,
|
||||
&item.path.to_string_lossy(),
|
||||
),
|
||||
is_dir: item.is_dir,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -178,7 +217,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
};
|
||||
|
||||
if !suffix.starts_with('.') {
|
||||
match_candidates.retain(|m| !m.string.starts_with('.'));
|
||||
match_candidates.retain(|m| !m.path.string.starts_with('.'));
|
||||
}
|
||||
|
||||
if suffix == "" {
|
||||
@@ -186,7 +225,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
this.delegate.matches.clear();
|
||||
this.delegate
|
||||
.matches
|
||||
.extend(match_candidates.iter().map(|m| m.id));
|
||||
.extend(match_candidates.iter().map(|m| m.path.id));
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
@@ -194,8 +233,9 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
return;
|
||||
}
|
||||
|
||||
let candidates = match_candidates.iter().map(|m| &m.path).collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
match_candidates.as_slice(),
|
||||
candidates.as_slice(),
|
||||
&suffix,
|
||||
false,
|
||||
100,
|
||||
@@ -217,7 +257,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
this.delegate.directory_state.as_ref().and_then(|d| {
|
||||
d.match_candidates
|
||||
.get(*m)
|
||||
.map(|c| !c.string.starts_with(&suffix))
|
||||
.map(|c| !c.path.string.starts_with(&suffix))
|
||||
}),
|
||||
*m,
|
||||
)
|
||||
@@ -239,7 +279,16 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
let m = self.matches.get(self.selected_index)?;
|
||||
let directory_state = self.directory_state.as_ref()?;
|
||||
let candidate = directory_state.match_candidates.get(*m)?;
|
||||
Some(format!("{}/{}", directory_state.path, candidate.string))
|
||||
Some(format!(
|
||||
"{}{}{}",
|
||||
directory_state.path,
|
||||
candidate.path.string,
|
||||
if candidate.is_dir {
|
||||
MAIN_SEPARATOR_STR
|
||||
} else {
|
||||
""
|
||||
}
|
||||
))
|
||||
})
|
||||
.unwrap_or(query),
|
||||
)
|
||||
@@ -260,7 +309,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
.resolve_tilde(&directory_state.path, cx)
|
||||
.as_ref(),
|
||||
)
|
||||
.join(&candidate.string);
|
||||
.join(&candidate.path.string);
|
||||
if let Some(tx) = self.tx.take() {
|
||||
tx.send(Some(vec![result])).ok();
|
||||
}
|
||||
@@ -294,21 +343,19 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(LabelLike::new().child(candidate.string.clone())),
|
||||
.child(LabelLike::new().child(candidate.path.string.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
let text = if let Some(error) = self.directory_state.as_ref().and_then(|s| s.error.clone())
|
||||
{
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
|
||||
if let Some(error) = self.directory_state.as_ref().and_then(|s| s.error.clone()) {
|
||||
error
|
||||
} else {
|
||||
"No such file or directory".into()
|
||||
};
|
||||
Some(text)
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
Arc::from("[directory/]filename.ext")
|
||||
Arc::from(format!("[directory{MAIN_SEPARATOR_STR}]filename.ext"))
|
||||
}
|
||||
}
|
||||
|
||||
324
crates/file_finder/src/open_path_prompt_tests.rs
Normal file
324
crates/file_finder/src/open_path_prompt_tests.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{AppContext, Entity, TestAppContext, VisualTestContext};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use ui::rems;
|
||||
use util::path;
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
use crate::OpenPathDelegate;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_path_prompt(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"a1": "A1",
|
||||
"a2": "A2",
|
||||
"a3": "A3",
|
||||
"dir1": {},
|
||||
"dir2": {
|
||||
"c": "C",
|
||||
"d1": "D1",
|
||||
"d2": "D2",
|
||||
"d3": "D3",
|
||||
"dir3": {},
|
||||
"dir4": {}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, cx) = build_open_path_prompt(project, cx);
|
||||
|
||||
let query = path!("/root");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
|
||||
|
||||
// If the query ends with a slash, the picker should show the contents of the directory.
|
||||
let query = path!("/root/");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
collect_match_candidates(&picker, cx),
|
||||
vec!["a1", "a2", "a3", "dir1", "dir2"]
|
||||
);
|
||||
|
||||
// Show candidates for the query "a".
|
||||
let query = path!("/root/a");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
collect_match_candidates(&picker, cx),
|
||||
vec!["a1", "a2", "a3"]
|
||||
);
|
||||
|
||||
// Show candidates for the query "d".
|
||||
let query = path!("/root/d");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
|
||||
|
||||
let query = path!("/root/dir2");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir2"]);
|
||||
|
||||
let query = path!("/root/dir2/");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
collect_match_candidates(&picker, cx),
|
||||
vec!["c", "d1", "d2", "d3", "dir3", "dir4"]
|
||||
);
|
||||
|
||||
// Show candidates for the query "d".
|
||||
let query = path!("/root/dir2/d");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
collect_match_candidates(&picker, cx),
|
||||
vec!["d1", "d2", "d3", "dir3", "dir4"]
|
||||
);
|
||||
|
||||
let query = path!("/root/dir2/di");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir3", "dir4"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"a": "A",
|
||||
"dir1": {},
|
||||
"dir2": {
|
||||
"c": "C",
|
||||
"d": "D",
|
||||
"dir3": {},
|
||||
"dir4": {}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, cx) = build_open_path_prompt(project, cx);
|
||||
|
||||
// Confirm completion for the query "/root", since it's a directory, it should add a trailing slash.
|
||||
let query = path!("/root");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/"));
|
||||
|
||||
// Confirm completion for the query "/root/", selecting the first candidate "a", since it's a file, it should not add a trailing slash.
|
||||
let query = path!("/root/");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/a"));
|
||||
|
||||
// Confirm completion for the query "/root/", selecting the second candidate "dir1", since it's a directory, it should add a trailing slash.
|
||||
let query = path!("/root/");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
confirm_completion(query, 1, &picker, cx),
|
||||
path!("/root/dir1/")
|
||||
);
|
||||
|
||||
let query = path!("/root/a");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/a"));
|
||||
|
||||
let query = path!("/root/d");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
confirm_completion(query, 1, &picker, cx),
|
||||
path!("/root/dir2/")
|
||||
);
|
||||
|
||||
let query = path!("/root/dir2");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
confirm_completion(query, 0, &picker, cx),
|
||||
path!("/root/dir2/")
|
||||
);
|
||||
|
||||
let query = path!("/root/dir2/");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
confirm_completion(query, 0, &picker, cx),
|
||||
path!("/root/dir2/c")
|
||||
);
|
||||
|
||||
let query = path!("/root/dir2/");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
confirm_completion(query, 2, &picker, cx),
|
||||
path!("/root/dir2/dir3/")
|
||||
);
|
||||
|
||||
let query = path!("/root/dir2/d");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
confirm_completion(query, 0, &picker, cx),
|
||||
path!("/root/dir2/d")
|
||||
);
|
||||
|
||||
let query = path!("/root/dir2/d");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
confirm_completion(query, 1, &picker, cx),
|
||||
path!("/root/dir2/dir3/")
|
||||
);
|
||||
|
||||
let query = path!("/root/dir2/di");
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
confirm_completion(query, 1, &picker, cx),
|
||||
path!("/root/dir2/dir4/")
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"a": "A",
|
||||
"dir1": {},
|
||||
"dir2": {}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, cx) = build_open_path_prompt(project, cx);
|
||||
|
||||
// Support both forward and backward slashes.
|
||||
let query = "C:/root/";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
collect_match_candidates(&picker, cx),
|
||||
vec!["a", "dir1", "dir2"]
|
||||
);
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), "C:/root/a");
|
||||
|
||||
let query = "C:\\root/";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
collect_match_candidates(&picker, cx),
|
||||
vec!["a", "dir1", "dir2"]
|
||||
);
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root/a");
|
||||
|
||||
let query = "C:\\root\\";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
collect_match_candidates(&picker, cx),
|
||||
vec!["a", "dir1", "dir2"]
|
||||
);
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root\\a");
|
||||
|
||||
// Confirm completion for the query "C:/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash.
|
||||
let query = "C:/root/d";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
|
||||
assert_eq!(confirm_completion(query, 1, &picker, cx), "C:/root/dir2\\");
|
||||
|
||||
let query = "C:\\root/d";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root/dir1\\");
|
||||
|
||||
let query = "C:\\root\\d";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
|
||||
assert_eq!(
|
||||
confirm_completion(query, 0, &picker, cx),
|
||||
"C:\\root\\dir1\\"
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||
cx.update(|cx| {
|
||||
let state = AppState::test(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
super::init(cx);
|
||||
editor::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
state
|
||||
})
|
||||
}
|
||||
|
||||
fn build_open_path_prompt(
|
||||
project: Entity<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<Picker<OpenPathDelegate>>, &mut VisualTestContext) {
|
||||
let (tx, _) = futures::channel::oneshot::channel();
|
||||
let lister = project::DirectoryLister::Project(project.clone());
|
||||
let delegate = OpenPathDelegate::new(tx, lister.clone());
|
||||
|
||||
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
|
||||
(
|
||||
workspace.update_in(cx, |_, window, cx| {
|
||||
cx.new(|cx| {
|
||||
let picker = Picker::uniform_list(delegate, window, cx)
|
||||
.width(rems(34.))
|
||||
.modal(false);
|
||||
let query = lister.default_query(cx);
|
||||
picker.set_query(query, window, cx);
|
||||
picker
|
||||
})
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
async fn insert_query(
|
||||
query: &str,
|
||||
picker: &Entity<Picker<OpenPathDelegate>>,
|
||||
cx: &mut VisualTestContext,
|
||||
) {
|
||||
picker
|
||||
.update_in(cx, |f, window, cx| {
|
||||
f.delegate.update_matches(query.to_string(), window, cx)
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
fn confirm_completion(
|
||||
query: &str,
|
||||
select: usize,
|
||||
picker: &Entity<Picker<OpenPathDelegate>>,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> String {
|
||||
picker
|
||||
.update_in(cx, |f, window, cx| {
|
||||
if f.delegate.selected_index() != select {
|
||||
f.delegate.set_selected_index(select, window, cx);
|
||||
}
|
||||
f.delegate.confirm_completion(query.to_string(), window, cx)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn collect_match_candidates(
|
||||
picker: &Entity<Picker<OpenPathDelegate>>,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> Vec<String> {
|
||||
picker.update(cx, |f, _| f.delegate.collect_match_candidates())
|
||||
}
|
||||
@@ -11,19 +11,18 @@ use collections::HashMap;
|
||||
use git::status::StatusCode;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use git::status::TrackedStatus;
|
||||
use git::GitHostingProviderRegistry;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use git::{repository::RepoPath, status::FileStatus};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use ashpd::desktop::trash;
|
||||
use std::borrow::Cow;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use std::collections::HashSet;
|
||||
#[cfg(unix)]
|
||||
use std::os::fd::AsFd;
|
||||
#[cfg(unix)]
|
||||
use std::os::fd::AsRawFd;
|
||||
use util::command::new_std_command;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
@@ -138,7 +137,6 @@ pub trait Fs: Send + Sync {
|
||||
|
||||
fn home_dir(&self) -> Option<PathBuf>;
|
||||
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
|
||||
fn git_init(&self, abs_work_directory: &Path, fallback_branch_name: String) -> Result<()>;
|
||||
fn is_fake(&self) -> bool;
|
||||
async fn is_case_sensitive(&self) -> Result<bool>;
|
||||
|
||||
@@ -249,6 +247,7 @@ impl From<MTime> for proto::Timestamp {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RealFs {
|
||||
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
git_binary_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
@@ -301,8 +300,14 @@ impl FileHandle for std::fs::File {
|
||||
pub struct RealWatcher {}
|
||||
|
||||
impl RealFs {
|
||||
pub fn new(git_binary_path: Option<PathBuf>) -> Self {
|
||||
Self { git_binary_path }
|
||||
pub fn new(
|
||||
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
git_binary_path: Option<PathBuf>,
|
||||
) -> Self {
|
||||
Self {
|
||||
git_hosting_provider_registry,
|
||||
git_binary_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,32 +770,10 @@ impl Fs for RealFs {
|
||||
Some(Arc::new(RealGitRepository::new(
|
||||
repo,
|
||||
self.git_binary_path.clone(),
|
||||
self.git_hosting_provider_registry.clone(),
|
||||
)))
|
||||
}
|
||||
|
||||
fn git_init(&self, abs_work_directory_path: &Path, fallback_branch_name: String) -> Result<()> {
|
||||
let config = new_std_command("git")
|
||||
.current_dir(abs_work_directory_path)
|
||||
.args(&["config", "--global", "--get", "init.defaultBranch"])
|
||||
.output()?;
|
||||
|
||||
let branch_name;
|
||||
|
||||
if config.status.success() && !config.stdout.is_empty() {
|
||||
branch_name = String::from_utf8_lossy(&config.stdout);
|
||||
} else {
|
||||
branch_name = Cow::Borrowed(fallback_branch_name.as_str());
|
||||
}
|
||||
|
||||
new_std_command("git")
|
||||
.current_dir(abs_work_directory_path)
|
||||
.args(&["init", "-b"])
|
||||
.arg(branch_name.trim())
|
||||
.output()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_fake(&self) -> bool {
|
||||
false
|
||||
}
|
||||
@@ -2101,14 +2084,6 @@ impl Fs for FakeFs {
|
||||
}
|
||||
}
|
||||
|
||||
fn git_init(
|
||||
&self,
|
||||
abs_work_directory_path: &Path,
|
||||
_fallback_branch_name: String,
|
||||
) -> Result<()> {
|
||||
smol::block_on(self.create_dir(&abs_work_directory_path.join(".git")))
|
||||
}
|
||||
|
||||
fn is_fake(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
borrow::{Borrow, Cow},
|
||||
sync::atomic::{self, AtomicBool},
|
||||
};
|
||||
|
||||
@@ -50,22 +50,24 @@ impl<'a> Matcher<'a> {
|
||||
|
||||
/// Filter and score fuzzy match candidates. Results are returned unsorted, in the same order as
|
||||
/// the input candidates.
|
||||
pub fn match_candidates<C: MatchCandidate, R, F>(
|
||||
pub fn match_candidates<C, R, F, T>(
|
||||
&mut self,
|
||||
prefix: &[char],
|
||||
lowercase_prefix: &[char],
|
||||
candidates: impl Iterator<Item = C>,
|
||||
candidates: impl Iterator<Item = T>,
|
||||
results: &mut Vec<R>,
|
||||
cancel_flag: &AtomicBool,
|
||||
build_match: F,
|
||||
) where
|
||||
C: MatchCandidate,
|
||||
T: Borrow<C>,
|
||||
F: Fn(&C, f64, &Vec<usize>) -> R,
|
||||
{
|
||||
let mut candidate_chars = Vec::new();
|
||||
let mut lowercase_candidate_chars = Vec::new();
|
||||
|
||||
for candidate in candidates {
|
||||
if !candidate.has_chars(self.query_char_bag) {
|
||||
if !candidate.borrow().has_chars(self.query_char_bag) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -75,7 +77,7 @@ impl<'a> Matcher<'a> {
|
||||
|
||||
candidate_chars.clear();
|
||||
lowercase_candidate_chars.clear();
|
||||
for c in candidate.to_string().chars() {
|
||||
for c in candidate.borrow().to_string().chars() {
|
||||
candidate_chars.push(c);
|
||||
lowercase_candidate_chars.append(&mut c.to_lowercase().collect::<Vec<_>>());
|
||||
}
|
||||
@@ -98,7 +100,11 @@ impl<'a> Matcher<'a> {
|
||||
);
|
||||
|
||||
if score > 0.0 {
|
||||
results.push(build_match(&candidate, score, &self.match_positions));
|
||||
results.push(build_match(
|
||||
candidate.borrow(),
|
||||
score,
|
||||
&self.match_positions,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
};
|
||||
use gpui::BackgroundExecutor;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
borrow::{Borrow, Cow},
|
||||
cmp::{self, Ordering},
|
||||
iter,
|
||||
ops::Range,
|
||||
@@ -113,14 +113,17 @@ impl Ord for StringMatch {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn match_strings(
|
||||
candidates: &[StringMatchCandidate],
|
||||
pub async fn match_strings<T>(
|
||||
candidates: &[T],
|
||||
query: &str,
|
||||
smart_case: bool,
|
||||
max_results: usize,
|
||||
cancel_flag: &AtomicBool,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Vec<StringMatch> {
|
||||
) -> Vec<StringMatch>
|
||||
where
|
||||
T: Borrow<StringMatchCandidate> + Sync,
|
||||
{
|
||||
if candidates.is_empty() || max_results == 0 {
|
||||
return Default::default();
|
||||
}
|
||||
@@ -129,10 +132,10 @@ pub async fn match_strings(
|
||||
return candidates
|
||||
.iter()
|
||||
.map(|candidate| StringMatch {
|
||||
candidate_id: candidate.id,
|
||||
candidate_id: candidate.borrow().id,
|
||||
score: 0.,
|
||||
positions: Default::default(),
|
||||
string: candidate.string.clone(),
|
||||
string: candidate.borrow().string.clone(),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
@@ -163,10 +166,12 @@ pub async fn match_strings(
|
||||
matcher.match_candidates(
|
||||
&[],
|
||||
&[],
|
||||
candidates[segment_start..segment_end].iter(),
|
||||
candidates[segment_start..segment_end]
|
||||
.iter()
|
||||
.map(|c| c.borrow()),
|
||||
results,
|
||||
cancel_flag,
|
||||
|candidate, score, positions| StringMatch {
|
||||
|candidate: &&StringMatchCandidate, score, positions| StringMatch {
|
||||
candidate_id: candidate.id,
|
||||
score,
|
||||
positions: positions.clone(),
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use crate::commit::get_messages;
|
||||
use crate::Oid;
|
||||
use crate::{parse_git_remote_url, BuildCommitPermalinkParams, GitHostingProviderRegistry, Oid};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::AsyncWriteExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::Write;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use std::{ops::Range, path::Path};
|
||||
use text::Rope;
|
||||
use time::macros::format_description;
|
||||
use time::OffsetDateTime;
|
||||
use time::UtcOffset;
|
||||
use url::Url;
|
||||
|
||||
pub use git2 as libgit;
|
||||
|
||||
@@ -17,34 +19,52 @@ pub use git2 as libgit;
|
||||
pub struct Blame {
|
||||
pub entries: Vec<BlameEntry>,
|
||||
pub messages: HashMap<Oid, String>,
|
||||
pub permalinks: HashMap<Oid, Url>,
|
||||
pub remote_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Blame {
|
||||
pub async fn for_path(
|
||||
pub fn for_path(
|
||||
git_binary: &Path,
|
||||
working_directory: &Path,
|
||||
path: &Path,
|
||||
content: &Rope,
|
||||
remote_url: Option<String>,
|
||||
provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
) -> Result<Self> {
|
||||
let output = run_git_blame(git_binary, working_directory, path, content).await?;
|
||||
let output = run_git_blame(git_binary, working_directory, path, content)?;
|
||||
let mut entries = parse_git_blame(&output)?;
|
||||
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start));
|
||||
|
||||
let mut permalinks = HashMap::default();
|
||||
let mut unique_shas = HashSet::default();
|
||||
let parsed_remote_url = remote_url
|
||||
.as_deref()
|
||||
.and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url));
|
||||
|
||||
for entry in entries.iter_mut() {
|
||||
unique_shas.insert(entry.sha);
|
||||
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
|
||||
// now do the parsing.
|
||||
if let Some((provider, remote)) = parsed_remote_url.as_ref() {
|
||||
permalinks.entry(entry.sha).or_insert_with(|| {
|
||||
provider.build_commit_permalink(
|
||||
remote,
|
||||
BuildCommitPermalinkParams {
|
||||
sha: entry.sha.to_string().as_str(),
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let shas = unique_shas.into_iter().collect::<Vec<_>>();
|
||||
let messages = get_messages(working_directory, &shas)
|
||||
.await
|
||||
.context("failed to get commit messages")?;
|
||||
let messages =
|
||||
get_messages(working_directory, &shas).context("failed to get commit messages")?;
|
||||
|
||||
Ok(Self {
|
||||
entries,
|
||||
permalinks,
|
||||
messages,
|
||||
remote_url,
|
||||
})
|
||||
@@ -54,13 +74,13 @@ impl Blame {
|
||||
const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD";
|
||||
const GIT_BLAME_NO_PATH: &str = "fatal: no such path";
|
||||
|
||||
async fn run_git_blame(
|
||||
fn run_git_blame(
|
||||
git_binary: &Path,
|
||||
working_directory: &Path,
|
||||
path: &Path,
|
||||
contents: &Rope,
|
||||
) -> Result<String> {
|
||||
let mut child = util::command::new_smol_command(git_binary)
|
||||
let child = util::command::new_std_command(git_binary)
|
||||
.current_dir(working_directory)
|
||||
.arg("blame")
|
||||
.arg("--incremental")
|
||||
@@ -73,19 +93,18 @@ async fn run_git_blame(
|
||||
.spawn()
|
||||
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
|
||||
|
||||
let stdin = child
|
||||
let mut stdin = child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.as_ref()
|
||||
.context("failed to get pipe to stdin of git blame command")?;
|
||||
|
||||
for chunk in contents.chunks() {
|
||||
stdin.write_all(chunk.as_bytes()).await?;
|
||||
stdin.write_all(chunk.as_bytes())?;
|
||||
}
|
||||
stdin.flush().await?;
|
||||
stdin.flush()?;
|
||||
|
||||
let output = child
|
||||
.output()
|
||||
.await
|
||||
.wait_with_output()
|
||||
.map_err(|e| anyhow!("Failed to read git blame output: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
|
||||
@@ -3,21 +3,20 @@ use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
|
||||
pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
|
||||
if shas.is_empty() {
|
||||
return Ok(HashMap::default());
|
||||
}
|
||||
|
||||
const MARKER: &str = "<MARKER>";
|
||||
|
||||
let output = util::command::new_smol_command("git")
|
||||
let output = util::command::new_std_command("git")
|
||||
.current_dir(working_directory)
|
||||
.arg("show")
|
||||
.arg("-s")
|
||||
.arg(format!("--format=%B{}", MARKER))
|
||||
.args(shas.iter().map(ToString::to_string))
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
|
||||
|
||||
anyhow::ensure!(
|
||||
|
||||
@@ -50,14 +50,11 @@ actions!(
|
||||
Fetch,
|
||||
Commit,
|
||||
ExpandCommitEditor,
|
||||
GenerateCommitMessage,
|
||||
Init,
|
||||
GenerateCommitMessage
|
||||
]
|
||||
);
|
||||
|
||||
action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
|
||||
action_with_deprecated_aliases!(git, Restore, ["editor::RevertSelectedHunks"]);
|
||||
action_with_deprecated_aliases!(git, Blame, ["editor::ToggleGitBlame"]);
|
||||
|
||||
/// The length of a Git short SHA.
|
||||
pub const SHORT_SHA_LENGTH: usize = 7;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -54,39 +54,6 @@ impl From<TrackedStatus> for FileStatus {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum StageStatus {
|
||||
Staged,
|
||||
Unstaged,
|
||||
PartiallyStaged,
|
||||
}
|
||||
|
||||
impl StageStatus {
|
||||
pub fn is_fully_staged(&self) -> bool {
|
||||
matches!(self, StageStatus::Staged)
|
||||
}
|
||||
|
||||
pub fn is_fully_unstaged(&self) -> bool {
|
||||
matches!(self, StageStatus::Unstaged)
|
||||
}
|
||||
|
||||
pub fn has_staged(&self) -> bool {
|
||||
matches!(self, StageStatus::Staged | StageStatus::PartiallyStaged)
|
||||
}
|
||||
|
||||
pub fn has_unstaged(&self) -> bool {
|
||||
matches!(self, StageStatus::Unstaged | StageStatus::PartiallyStaged)
|
||||
}
|
||||
|
||||
pub fn as_bool(self) -> Option<bool> {
|
||||
match self {
|
||||
StageStatus::Staged => Some(true),
|
||||
StageStatus::Unstaged => Some(false),
|
||||
StageStatus::PartiallyStaged => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileStatus {
|
||||
pub const fn worktree(worktree_status: StatusCode) -> Self {
|
||||
FileStatus::Tracked(TrackedStatus {
|
||||
@@ -139,15 +106,15 @@ impl FileStatus {
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
pub fn staging(self) -> StageStatus {
|
||||
pub fn is_staged(self) -> Option<bool> {
|
||||
match self {
|
||||
FileStatus::Untracked | FileStatus::Ignored | FileStatus::Unmerged { .. } => {
|
||||
StageStatus::Unstaged
|
||||
Some(false)
|
||||
}
|
||||
FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
|
||||
(StatusCode::Unmodified, _) => StageStatus::Unstaged,
|
||||
(_, StatusCode::Unmodified) => StageStatus::Staged,
|
||||
_ => StageStatus::PartiallyStaged,
|
||||
(StatusCode::Unmodified, _) => Some(false),
|
||||
(_, StatusCode::Unmodified) => Some(true),
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,9 @@ mod providers;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use git::repository::GitRepository;
|
||||
use git::GitHostingProviderRegistry;
|
||||
use gpui::App;
|
||||
use url::Url;
|
||||
use util::maybe;
|
||||
|
||||
pub use crate::providers::*;
|
||||
|
||||
@@ -18,7 +15,7 @@ pub fn init(cx: &App) {
|
||||
provider_registry.register_hosting_provider(Arc::new(Chromium));
|
||||
provider_registry.register_hosting_provider(Arc::new(Codeberg));
|
||||
provider_registry.register_hosting_provider(Arc::new(Gitee));
|
||||
provider_registry.register_hosting_provider(Arc::new(Github::new()));
|
||||
provider_registry.register_hosting_provider(Arc::new(Github));
|
||||
provider_registry.register_hosting_provider(Arc::new(Gitlab::new()));
|
||||
provider_registry.register_hosting_provider(Arc::new(Sourcehut));
|
||||
}
|
||||
@@ -37,51 +34,5 @@ pub fn register_additional_providers(
|
||||
|
||||
if let Ok(gitlab_self_hosted) = Gitlab::from_remote_url(&origin_url) {
|
||||
provider_registry.register_hosting_provider(Arc::new(gitlab_self_hosted));
|
||||
} else if let Ok(github_self_hosted) = Github::from_remote_url(&origin_url) {
|
||||
provider_registry.register_hosting_provider(Arc::new(github_self_hosted));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_host_from_git_remote_url(remote_url: &str) -> Result<String> {
|
||||
maybe!({
|
||||
if let Some(remote_url) = remote_url.strip_prefix("git@") {
|
||||
if let Some((host, _)) = remote_url.trim_start_matches("git@").split_once(':') {
|
||||
return Some(host.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Url::parse(&remote_url)
|
||||
.ok()
|
||||
.and_then(|remote_url| remote_url.host_str().map(|host| host.to_string()))
|
||||
})
|
||||
.ok_or_else(|| anyhow!("URL has no host"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::get_host_from_git_remote_url;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_get_host_from_git_remote_url() {
|
||||
let tests = [
|
||||
(
|
||||
"https://jlannister@github.com/some-org/some-repo.git",
|
||||
Some("github.com".to_string()),
|
||||
),
|
||||
(
|
||||
"git@github.com:zed-industries/zed.git",
|
||||
Some("github.com".to_string()),
|
||||
),
|
||||
(
|
||||
"git@my.super.long.subdomain.com:zed-industries/zed.git",
|
||||
Some("my.super.long.subdomain.com".to_string()),
|
||||
),
|
||||
];
|
||||
|
||||
for (remote_url, expected_host) in tests {
|
||||
let host = get_host_from_git_remote_url(remote_url).ok();
|
||||
assert_eq!(host, expected_host);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ use git::{
|
||||
PullRequest, RemoteUrl,
|
||||
};
|
||||
|
||||
use crate::get_host_from_git_remote_url;
|
||||
|
||||
fn pull_request_number_regex() -> &'static Regex {
|
||||
static PULL_REQUEST_NUMBER_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\(#(\d+)\)$").unwrap());
|
||||
@@ -45,38 +43,9 @@ struct User {
|
||||
pub avatar_url: String,
|
||||
}
|
||||
|
||||
pub struct Github {
|
||||
name: String,
|
||||
base_url: Url,
|
||||
}
|
||||
pub struct Github;
|
||||
|
||||
impl Github {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
name: "GitHub".to_string(),
|
||||
base_url: Url::parse("https://github.com").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_remote_url(remote_url: &str) -> Result<Self> {
|
||||
let host = get_host_from_git_remote_url(remote_url)?;
|
||||
if host == "github.com" {
|
||||
bail!("the GitHub instance is not self-hosted");
|
||||
}
|
||||
|
||||
// TODO: detecting self hosted instances by checking whether "github" is in the url or not
|
||||
// is not very reliable. See https://github.com/zed-industries/zed/issues/26393 for more
|
||||
// information.
|
||||
if !host.contains("github") {
|
||||
bail!("not a GitHub URL");
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
name: "GitHub Self-Hosted".to_string(),
|
||||
base_url: Url::parse(&format!("https://{}", host))?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn fetch_github_commit_author(
|
||||
&self,
|
||||
repo_owner: &str,
|
||||
@@ -84,10 +53,7 @@ impl Github {
|
||||
commit: &str,
|
||||
client: &Arc<dyn HttpClient>,
|
||||
) -> Result<Option<User>> {
|
||||
let Some(host) = self.base_url.host_str() else {
|
||||
bail!("failed to get host from github base url");
|
||||
};
|
||||
let url = format!("https://api.{host}/repos/{repo_owner}/{repo}/commits/{commit}");
|
||||
let url = format!("https://api.github.com/repos/{repo_owner}/{repo}/commits/{commit}");
|
||||
|
||||
let mut request = Request::get(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
@@ -124,17 +90,15 @@ impl Github {
|
||||
#[async_trait]
|
||||
impl GitHostingProvider for Github {
|
||||
fn name(&self) -> String {
|
||||
self.name.clone()
|
||||
"GitHub".to_string()
|
||||
}
|
||||
|
||||
fn base_url(&self) -> Url {
|
||||
self.base_url.clone()
|
||||
Url::parse("https://github.com").unwrap()
|
||||
}
|
||||
|
||||
fn supports_avatars(&self) -> bool {
|
||||
// Avatars are not supported for self-hosted GitHub instances
|
||||
// See tracking issue: https://github.com/zed-industries/zed/issues/11043
|
||||
&self.name == "GitHub"
|
||||
true
|
||||
}
|
||||
|
||||
fn format_line_number(&self, line: u32) -> String {
|
||||
@@ -149,7 +113,7 @@ impl GitHostingProvider for Github {
|
||||
let url = RemoteUrl::from_str(url).ok()?;
|
||||
|
||||
let host = url.host_str()?;
|
||||
if host != self.base_url.host_str()? {
|
||||
if host != "github.com" {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -239,76 +203,9 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_invalid_self_hosted_remote_url() {
|
||||
let remote_url = "git@github.com:zed-industries/zed.git";
|
||||
let github = Github::from_remote_url(remote_url);
|
||||
assert!(github.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_remote_url_ssh() {
|
||||
let remote_url = "git@github.my-enterprise.com:zed-industries/zed.git";
|
||||
let github = Github::from_remote_url(remote_url).unwrap();
|
||||
|
||||
assert!(!github.supports_avatars());
|
||||
assert_eq!(github.name, "GitHub Self-Hosted".to_string());
|
||||
assert_eq!(
|
||||
github.base_url,
|
||||
Url::parse("https://github.my-enterprise.com").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_remote_url_https() {
|
||||
let remote_url = "https://github.my-enterprise.com/zed-industries/zed.git";
|
||||
let github = Github::from_remote_url(remote_url).unwrap();
|
||||
|
||||
assert!(!github.supports_avatars());
|
||||
assert_eq!(github.name, "GitHub Self-Hosted".to_string());
|
||||
assert_eq!(
|
||||
github.base_url,
|
||||
Url::parse("https://github.my-enterprise.com").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_self_hosted_ssh_url() {
|
||||
let remote_url = "git@github.my-enterprise.com:zed-industries/zed.git";
|
||||
let parsed_remote = Github::from_remote_url(remote_url)
|
||||
.unwrap()
|
||||
.parse_remote_url(remote_url)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed_remote,
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_self_hosted_https_url_with_subgroup() {
|
||||
let remote_url = "https://github.my-enterprise.com/zed-industries/zed.git";
|
||||
let parsed_remote = Github::from_remote_url(remote_url)
|
||||
.unwrap()
|
||||
.parse_remote_url(remote_url)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed_remote,
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_ssh_url() {
|
||||
let parsed_remote = Github::new()
|
||||
let parsed_remote = Github
|
||||
.parse_remote_url("git@github.com:zed-industries/zed.git")
|
||||
.unwrap();
|
||||
|
||||
@@ -323,7 +220,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_https_url() {
|
||||
let parsed_remote = Github::new()
|
||||
let parsed_remote = Github
|
||||
.parse_remote_url("https://github.com/zed-industries/zed.git")
|
||||
.unwrap();
|
||||
|
||||
@@ -338,7 +235,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_https_url_with_username() {
|
||||
let parsed_remote = Github::new()
|
||||
let parsed_remote = Github
|
||||
.parse_remote_url("https://jlannister@github.com/some-org/some-repo.git")
|
||||
.unwrap();
|
||||
|
||||
@@ -357,7 +254,7 @@ mod tests {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
};
|
||||
let permalink = Github::new().build_permalink(
|
||||
let permalink = Github.build_permalink(
|
||||
remote,
|
||||
BuildPermalinkParams {
|
||||
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||
@@ -372,7 +269,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_build_github_permalink() {
|
||||
let permalink = Github::new().build_permalink(
|
||||
let permalink = Github.build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
@@ -390,7 +287,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_build_github_permalink_with_single_line_selection() {
|
||||
let permalink = Github::new().build_permalink(
|
||||
let permalink = Github.build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
@@ -408,7 +305,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_build_github_permalink_with_multi_line_selection() {
|
||||
let permalink = Github::new().build_permalink(
|
||||
let permalink = Github.build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
@@ -431,9 +328,8 @@ mod tests {
|
||||
repo: "zed".into(),
|
||||
};
|
||||
|
||||
let github = Github::new();
|
||||
let message = "This does not contain a pull request";
|
||||
assert!(github.extract_pull_request(&remote, message).is_none());
|
||||
assert!(Github.extract_pull_request(&remote, message).is_none());
|
||||
|
||||
// Pull request number at end of first line
|
||||
let message = indoc! {r#"
|
||||
@@ -448,7 +344,7 @@ mod tests {
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
github
|
||||
Github
|
||||
.extract_pull_request(&remote, &message)
|
||||
.unwrap()
|
||||
.url
|
||||
@@ -463,6 +359,6 @@ mod tests {
|
||||
See the original PR, this is a fix.
|
||||
"#
|
||||
};
|
||||
assert_eq!(github.extract_pull_request(&remote, &message), None);
|
||||
assert_eq!(Github.extract_pull_request(&remote, &message), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use url::Url;
|
||||
use util::maybe;
|
||||
|
||||
use git::{
|
||||
BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
|
||||
RemoteUrl,
|
||||
};
|
||||
|
||||
use crate::get_host_from_git_remote_url;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Gitlab {
|
||||
name: String,
|
||||
@@ -25,14 +24,19 @@ impl Gitlab {
|
||||
}
|
||||
|
||||
pub fn from_remote_url(remote_url: &str) -> Result<Self> {
|
||||
let host = get_host_from_git_remote_url(remote_url)?;
|
||||
if host == "gitlab.com" {
|
||||
bail!("the GitLab instance is not self-hosted");
|
||||
}
|
||||
let host = maybe!({
|
||||
if let Some(remote_url) = remote_url.strip_prefix("git@") {
|
||||
if let Some((host, _)) = remote_url.trim_start_matches("git@").split_once(':') {
|
||||
return Some(host.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Url::parse(&remote_url)
|
||||
.ok()
|
||||
.and_then(|remote_url| remote_url.host_str().map(|host| host.to_string()))
|
||||
})
|
||||
.ok_or_else(|| anyhow!("URL has no host"))?;
|
||||
|
||||
// TODO: detecting self hosted instances by checking whether "gitlab" is in the url or not
|
||||
// is not very reliable. See https://github.com/zed-industries/zed/issues/26393 for more
|
||||
// information.
|
||||
if !host.contains("gitlab") {
|
||||
bail!("not a GitLab URL");
|
||||
}
|
||||
@@ -126,13 +130,6 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_invalid_self_hosted_remote_url() {
|
||||
let remote_url = "https://gitlab.com/zed-industries/zed.git";
|
||||
let github = Gitlab::from_remote_url(remote_url);
|
||||
assert!(github.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_ssh_url() {
|
||||
let parsed_remote = Gitlab::new()
|
||||
|
||||
@@ -18,15 +18,13 @@ test-support = ["multi_buffer/test-support"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
askpass.workspace = true
|
||||
assistant_settings.workspace = true
|
||||
askpass.workspace= true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
git.workspace = true
|
||||
@@ -39,7 +37,6 @@ linkme.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
notifications.workspace = true
|
||||
panel.workspace = true
|
||||
picker.workspace = true
|
||||
postage.workspace = true
|
||||
@@ -54,7 +51,6 @@ strum.workspace = true
|
||||
telemetry.workspace = true
|
||||
theme.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -74,7 +74,7 @@ impl Render for AskPassModal {
|
||||
.px(DynamicSpacing::Base12.rems(cx))
|
||||
.pt(DynamicSpacing::Base08.rems(cx))
|
||||
.pb(DynamicSpacing::Base04.rems(cx))
|
||||
.rounded_t_md()
|
||||
.rounded_t_sm()
|
||||
.w_full()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(IconName::GitBranch).size(IconSize::XSmall))
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
|
||||
use git::repository::Branch;
|
||||
use gpui::{
|
||||
rems, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render,
|
||||
SharedString, Styled, Subscription, Task, Window,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||
Task, Window,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::git::Repository;
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
use time_format::format_local_timestamp;
|
||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle};
|
||||
use util::ResultExt;
|
||||
use workspace::notifications::DetachAndPromptErr;
|
||||
use workspace::{ModalView, Workspace};
|
||||
@@ -20,30 +18,10 @@ use workspace::{ModalView, Workspace};
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||
workspace.register_action(open);
|
||||
workspace.register_action(switch);
|
||||
workspace.register_action(checkout_branch);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn checkout_branch(
|
||||
workspace: &mut Workspace,
|
||||
_: &zed_actions::git::CheckoutBranch,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
open(workspace, &zed_actions::git::Branch, window, cx);
|
||||
}
|
||||
|
||||
pub fn switch(
|
||||
workspace: &mut Workspace,
|
||||
_: &zed_actions::git::Switch,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
open(workspace, &zed_actions::git::Branch, window, cx);
|
||||
}
|
||||
|
||||
pub fn open(
|
||||
workspace: &mut Workspace,
|
||||
_: &zed_actions::git::Branch,
|
||||
@@ -53,7 +31,7 @@ pub fn open(
|
||||
let repository = workspace.project().read(cx).active_repository(cx).clone();
|
||||
let style = BranchListStyle::Modal;
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
BranchList::new(repository, style, rems(34.), window, cx)
|
||||
BranchList::new(repository, style, 34., window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -63,7 +41,7 @@ pub fn popover(
|
||||
cx: &mut App,
|
||||
) -> Entity<BranchList> {
|
||||
cx.new(|cx| {
|
||||
let list = BranchList::new(repository, BranchListStyle::Popover, rems(20.), window, cx);
|
||||
let list = BranchList::new(repository, BranchListStyle::Popover, 15., window, cx);
|
||||
list.focus_handle(cx).focus(window);
|
||||
list
|
||||
})
|
||||
@@ -76,7 +54,8 @@ enum BranchListStyle {
|
||||
}
|
||||
|
||||
pub struct BranchList {
|
||||
width: Rems,
|
||||
rem_width: f32,
|
||||
pub popover_handle: PopoverMenuHandle<Self>,
|
||||
pub picker: Entity<Picker<BranchListDelegate>>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
@@ -85,26 +64,20 @@ impl BranchList {
|
||||
fn new(
|
||||
repository: Option<Entity<Repository>>,
|
||||
style: BranchListStyle,
|
||||
width: Rems,
|
||||
rem_width: f32,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let popover_handle = PopoverMenuHandle::default();
|
||||
let all_branches_request = repository
|
||||
.clone()
|
||||
.map(|repository| repository.read(cx).branches());
|
||||
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
let mut all_branches = all_branches_request
|
||||
let all_branches = all_branches_request
|
||||
.context("No active repository")?
|
||||
.await??;
|
||||
|
||||
all_branches.sort_by_key(|branch| {
|
||||
branch
|
||||
.most_recent_commit
|
||||
.as_ref()
|
||||
.map(|commit| 0 - commit.commit_timestamp)
|
||||
});
|
||||
|
||||
this.update_in(&mut cx, |this, window, cx| {
|
||||
this.picker.update(cx, |picker, cx| {
|
||||
picker.delegate.all_branches = Some(all_branches);
|
||||
@@ -116,7 +89,7 @@ impl BranchList {
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
let delegate = BranchListDelegate::new(repository.clone(), style);
|
||||
let delegate = BranchListDelegate::new(repository.clone(), style, 20);
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
|
||||
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
|
||||
@@ -125,20 +98,11 @@ impl BranchList {
|
||||
|
||||
Self {
|
||||
picker,
|
||||
width,
|
||||
rem_width,
|
||||
popover_handle,
|
||||
_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_modifiers_changed(
|
||||
&mut self,
|
||||
ev: &ModifiersChangedEvent,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.picker
|
||||
.update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers)
|
||||
}
|
||||
}
|
||||
impl ModalView for BranchList {}
|
||||
impl EventEmitter<DismissEvent> for BranchList {}
|
||||
@@ -152,8 +116,7 @@ impl Focusable for BranchList {
|
||||
impl Render for BranchList {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w(self.width)
|
||||
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
|
||||
.w(rems(self.rem_width))
|
||||
.child(self.picker.clone())
|
||||
.on_mouse_down_out({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
@@ -166,10 +129,20 @@ impl Render for BranchList {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct BranchEntry {
|
||||
branch: Branch,
|
||||
positions: Vec<usize>,
|
||||
is_new: bool,
|
||||
enum BranchEntry {
|
||||
Branch(StringMatch),
|
||||
History(String),
|
||||
NewBranch { name: String },
|
||||
}
|
||||
|
||||
impl BranchEntry {
|
||||
fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Branch(branch) => &branch.string,
|
||||
Self::History(branch) => &branch,
|
||||
Self::NewBranch { name } => &name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BranchListDelegate {
|
||||
@@ -179,11 +152,16 @@ pub struct BranchListDelegate {
|
||||
style: BranchListStyle,
|
||||
selected_index: usize,
|
||||
last_query: String,
|
||||
modifiers: Modifiers,
|
||||
/// Max length of branch name before we truncate it and add a trailing `...`.
|
||||
branch_name_trailoff_after: usize,
|
||||
}
|
||||
|
||||
impl BranchListDelegate {
|
||||
fn new(repo: Option<Entity<Repository>>, style: BranchListStyle) -> Self {
|
||||
fn new(
|
||||
repo: Option<Entity<Repository>>,
|
||||
style: BranchListStyle,
|
||||
branch_name_trailoff_after: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
matches: vec![],
|
||||
repo,
|
||||
@@ -191,30 +169,15 @@ impl BranchListDelegate {
|
||||
all_branches: None,
|
||||
selected_index: 0,
|
||||
last_query: Default::default(),
|
||||
modifiers: Default::default(),
|
||||
branch_name_trailoff_after,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_branch(
|
||||
&self,
|
||||
new_branch_name: SharedString,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
let Some(repo) = self.repo.clone() else {
|
||||
return;
|
||||
};
|
||||
cx.spawn(|_, cx| async move {
|
||||
cx.update(|cx| repo.read(cx).create_branch(new_branch_name.to_string()))?
|
||||
.await??;
|
||||
cx.update(|cx| repo.read(cx).change_branch(new_branch_name.to_string()))?
|
||||
.await??;
|
||||
Ok(())
|
||||
})
|
||||
.detach_and_prompt_err("Failed to create branch", window, cx, |e, _, _| {
|
||||
Some(e.to_string())
|
||||
});
|
||||
cx.emit(DismissEvent);
|
||||
pub fn branch_count(&self) -> usize {
|
||||
self.matches
|
||||
.iter()
|
||||
.filter(|item| matches!(item, BranchEntry::Branch(_)))
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,28 +211,37 @@ impl PickerDelegate for BranchListDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let Some(all_branches) = self.all_branches.clone() else {
|
||||
let Some(mut all_branches) = self.all_branches.clone() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
const RECENT_BRANCHES_COUNT: usize = 10;
|
||||
cx.spawn_in(window, move |picker, mut cx| async move {
|
||||
let mut matches: Vec<BranchEntry> = if query.is_empty() {
|
||||
all_branches
|
||||
const RECENT_BRANCHES_COUNT: usize = 10;
|
||||
if query.is_empty() {
|
||||
if all_branches.len() > RECENT_BRANCHES_COUNT {
|
||||
// Truncate list of recent branches
|
||||
// Do a partial sort to show recent-ish branches first.
|
||||
all_branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
|
||||
rhs.priority_key().cmp(&lhs.priority_key())
|
||||
});
|
||||
all_branches.truncate(RECENT_BRANCHES_COUNT);
|
||||
}
|
||||
all_branches.sort_unstable_by(|lhs, rhs| {
|
||||
rhs.is_head.cmp(&lhs.is_head).then(lhs.name.cmp(&rhs.name))
|
||||
});
|
||||
}
|
||||
|
||||
let candidates = all_branches
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
|
||||
.collect::<Vec<StringMatchCandidate>>();
|
||||
let matches: Vec<BranchEntry> = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.take(RECENT_BRANCHES_COUNT)
|
||||
.map(|branch| BranchEntry {
|
||||
branch,
|
||||
positions: Vec::new(),
|
||||
is_new: false,
|
||||
})
|
||||
.map(|candidate| BranchEntry::History(candidate.string))
|
||||
.collect()
|
||||
} else {
|
||||
let candidates = all_branches
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name.clone()))
|
||||
.collect::<Vec<StringMatchCandidate>>();
|
||||
fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
@@ -281,35 +253,20 @@ impl PickerDelegate for BranchListDelegate {
|
||||
.await
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|candidate| BranchEntry {
|
||||
branch: all_branches[candidate.candidate_id].clone(),
|
||||
positions: candidate.positions,
|
||||
is_new: false,
|
||||
})
|
||||
.map(BranchEntry::Branch)
|
||||
.collect()
|
||||
};
|
||||
picker
|
||||
.update(&mut cx, |picker, _| {
|
||||
#[allow(clippy::nonminimal_bool)]
|
||||
if !query.is_empty()
|
||||
&& !matches
|
||||
.first()
|
||||
.is_some_and(|entry| entry.branch.name == query)
|
||||
{
|
||||
matches.push(BranchEntry {
|
||||
branch: Branch {
|
||||
name: query.clone().into(),
|
||||
is_head: false,
|
||||
upstream: None,
|
||||
most_recent_commit: None,
|
||||
},
|
||||
positions: Vec::new(),
|
||||
is_new: true,
|
||||
})
|
||||
}
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.matches = matches;
|
||||
if delegate.matches.is_empty() {
|
||||
if !query.is_empty() {
|
||||
delegate.matches.push(BranchEntry::NewBranch {
|
||||
name: query.trim().replace(' ', "-"),
|
||||
});
|
||||
}
|
||||
|
||||
delegate.selected_index = 0;
|
||||
} else {
|
||||
delegate.selected_index =
|
||||
@@ -321,14 +278,10 @@ impl PickerDelegate for BranchListDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let Some(entry) = self.matches.get(self.selected_index()) else {
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let Some(branch) = self.matches.get(self.selected_index()) else {
|
||||
return;
|
||||
};
|
||||
if entry.is_new {
|
||||
self.create_branch(entry.branch.name.clone(), window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let current_branch = self.repo.as_ref().map(|repo| {
|
||||
repo.update(cx, |repo, _| {
|
||||
@@ -338,14 +291,14 @@ impl PickerDelegate for BranchListDelegate {
|
||||
|
||||
if current_branch
|
||||
.flatten()
|
||||
.is_some_and(|current_branch| current_branch == entry.branch.name)
|
||||
.is_some_and(|current_branch| current_branch == branch.name())
|
||||
{
|
||||
cx.emit(DismissEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
cx.spawn_in(window, {
|
||||
let branch = entry.branch.clone();
|
||||
let branch = branch.clone();
|
||||
|picker, mut cx| async move {
|
||||
let branch_change_task = picker.update(&mut cx, |this, cx| {
|
||||
let repo = this
|
||||
@@ -358,8 +311,22 @@ impl PickerDelegate for BranchListDelegate {
|
||||
let cx = cx.to_async();
|
||||
|
||||
anyhow::Ok(async move {
|
||||
cx.update(|cx| repo.read(cx).change_branch(branch.name.to_string()))?
|
||||
.await?
|
||||
match branch {
|
||||
BranchEntry::Branch(StringMatch {
|
||||
string: branch_name,
|
||||
..
|
||||
})
|
||||
| BranchEntry::History(branch_name) => {
|
||||
cx.update(|cx| repo.read(cx).change_branch(branch_name))?
|
||||
.await?
|
||||
}
|
||||
BranchEntry::NewBranch { name: branch_name } => {
|
||||
cx.update(|cx| repo.read(cx).create_branch(branch_name.clone()))?
|
||||
.await??;
|
||||
cx.update(|cx| repo.read(cx).change_branch(branch_name))?
|
||||
.await?
|
||||
}
|
||||
}
|
||||
})
|
||||
})??;
|
||||
|
||||
@@ -379,35 +346,16 @@ impl PickerDelegate for BranchListDelegate {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_header(&self, _: &mut Window, _cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let entry = &self.matches[ix];
|
||||
|
||||
let (commit_time, subject) = entry
|
||||
.branch
|
||||
.most_recent_commit
|
||||
.as_ref()
|
||||
.map(|commit| {
|
||||
let subject = commit.subject.clone();
|
||||
let commit_time = OffsetDateTime::from_unix_timestamp(commit.commit_timestamp)
|
||||
.unwrap_or_else(|_| OffsetDateTime::now_utc());
|
||||
let formatted_time = format_local_timestamp(
|
||||
commit_time,
|
||||
OffsetDateTime::now_utc(),
|
||||
time_format::TimestampFormat::Relative,
|
||||
);
|
||||
(Some(formatted_time), Some(subject))
|
||||
})
|
||||
.unwrap_or_else(|| (None, None));
|
||||
let hit = &self.matches[ix];
|
||||
let shortened_branch_name =
|
||||
util::truncate_and_trailoff(&hit.name(), self.branch_name_trailoff_after);
|
||||
|
||||
Some(
|
||||
ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
|
||||
@@ -418,68 +366,29 @@ impl PickerDelegate for BranchListDelegate {
|
||||
})
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.flex_shrink()
|
||||
.overflow_x_hidden()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(div().flex_shrink().overflow_x_hidden().child(
|
||||
if entry.is_new {
|
||||
Label::new(format!(
|
||||
"Create branch \"{}\"…",
|
||||
entry.branch.name
|
||||
))
|
||||
.single_line()
|
||||
.into_any_element()
|
||||
} else {
|
||||
HighlightedLabel::new(
|
||||
entry.branch.name.clone(),
|
||||
entry.positions.clone(),
|
||||
)
|
||||
.truncate()
|
||||
.into_any_element()
|
||||
},
|
||||
))
|
||||
.when_some(commit_time, |el, commit_time| {
|
||||
el.child(
|
||||
Label::new(commit_time)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_element(),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when(self.style == BranchListStyle::Modal, |el| {
|
||||
el.child(div().max_w_96().child({
|
||||
let message = if entry.is_new {
|
||||
if let Some(current_branch) =
|
||||
self.repo.as_ref().and_then(|repo| {
|
||||
repo.read(cx).current_branch().map(|b| b.name.clone())
|
||||
})
|
||||
{
|
||||
format!("based off {}", current_branch)
|
||||
} else {
|
||||
"based off the current branch".to_string()
|
||||
}
|
||||
} else {
|
||||
subject.unwrap_or("no commits found".into()).to_string()
|
||||
};
|
||||
Label::new(message)
|
||||
.size(LabelSize::Small)
|
||||
.truncate()
|
||||
.color(Color::Muted)
|
||||
}))
|
||||
}),
|
||||
),
|
||||
.when(matches!(hit, BranchEntry::History(_)), |el| {
|
||||
el.end_slot(
|
||||
Icon::new(IconName::HistoryRerun)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
})
|
||||
.map(|el| match hit {
|
||||
BranchEntry::Branch(branch) => {
|
||||
let highlights: Vec<_> = branch
|
||||
.positions
|
||||
.iter()
|
||||
.filter(|index| index < &&self.branch_name_trailoff_after)
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
el.child(HighlightedLabel::new(shortened_branch_name, highlights))
|
||||
}
|
||||
BranchEntry::History(_) => el.child(Label::new(shortened_branch_name)),
|
||||
BranchEntry::NewBranch { name } => {
|
||||
el.child(Label::new(format!("Create branch '{name}'")))
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ If you can accurately express the change in just the subject line, don't include
|
||||
|
||||
Don't repeat information from the subject line in the message body.
|
||||
|
||||
Only return the commit message in your response. Do not include any additional meta-commentary about the task. Do not include the raw diff output in the commit message.
|
||||
Only return the commit message in your response. Do not include any additional meta-commentary about the task.
|
||||
|
||||
Follow good Git style:
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
use crate::branch_picker::{self, BranchList};
|
||||
use crate::git_panel::{commit_message_editor, GitPanel};
|
||||
use git::{Commit, GenerateCommitMessage};
|
||||
use git::Commit;
|
||||
use panel::{panel_button, panel_editor_style, panel_filled_button};
|
||||
use ui::{prelude::*, KeybindingHint, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
use ui::{prelude::*, KeybindingHint, PopoverMenu, Tooltip};
|
||||
|
||||
use editor::{Editor, EditorElement};
|
||||
use gpui::*;
|
||||
@@ -65,11 +65,11 @@ pub fn init(cx: &mut App) {
|
||||
}
|
||||
|
||||
pub struct CommitModal {
|
||||
branch_list: Entity<BranchList>,
|
||||
git_panel: Entity<GitPanel>,
|
||||
commit_editor: Entity<Editor>,
|
||||
restore_dock: RestoreDock,
|
||||
properties: ModalContainerProperties,
|
||||
branch_list_handle: PopoverMenuHandle<BranchList>,
|
||||
}
|
||||
|
||||
impl Focusable for CommitModal {
|
||||
@@ -146,6 +146,7 @@ impl CommitModal {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let panel = git_panel.read(cx);
|
||||
let active_repository = panel.active_repository.clone();
|
||||
let suggested_commit_message = panel.suggest_commit_message();
|
||||
|
||||
let commit_editor = git_panel.update(cx, |git_panel, cx| {
|
||||
@@ -176,7 +177,11 @@ impl CommitModal {
|
||||
let focus_handle = commit_editor.focus_handle(cx);
|
||||
|
||||
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
|
||||
if !this.branch_list_handle.is_focused(window, cx) {
|
||||
if !this
|
||||
.branch_list
|
||||
.focus_handle(cx)
|
||||
.contains_focused(window, cx)
|
||||
{
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
})
|
||||
@@ -185,11 +190,11 @@ impl CommitModal {
|
||||
let properties = ModalContainerProperties::new(window, 50);
|
||||
|
||||
Self {
|
||||
branch_list: branch_picker::popover(active_repository.clone(), window, cx),
|
||||
git_panel,
|
||||
commit_editor,
|
||||
restore_dock,
|
||||
properties,
|
||||
branch_list_handle: PopoverMenuHandle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,29 +232,34 @@ impl CommitModal {
|
||||
}
|
||||
|
||||
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let (can_commit, tooltip, commit_label, co_authors, generate_commit_message, active_repo) =
|
||||
let git_panel = self.git_panel.clone();
|
||||
|
||||
let (branch, can_commit, tooltip, commit_label, co_authors, generate_commit_message) =
|
||||
self.git_panel.update(cx, |git_panel, cx| {
|
||||
let branch = git_panel
|
||||
.active_repository
|
||||
.as_ref()
|
||||
.and_then(|repo| {
|
||||
repo.read(cx)
|
||||
.repository_entry
|
||||
.branch()
|
||||
.map(|b| b.name.clone())
|
||||
})
|
||||
.unwrap_or_else(|| "<no branch>".into());
|
||||
let (can_commit, tooltip) = git_panel.configure_commit_button(cx);
|
||||
let title = git_panel.commit_button_title();
|
||||
let co_authors = git_panel.render_co_authors(cx);
|
||||
let generate_commit_message = git_panel.render_generate_commit_message_button(cx);
|
||||
let active_repo = git_panel.active_repository.clone();
|
||||
(
|
||||
branch,
|
||||
can_commit,
|
||||
tooltip,
|
||||
title,
|
||||
co_authors,
|
||||
generate_commit_message,
|
||||
active_repo,
|
||||
)
|
||||
});
|
||||
|
||||
let branch = active_repo
|
||||
.as_ref()
|
||||
.and_then(|repo| repo.read(cx).repository_entry.branch())
|
||||
.map(|b| b.name.clone())
|
||||
.unwrap_or_else(|| "<no branch>".into());
|
||||
|
||||
let branch_picker_button = panel_button(branch)
|
||||
.icon(IconName::GitBranch)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -266,8 +276,10 @@ impl CommitModal {
|
||||
.style(ButtonStyle::Transparent);
|
||||
|
||||
let branch_picker = PopoverMenu::new("popover-button")
|
||||
.menu(move |window, cx| Some(branch_picker::popover(active_repo.clone(), window, cx)))
|
||||
.with_handle(self.branch_list_handle.clone())
|
||||
.menu({
|
||||
let branch_list = self.branch_list.clone();
|
||||
move |_window, _cx| Some(branch_list.clone())
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
branch_picker_button,
|
||||
Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
|
||||
@@ -277,7 +289,6 @@ impl CommitModal {
|
||||
x: px(0.0),
|
||||
y: px(-2.0),
|
||||
});
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
let close_kb_hint =
|
||||
if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
|
||||
@@ -289,9 +300,12 @@ impl CommitModal {
|
||||
None
|
||||
};
|
||||
|
||||
let panel_editor_focus_handle =
|
||||
git_panel.update(cx, |git_panel, cx| git_panel.editor_focus_handle(cx));
|
||||
|
||||
let commit_button = panel_filled_button(commit_label)
|
||||
.tooltip({
|
||||
let panel_editor_focus_handle = focus_handle.clone();
|
||||
let panel_editor_focus_handle = panel_editor_focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(tooltip, &Commit, &panel_editor_focus_handle, window, cx)
|
||||
}
|
||||
@@ -316,14 +330,7 @@ impl CommitModal {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.flex_shrink()
|
||||
.overflow_x_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_shrink()
|
||||
.overflow_x_hidden()
|
||||
.child(branch_picker),
|
||||
)
|
||||
.child(branch_picker)
|
||||
.children(generate_commit_message)
|
||||
.children(co_authors),
|
||||
)
|
||||
@@ -350,14 +357,6 @@ impl CommitModal {
|
||||
.update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn toggle_branch_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.branch_list_handle.is_focused(window, cx) {
|
||||
self.focus_handle(cx).focus(window)
|
||||
} else {
|
||||
self.branch_list_handle.toggle(window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CommitModal {
|
||||
@@ -373,24 +372,11 @@ impl Render for CommitModal {
|
||||
.key_context("GitCommit")
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.on_action(cx.listener(Self::commit))
|
||||
.on_action(cx.listener(|this, _: &GenerateCommitMessage, _, cx| {
|
||||
this.git_panel.update(cx, |panel, cx| {
|
||||
panel.generate_commit_message(cx);
|
||||
})
|
||||
}))
|
||||
.on_action(
|
||||
cx.listener(|this, _: &zed_actions::git::Branch, window, cx| {
|
||||
this.toggle_branch_selector(window, cx);
|
||||
}),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &zed_actions::git::CheckoutBranch, window, cx| {
|
||||
this.toggle_branch_selector(window, cx);
|
||||
}),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &zed_actions::git::Switch, window, cx| {
|
||||
this.toggle_branch_selector(window, cx);
|
||||
this.branch_list.update(cx, |branch_list, cx| {
|
||||
branch_list.popover_handle.toggle(window, cx);
|
||||
})
|
||||
}),
|
||||
)
|
||||
.elevation_3(cx)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -58,22 +58,15 @@ pub struct GitPanelSettingsContent {
|
||||
///
|
||||
/// Default: inherits editor scrollbar settings
|
||||
pub scrollbar: Option<ScrollbarSettings>,
|
||||
|
||||
/// What the default branch name should be when
|
||||
/// `init.defaultBranch` is not set in git
|
||||
///
|
||||
/// Default: main
|
||||
pub fallback_branch_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq)]
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct GitPanelSettings {
|
||||
pub button: bool,
|
||||
pub dock: DockPosition,
|
||||
pub default_width: Pixels,
|
||||
pub status_style: StatusStyle,
|
||||
pub scrollbar: ScrollbarSettings,
|
||||
pub fallback_branch_name: String,
|
||||
}
|
||||
|
||||
impl Settings for GitPanelSettings {
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
use std::any::Any;
|
||||
|
||||
use ::settings::Settings;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use git::{
|
||||
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||
status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
|
||||
};
|
||||
use git::status::FileStatus;
|
||||
use git_panel_settings::GitPanelSettings;
|
||||
use gpui::{actions, App, Entity, FocusHandle};
|
||||
use onboarding::{clear_dismissed, GitOnboardingModal};
|
||||
use project::Project;
|
||||
use gpui::App;
|
||||
use project_diff::ProjectDiff;
|
||||
use ui::prelude::*;
|
||||
use ui::{ActiveTheme, Color, Icon, IconName, IntoElement};
|
||||
use workspace::Workspace;
|
||||
|
||||
mod askpass_modal;
|
||||
@@ -19,14 +11,11 @@ pub mod branch_picker;
|
||||
mod commit_modal;
|
||||
pub mod git_panel;
|
||||
mod git_panel_settings;
|
||||
pub mod onboarding;
|
||||
pub mod picker_prompt;
|
||||
pub mod project_diff;
|
||||
pub(crate) mod remote_output;
|
||||
mod remote_output_toast;
|
||||
pub mod repository_selector;
|
||||
|
||||
actions!(git, [ResetOnboarding]);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
GitPanelSettings::register(cx);
|
||||
branch_picker::init(cx);
|
||||
@@ -36,516 +25,67 @@ pub fn init(cx: &mut App) {
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, _, cx| {
|
||||
let project = workspace.project().read(cx);
|
||||
if project.is_read_only(cx) {
|
||||
if project.is_via_collab() {
|
||||
return;
|
||||
}
|
||||
if !project.is_via_collab() {
|
||||
workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.fetch(window, cx);
|
||||
});
|
||||
});
|
||||
workspace.register_action(|workspace, _: &git::Push, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.push(false, window, cx);
|
||||
});
|
||||
});
|
||||
workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.push(true, window, cx);
|
||||
});
|
||||
});
|
||||
workspace.register_action(|workspace, _: &git::Pull, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.pull(window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
workspace.register_action(|workspace, action: &git::StageAll, window, cx| {
|
||||
workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.stage_all(action, window, cx);
|
||||
panel.fetch(window, cx);
|
||||
});
|
||||
});
|
||||
workspace.register_action(|workspace, action: &git::UnstageAll, window, cx| {
|
||||
workspace.register_action(|workspace, _: &git::Push, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.unstage_all(action, window, cx);
|
||||
panel.push(false, window, cx);
|
||||
});
|
||||
});
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_action_types(&[
|
||||
zed_actions::OpenGitIntegrationOnboarding.type_id(),
|
||||
// ResetOnboarding.type_id(),
|
||||
]);
|
||||
});
|
||||
workspace.register_action(
|
||||
move |workspace, _: &zed_actions::OpenGitIntegrationOnboarding, window, cx| {
|
||||
GitOnboardingModal::toggle(workspace, window, cx)
|
||||
},
|
||||
);
|
||||
workspace.register_action(move |_, _: &ResetOnboarding, window, cx| {
|
||||
clear_dismissed(cx);
|
||||
window.refresh();
|
||||
});
|
||||
workspace.register_action(|workspace, _action: &git::Init, window, cx| {
|
||||
workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.git_init(window, cx);
|
||||
panel.push(true, window, cx);
|
||||
});
|
||||
});
|
||||
workspace.register_action(|workspace, _: &git::Pull, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.pull(window, cx);
|
||||
});
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
|
||||
GitStatusIcon::new(status)
|
||||
}
|
||||
|
||||
fn can_push_and_pull(project: &Entity<Project>, cx: &App) -> bool {
|
||||
!project.read(cx).is_via_collab()
|
||||
}
|
||||
|
||||
fn render_remote_button(
|
||||
id: impl Into<SharedString>,
|
||||
branch: &Branch,
|
||||
keybinding_target: Option<FocusHandle>,
|
||||
show_fetch_button: bool,
|
||||
) -> Option<impl IntoElement> {
|
||||
let id = id.into();
|
||||
let upstream = branch.upstream.as_ref();
|
||||
match upstream {
|
||||
Some(Upstream {
|
||||
tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }),
|
||||
..
|
||||
}) => match (*ahead, *behind) {
|
||||
(0, 0) if show_fetch_button => {
|
||||
Some(remote_button::render_fetch_button(keybinding_target, id))
|
||||
}
|
||||
(0, 0) => None,
|
||||
(ahead, 0) => Some(remote_button::render_push_button(
|
||||
keybinding_target.clone(),
|
||||
id,
|
||||
ahead,
|
||||
)),
|
||||
(ahead, behind) => Some(remote_button::render_pull_button(
|
||||
keybinding_target.clone(),
|
||||
id,
|
||||
ahead,
|
||||
behind,
|
||||
)),
|
||||
},
|
||||
Some(Upstream {
|
||||
tracking: UpstreamTracking::Gone,
|
||||
..
|
||||
}) => Some(remote_button::render_republish_button(
|
||||
keybinding_target,
|
||||
id,
|
||||
)),
|
||||
None => Some(remote_button::render_publish_button(keybinding_target, id)),
|
||||
}
|
||||
}
|
||||
|
||||
mod remote_button {
|
||||
use gpui::{hsla, point, Action, AnyView, BoxShadow, ClickEvent, Corner, FocusHandle};
|
||||
use ui::{
|
||||
div, h_flex, px, rems, ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, Clickable,
|
||||
ContextMenu, ElementId, ElevationIndex, FluentBuilder, Icon, IconName, IconSize,
|
||||
IntoElement, Label, LabelCommon, LabelSize, LineHeightStyle, ParentElement, PopoverMenu,
|
||||
RenderOnce, SharedString, Styled, Tooltip, Window,
|
||||
// TODO: Add updated status colors to theme
|
||||
pub fn git_status_icon(status: FileStatus, cx: &App) -> impl IntoElement {
|
||||
let (icon_name, color) = if status.is_conflicted() {
|
||||
(
|
||||
IconName::Warning,
|
||||
cx.theme().colors().version_control_conflict,
|
||||
)
|
||||
} else if status.is_deleted() {
|
||||
(
|
||||
IconName::SquareMinus,
|
||||
cx.theme().colors().version_control_deleted,
|
||||
)
|
||||
} else if status.is_modified() {
|
||||
(
|
||||
IconName::SquareDot,
|
||||
cx.theme().colors().version_control_modified,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
IconName::SquarePlus,
|
||||
cx.theme().colors().version_control_added,
|
||||
)
|
||||
};
|
||||
|
||||
pub fn render_fetch_button(
|
||||
keybinding_target: Option<FocusHandle>,
|
||||
id: SharedString,
|
||||
) -> SplitButton {
|
||||
SplitButton::new(
|
||||
id,
|
||||
"Fetch",
|
||||
0,
|
||||
0,
|
||||
Some(IconName::ArrowCircle),
|
||||
keybinding_target.clone(),
|
||||
move |_, window, cx| {
|
||||
window.dispatch_action(Box::new(git::Fetch), cx);
|
||||
},
|
||||
move |window, cx| {
|
||||
git_action_tooltip(
|
||||
"Fetch updates from remote",
|
||||
&git::Fetch,
|
||||
"git fetch",
|
||||
keybinding_target.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_push_button(
|
||||
keybinding_target: Option<FocusHandle>,
|
||||
id: SharedString,
|
||||
ahead: u32,
|
||||
) -> SplitButton {
|
||||
SplitButton::new(
|
||||
id,
|
||||
"Push",
|
||||
ahead as usize,
|
||||
0,
|
||||
None,
|
||||
keybinding_target.clone(),
|
||||
move |_, window, cx| {
|
||||
window.dispatch_action(Box::new(git::Push), cx);
|
||||
},
|
||||
move |window, cx| {
|
||||
git_action_tooltip(
|
||||
"Push committed changes to remote",
|
||||
&git::Push,
|
||||
"git push",
|
||||
keybinding_target.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_pull_button(
|
||||
keybinding_target: Option<FocusHandle>,
|
||||
id: SharedString,
|
||||
ahead: u32,
|
||||
behind: u32,
|
||||
) -> SplitButton {
|
||||
SplitButton::new(
|
||||
id,
|
||||
"Pull",
|
||||
ahead as usize,
|
||||
behind as usize,
|
||||
None,
|
||||
keybinding_target.clone(),
|
||||
move |_, window, cx| {
|
||||
window.dispatch_action(Box::new(git::Pull), cx);
|
||||
},
|
||||
move |window, cx| {
|
||||
git_action_tooltip(
|
||||
"Pull",
|
||||
&git::Pull,
|
||||
"git pull",
|
||||
keybinding_target.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_publish_button(
|
||||
keybinding_target: Option<FocusHandle>,
|
||||
id: SharedString,
|
||||
) -> SplitButton {
|
||||
SplitButton::new(
|
||||
id,
|
||||
"Publish",
|
||||
0,
|
||||
0,
|
||||
Some(IconName::ArrowUpFromLine),
|
||||
keybinding_target.clone(),
|
||||
move |_, window, cx| {
|
||||
window.dispatch_action(Box::new(git::Push), cx);
|
||||
},
|
||||
move |window, cx| {
|
||||
git_action_tooltip(
|
||||
"Publish branch to remote",
|
||||
&git::Push,
|
||||
"git push --set-upstream",
|
||||
keybinding_target.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_republish_button(
|
||||
keybinding_target: Option<FocusHandle>,
|
||||
id: SharedString,
|
||||
) -> SplitButton {
|
||||
SplitButton::new(
|
||||
id,
|
||||
"Republish",
|
||||
0,
|
||||
0,
|
||||
Some(IconName::ArrowUpFromLine),
|
||||
keybinding_target.clone(),
|
||||
move |_, window, cx| {
|
||||
window.dispatch_action(Box::new(git::Push), cx);
|
||||
},
|
||||
move |window, cx| {
|
||||
git_action_tooltip(
|
||||
"Re-publish branch to remote",
|
||||
&git::Push,
|
||||
"git push --set-upstream",
|
||||
keybinding_target.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn git_action_tooltip(
|
||||
label: impl Into<SharedString>,
|
||||
action: &dyn Action,
|
||||
command: impl Into<SharedString>,
|
||||
focus_handle: Option<FocusHandle>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyView {
|
||||
let label = label.into();
|
||||
let command = command.into();
|
||||
|
||||
if let Some(handle) = focus_handle {
|
||||
Tooltip::with_meta_in(
|
||||
label.clone(),
|
||||
Some(action),
|
||||
command.clone(),
|
||||
&handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
Tooltip::with_meta(label.clone(), Some(action), command.clone(), window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_git_action_menu(
|
||||
id: impl Into<ElementId>,
|
||||
keybinding_target: Option<FocusHandle>,
|
||||
) -> impl IntoElement {
|
||||
PopoverMenu::new(id.into())
|
||||
.trigger(
|
||||
ui::ButtonLike::new_rounded_right("split-button-right")
|
||||
.layer(ui::ElevationIndex::ModalSurface)
|
||||
.size(ui::ButtonSize::None)
|
||||
.child(
|
||||
div()
|
||||
.px_1()
|
||||
.child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
|
||||
),
|
||||
)
|
||||
.menu(move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |context_menu, _, _| {
|
||||
context_menu
|
||||
.when_some(keybinding_target.clone(), |el, keybinding_target| {
|
||||
el.context(keybinding_target.clone())
|
||||
})
|
||||
.action("Fetch", git::Fetch.boxed_clone())
|
||||
.action("Pull", git::Pull.boxed_clone())
|
||||
.separator()
|
||||
.action("Push", git::Push.boxed_clone())
|
||||
.action("Force Push", git::ForcePush.boxed_clone())
|
||||
}))
|
||||
})
|
||||
.anchor(Corner::TopRight)
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct SplitButton {
|
||||
pub left: ButtonLike,
|
||||
pub right: AnyElement,
|
||||
}
|
||||
|
||||
impl SplitButton {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
id: impl Into<SharedString>,
|
||||
left_label: impl Into<SharedString>,
|
||||
ahead_count: usize,
|
||||
behind_count: usize,
|
||||
left_icon: Option<IconName>,
|
||||
keybinding_target: Option<FocusHandle>,
|
||||
left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
) -> Self {
|
||||
let id = id.into();
|
||||
|
||||
fn count(count: usize) -> impl IntoElement {
|
||||
h_flex()
|
||||
.ml_neg_px()
|
||||
.h(rems(0.875))
|
||||
.items_center()
|
||||
.overflow_hidden()
|
||||
.px_0p5()
|
||||
.child(
|
||||
Label::new(count.to_string())
|
||||
.size(LabelSize::XSmall)
|
||||
.line_height_style(LineHeightStyle::UiLabel),
|
||||
)
|
||||
}
|
||||
|
||||
let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0);
|
||||
|
||||
let left = ui::ButtonLike::new_rounded_left(ElementId::Name(
|
||||
format!("split-button-left-{}", id).into(),
|
||||
))
|
||||
.layer(ui::ElevationIndex::ModalSurface)
|
||||
.size(ui::ButtonSize::Compact)
|
||||
.when(should_render_counts, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.ml_neg_0p5()
|
||||
.mr_1()
|
||||
.when(behind_count > 0, |this| {
|
||||
this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall))
|
||||
.child(count(behind_count))
|
||||
})
|
||||
.when(ahead_count > 0, |this| {
|
||||
this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall))
|
||||
.child(count(ahead_count))
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(left_icon, |this, left_icon| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.ml_neg_0p5()
|
||||
.mr_1()
|
||||
.child(Icon::new(left_icon).size(IconSize::XSmall)),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.child(Label::new(left_label).size(LabelSize::Small))
|
||||
.mr_0p5(),
|
||||
)
|
||||
.on_click(left_on_click)
|
||||
.tooltip(tooltip);
|
||||
|
||||
let right = render_git_action_menu(
|
||||
ElementId::Name(format!("split-button-right-{}", id).into()),
|
||||
keybinding_target,
|
||||
)
|
||||
.into_any_element();
|
||||
|
||||
Self { left, right }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SplitButton {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
h_flex()
|
||||
.rounded_sm()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().text_muted.alpha(0.12))
|
||||
.child(div().flex_grow().child(self.left))
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
.w_px()
|
||||
.bg(cx.theme().colors().text_muted.alpha(0.16)),
|
||||
)
|
||||
.child(self.right)
|
||||
.bg(ElevationIndex::Surface.on_elevation_bg(cx))
|
||||
.shadow(smallvec::smallvec![BoxShadow {
|
||||
color: hsla(0.0, 0.0, 0.0, 0.16),
|
||||
offset: point(px(0.), px(1.)),
|
||||
blur_radius: px(0.),
|
||||
spread_radius: px(0.),
|
||||
}])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement, IntoComponent)]
|
||||
#[component(scope = "Version Control")]
|
||||
pub struct GitStatusIcon {
|
||||
status: FileStatus,
|
||||
}
|
||||
|
||||
impl GitStatusIcon {
|
||||
pub fn new(status: FileStatus) -> Self {
|
||||
Self { status }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for GitStatusIcon {
|
||||
fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
|
||||
let status = self.status;
|
||||
|
||||
let (icon_name, color) = if status.is_conflicted() {
|
||||
(
|
||||
IconName::Warning,
|
||||
cx.theme().colors().version_control_conflict,
|
||||
)
|
||||
} else if status.is_deleted() {
|
||||
(
|
||||
IconName::SquareMinus,
|
||||
cx.theme().colors().version_control_deleted,
|
||||
)
|
||||
} else if status.is_modified() {
|
||||
(
|
||||
IconName::SquareDot,
|
||||
cx.theme().colors().version_control_modified,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
IconName::SquarePlus,
|
||||
cx.theme().colors().version_control_added,
|
||||
)
|
||||
};
|
||||
|
||||
Icon::new(icon_name).color(Color::Custom(color))
|
||||
}
|
||||
}
|
||||
|
||||
// View this component preview using `workspace: open component-preview`
|
||||
impl ComponentPreview for GitStatusIcon {
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
|
||||
fn tracked_file_status(code: StatusCode) -> FileStatus {
|
||||
FileStatus::Tracked(git::status::TrackedStatus {
|
||||
index_status: code,
|
||||
worktree_status: code,
|
||||
})
|
||||
}
|
||||
|
||||
let modified = tracked_file_status(StatusCode::Modified);
|
||||
let added = tracked_file_status(StatusCode::Added);
|
||||
let deleted = tracked_file_status(StatusCode::Deleted);
|
||||
let conflict = UnmergedStatus {
|
||||
first_head: UnmergedStatusCode::Updated,
|
||||
second_head: UnmergedStatusCode::Updated,
|
||||
}
|
||||
.into();
|
||||
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.children(vec![example_group(vec![
|
||||
single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
|
||||
single_example("Added", GitStatusIcon::new(added).into_any_element()),
|
||||
single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
|
||||
single_example(
|
||||
"Conflicted",
|
||||
GitStatusIcon::new(conflict).into_any_element(),
|
||||
),
|
||||
])])
|
||||
.into_any_element()
|
||||
}
|
||||
Icon::new(icon_name).color(Color::Custom(color))
|
||||
}
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
use gpui::{
|
||||
svg, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Global,
|
||||
MouseDownEvent, Render,
|
||||
};
|
||||
use ui::{prelude::*, ButtonLike, TintColor, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::git_panel::GitPanel;
|
||||
|
||||
macro_rules! git_onboarding_event {
|
||||
($name:expr) => {
|
||||
telemetry::event!($name, source = "Git Onboarding");
|
||||
};
|
||||
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
|
||||
telemetry::event!($name, source = "Git Onboarding", $($key $(= $value)?),+);
|
||||
};
|
||||
}
|
||||
|
||||
/// Introduces user to the Git Panel and overall improved Git support
|
||||
pub struct GitOnboardingModal {
|
||||
focus_handle: FocusHandle,
|
||||
workspace: Entity<Workspace>,
|
||||
}
|
||||
|
||||
impl GitOnboardingModal {
|
||||
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
|
||||
let workspace_entity = cx.entity();
|
||||
workspace.toggle_modal(window, cx, |_window, cx| Self {
|
||||
workspace: workspace_entity,
|
||||
focus_handle: cx.focus_handle(),
|
||||
});
|
||||
}
|
||||
|
||||
fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.workspace.update(cx, |workspace, cx| {
|
||||
workspace.focus_panel::<GitPanel>(window, cx);
|
||||
});
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
|
||||
git_onboarding_event!("Open Panel Clicked");
|
||||
}
|
||||
|
||||
fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.open_url("https://zed.dev/blog/git");
|
||||
cx.notify();
|
||||
|
||||
git_onboarding_event!("Blog Link Clicked");
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for GitOnboardingModal {}
|
||||
|
||||
impl Focusable for GitOnboardingModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for GitOnboardingModal {}
|
||||
|
||||
impl Render for GitOnboardingModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let window_height = window.viewport_size().height;
|
||||
let max_height = window_height - px(200.);
|
||||
|
||||
let base = v_flex()
|
||||
.id("git-onboarding")
|
||||
.key_context("GitOnboardingModal")
|
||||
.relative()
|
||||
.w(px(450.))
|
||||
.h_full()
|
||||
.max_h(max_height)
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.elevation_3(cx)
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.overflow_hidden()
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
|
||||
git_onboarding_event!("Cancelled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(
|
||||
div().p_1p5().absolute().inset_0().h(px(160.)).child(
|
||||
svg()
|
||||
.path("icons/git_onboarding_bg.svg")
|
||||
.text_color(cx.theme().colors().icon_disabled)
|
||||
.w(px(420.))
|
||||
.h(px(128.))
|
||||
.overflow_hidden(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Introducing")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Headline::new("Native Git Support").size(HeadlineSize::Large)),
|
||||
)
|
||||
.child(h_flex().absolute().top_2().right_2().child(
|
||||
IconButton::new("cancel", IconName::X).on_click(cx.listener(
|
||||
|_, _: &ClickEvent, _window, cx| {
|
||||
git_onboarding_event!("Cancelled", trigger = "X click");
|
||||
cx.emit(DismissEvent);
|
||||
},
|
||||
)),
|
||||
));
|
||||
|
||||
let open_panel_button = Button::new("open-panel", "Get Started with the Git Panel")
|
||||
.icon_size(IconSize::Indicator)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::open_panel));
|
||||
|
||||
let blog_post_button = Button::new("view-blog", "Check out the Blog Post")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::view_blog));
|
||||
|
||||
let copy = "First-class support for staging, committing, pulling, pushing, viewing diffs, and more. All without leaving Zed.";
|
||||
|
||||
base.child(Label::new(copy).color(Color::Muted)).child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.mt_2()
|
||||
.gap_2()
|
||||
.child(open_panel_button)
|
||||
.child(blog_post_button),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompts the user to try Zed's git features
|
||||
pub struct GitBanner {
|
||||
dismissed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct GitBannerGlobal(Entity<GitBanner>);
|
||||
impl Global for GitBannerGlobal {}
|
||||
|
||||
impl GitBanner {
|
||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||
cx.set_global(GitBannerGlobal(cx.entity()));
|
||||
Self {
|
||||
dismissed: get_dismissed(),
|
||||
}
|
||||
}
|
||||
|
||||
fn should_show(&self, _cx: &mut App) -> bool {
|
||||
!self.dismissed
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, cx: &mut Context<Self>) {
|
||||
git_onboarding_event!("Banner Dismissed");
|
||||
persist_dismissed(cx);
|
||||
self.dismissed = true;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
const DISMISSED_AT_KEY: &str = "zed_git_banner_dismissed_at";
|
||||
|
||||
fn get_dismissed() -> bool {
|
||||
db::kvp::KEY_VALUE_STORE
|
||||
.read_kvp(DISMISSED_AT_KEY)
|
||||
.log_err()
|
||||
.map_or(false, |dismissed| dismissed.is_some())
|
||||
}
|
||||
|
||||
fn persist_dismissed(cx: &mut App) {
|
||||
cx.spawn(|_| {
|
||||
let time = chrono::Utc::now().to_rfc3339();
|
||||
db::kvp::KEY_VALUE_STORE.write_kvp(DISMISSED_AT_KEY.into(), time)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub(crate) fn clear_dismissed(cx: &mut App) {
|
||||
cx.defer(|cx| {
|
||||
cx.global::<GitBannerGlobal>()
|
||||
.clone()
|
||||
.0
|
||||
.update(cx, |this, cx| {
|
||||
this.dismissed = false;
|
||||
cx.notify();
|
||||
});
|
||||
});
|
||||
|
||||
cx.spawn(|_| db::kvp::KEY_VALUE_STORE.delete_kvp(DISMISSED_AT_KEY.into()))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
impl Render for GitBanner {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if !self.should_show(cx) {
|
||||
return div();
|
||||
}
|
||||
|
||||
let border_color = cx.theme().colors().editor_foreground.opacity(0.3);
|
||||
let banner = h_flex()
|
||||
.rounded_sm()
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.child(
|
||||
ButtonLike::new("try-git")
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::GitBranchSmall).size(IconSize::Small))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new("Introducing:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("Git Support").size(LabelSize::Small)),
|
||||
),
|
||||
)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
git_onboarding_event!("Banner Clicked");
|
||||
this.dismiss(cx);
|
||||
window.dispatch_action(
|
||||
Box::new(zed_actions::OpenGitIntegrationOnboarding),
|
||||
cx,
|
||||
)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div().border_l_1().border_color(border_color).child(
|
||||
IconButton::new("close", IconName::Close)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.on_click(cx.listener(|this, _, _window, cx| this.dismiss(cx)))
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Close Announcement Banner",
|
||||
None,
|
||||
"It won't show again for this feature",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
div().pr_2().child(banner)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::channel::oneshot;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
|
||||
@@ -25,9 +26,9 @@ pub fn prompt(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Option<usize>> {
|
||||
) -> Task<Result<Option<usize>>> {
|
||||
if options.is_empty() {
|
||||
return Task::ready(None);
|
||||
return Task::ready(Err(anyhow!("No options")));
|
||||
}
|
||||
let prompt = prompt.to_string().into();
|
||||
|
||||
@@ -36,17 +37,15 @@ pub fn prompt(
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let delegate = PickerPromptDelegate::new(prompt, options, tx, 70);
|
||||
|
||||
workspace
|
||||
.update_in(&mut cx, |workspace, window, cx| {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
PickerPrompt::new(delegate, 34., window, cx)
|
||||
})
|
||||
workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
PickerPrompt::new(delegate, 34., window, cx)
|
||||
})
|
||||
.ok();
|
||||
})?;
|
||||
|
||||
match rx.await {
|
||||
Ok(selection) => Some(selection),
|
||||
Err(_) => None, // User cancelled
|
||||
Ok(selection) => Some(selection).transpose(),
|
||||
Err(_) => anyhow::Ok(None), // User cancelled
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -95,14 +94,14 @@ pub struct PickerPromptDelegate {
|
||||
all_options: Vec<SharedString>,
|
||||
selected_index: usize,
|
||||
max_match_length: usize,
|
||||
tx: Option<oneshot::Sender<usize>>,
|
||||
tx: Option<oneshot::Sender<Result<usize>>>,
|
||||
}
|
||||
|
||||
impl PickerPromptDelegate {
|
||||
pub fn new(
|
||||
prompt: Arc<str>,
|
||||
options: Vec<SharedString>,
|
||||
tx: oneshot::Sender<usize>,
|
||||
tx: oneshot::Sender<Result<usize>>,
|
||||
max_chars: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -201,7 +200,7 @@ impl PickerDelegate for PickerPromptDelegate {
|
||||
return;
|
||||
};
|
||||
|
||||
self.tx.take().map(|tx| tx.send(option.candidate_id));
|
||||
self.tx.take().map(|tx| tx.send(Ok(option.candidate_id)));
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use crate::{
|
||||
git_panel::{GitPanel, GitPanelAddon, GitStatusEntry},
|
||||
remote_button::{render_publish_button, render_push_button},
|
||||
};
|
||||
use crate::git_panel::{GitPanel, GitPanelAddon, GitStatusEntry};
|
||||
use anyhow::Result;
|
||||
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
|
||||
use collections::HashSet;
|
||||
@@ -10,11 +7,10 @@ use editor::{
|
||||
scroll::Autoscroll,
|
||||
Editor, EditorEvent,
|
||||
};
|
||||
use feature_flags::FeatureFlagViewExt;
|
||||
use futures::StreamExt;
|
||||
use git::{
|
||||
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||
status::FileStatus,
|
||||
Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
|
||||
status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
|
||||
};
|
||||
use gpui::{
|
||||
actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity,
|
||||
@@ -28,27 +24,27 @@ use project::{
|
||||
};
|
||||
use std::any::{Any, TypeId};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{prelude::*, vertical_divider, KeyBinding, Tooltip};
|
||||
use ui::{prelude::*, vertical_divider, Tooltip};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||
searchable::SearchableItemHandle,
|
||||
CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation,
|
||||
ToolbarItemView, Workspace,
|
||||
ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
|
||||
Workspace,
|
||||
};
|
||||
|
||||
actions!(git, [Diff, Add]);
|
||||
actions!(git, [Diff]);
|
||||
|
||||
pub struct ProjectDiff {
|
||||
project: Entity<Project>,
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
editor: Entity<Editor>,
|
||||
project: Entity<Project>,
|
||||
git_store: Entity<GitStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
update_needed: postage::watch::Sender<()>,
|
||||
pending_scroll: Option<PathKey>,
|
||||
current_branch: Option<Branch>,
|
||||
|
||||
_task: Task<Result<()>>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
@@ -67,13 +63,13 @@ const NEW_NAMESPACE: &'static str = "2";
|
||||
|
||||
impl ProjectDiff {
|
||||
pub(crate) fn register(
|
||||
workspace: &mut Workspace,
|
||||
_window: Option<&mut Window>,
|
||||
_: &mut Workspace,
|
||||
window: Option<&mut Window>,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.register_action(Self::deploy);
|
||||
workspace.register_action(|workspace, _: &Add, window, cx| {
|
||||
Self::deploy(workspace, &Diff, window, cx);
|
||||
let Some(window) = window else { return };
|
||||
cx.when_flag_enabled::<feature_flags::GitUiFeatureFlag>(window, |workspace, _, _cx| {
|
||||
workspace.register_action(Self::deploy);
|
||||
});
|
||||
|
||||
workspace::register_serializable_item::<ProjectDiff>(cx);
|
||||
@@ -125,12 +121,6 @@ impl ProjectDiff {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn autoscroll(&self, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.request_autoscroll(Autoscroll::fit(), cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn new(
|
||||
project: Entity<Project>,
|
||||
workspace: Entity<Workspace>,
|
||||
@@ -148,7 +138,6 @@ impl ProjectDiff {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
diff_display_editor.disable_inline_diagnostics();
|
||||
diff_display_editor.set_expand_all_diff_hunks(cx);
|
||||
diff_display_editor.register_addon(GitPanelAddon {
|
||||
workspace: workspace.downgrade(),
|
||||
@@ -177,7 +166,7 @@ impl ProjectDiff {
|
||||
let this = cx.weak_entity();
|
||||
|cx| Self::handle_status_updates(this, recv, cx)
|
||||
});
|
||||
// Kick of a refresh immediately
|
||||
// Kick off a refresh immediately
|
||||
*send.borrow_mut() = ();
|
||||
|
||||
Self {
|
||||
@@ -189,7 +178,6 @@ impl ProjectDiff {
|
||||
multibuffer,
|
||||
pending_scroll: None,
|
||||
update_needed: send,
|
||||
current_branch: None,
|
||||
_task: worker,
|
||||
_subscription: git_store_subscription,
|
||||
}
|
||||
@@ -278,7 +266,7 @@ impl ProjectDiff {
|
||||
has_staged_hunks = true;
|
||||
has_unstaged_hunks = true;
|
||||
}
|
||||
DiffHunkSecondaryStatus::NoSecondaryHunk
|
||||
DiffHunkSecondaryStatus::None
|
||||
| DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
|
||||
has_staged_hunks = true;
|
||||
}
|
||||
@@ -308,7 +296,7 @@ impl ProjectDiff {
|
||||
|
||||
fn handle_editor_event(
|
||||
&mut self,
|
||||
editor: &Entity<Editor>,
|
||||
_: &Entity<Editor>,
|
||||
event: &EditorEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -330,11 +318,6 @@ 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>>> {
|
||||
@@ -460,20 +443,6 @@ impl ProjectDiff {
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
while let Some(_) = recv.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let new_branch =
|
||||
this.git_store
|
||||
.read(cx)
|
||||
.active_repository()
|
||||
.and_then(|active_repository| {
|
||||
active_repository.read(cx).current_branch().cloned()
|
||||
});
|
||||
if new_branch != this.current_branch {
|
||||
this.current_branch = new_branch;
|
||||
cx.notify();
|
||||
}
|
||||
})?;
|
||||
|
||||
let buffers_to_load = this.update(&mut cx, |this, cx| this.load_buffers(cx))?;
|
||||
for buffer_to_load in buffers_to_load {
|
||||
if let Some(buffer) = buffer_to_load.await.log_err() {
|
||||
@@ -483,10 +452,7 @@ impl ProjectDiff {
|
||||
})?;
|
||||
}
|
||||
}
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.pending_scroll.take();
|
||||
cx.notify();
|
||||
})?;
|
||||
this.update(&mut cx, |this, _| this.pending_scroll.take())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -675,11 +641,9 @@ impl Item for ProjectDiff {
|
||||
}
|
||||
|
||||
impl Render for ProjectDiff {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let is_empty = self.multibuffer.read(cx).is_empty();
|
||||
|
||||
let can_push_and_pull = crate::can_push_and_pull(&self.project, cx);
|
||||
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.key_context(if is_empty { "EmptyPane" } else { "GitDiff" })
|
||||
@@ -689,61 +653,7 @@ impl Render for ProjectDiff {
|
||||
.justify_center()
|
||||
.size_full()
|
||||
.when(is_empty, |el| {
|
||||
el.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_around()
|
||||
.child(Label::new("No uncommitted changes")),
|
||||
)
|
||||
.when(can_push_and_pull, |this_div| {
|
||||
let keybinding_focus_handle = self.focus_handle(cx);
|
||||
|
||||
this_div.when_some(self.current_branch.as_ref(), |this_div, branch| {
|
||||
let remote_button = crate::render_remote_button(
|
||||
"project-diff-remote-button",
|
||||
branch,
|
||||
Some(keybinding_focus_handle.clone()),
|
||||
false,
|
||||
);
|
||||
|
||||
match remote_button {
|
||||
Some(button) => {
|
||||
this_div.child(h_flex().justify_around().child(button))
|
||||
}
|
||||
None => this_div.child(
|
||||
h_flex()
|
||||
.justify_around()
|
||||
.child(Label::new("Remote up to date")),
|
||||
),
|
||||
}
|
||||
})
|
||||
})
|
||||
.map(|this| {
|
||||
let keybinding_focus_handle = self.focus_handle(cx).clone();
|
||||
|
||||
this.child(
|
||||
h_flex().justify_around().mt_1().child(
|
||||
Button::new("project-diff-close-button", "Close")
|
||||
// .style(ButtonStyle::Transparent)
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&CloseActiveItem::default(),
|
||||
&keybinding_focus_handle,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_click(move |_, window, cx| {
|
||||
window.focus(&keybinding_focus_handle);
|
||||
window.dispatch_action(
|
||||
Box::new(CloseActiveItem::default()),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
el.child(Label::new("No uncommitted changes"))
|
||||
})
|
||||
.when(!is_empty, |el| el.child(self.editor.clone()))
|
||||
}
|
||||
@@ -820,30 +730,23 @@ impl ProjectDiffToolbar {
|
||||
cx.dispatch_action(action.as_ref());
|
||||
})
|
||||
}
|
||||
|
||||
fn stage_all(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn dispatch_panel_action(
|
||||
&self,
|
||||
action: &dyn Action,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
.read_with(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<GitPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.stage_all(&Default::default(), window, cx);
|
||||
});
|
||||
panel.focus_handle(cx).focus(window)
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn unstage_all(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let Some(panel) = workspace.panel::<GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.unstage_all(&Default::default(), window, cx);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
let action = action.boxed_clone();
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(action.as_ref());
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -997,7 +900,7 @@ impl Render for ProjectDiffToolbar {
|
||||
&focus_handle,
|
||||
))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.unstage_all(window, cx)
|
||||
this.dispatch_panel_action(&UnstageAll, window, cx)
|
||||
})),
|
||||
)
|
||||
},
|
||||
@@ -1017,7 +920,7 @@ impl Render for ProjectDiffToolbar {
|
||||
&focus_handle,
|
||||
))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.stage_all(window, cx)
|
||||
this.dispatch_panel_action(&StageAll, window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
@@ -1038,287 +941,6 @@ impl Render for ProjectDiffToolbar {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement, IntoComponent)]
|
||||
#[component(scope = "Version Control")]
|
||||
pub struct ProjectDiffEmptyState {
|
||||
pub no_repo: bool,
|
||||
pub can_push_and_pull: bool,
|
||||
pub focus_handle: Option<FocusHandle>,
|
||||
pub current_branch: Option<Branch>,
|
||||
// has_pending_commits: bool,
|
||||
// ahead_of_remote: bool,
|
||||
// no_git_repository: bool,
|
||||
}
|
||||
|
||||
impl RenderOnce for ProjectDiffEmptyState {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let status_against_remote = |ahead_by: usize, behind_by: usize| -> bool {
|
||||
match self.current_branch {
|
||||
Some(Branch {
|
||||
upstream:
|
||||
Some(Upstream {
|
||||
tracking:
|
||||
UpstreamTracking::Tracked(UpstreamTrackingStatus {
|
||||
ahead, behind, ..
|
||||
}),
|
||||
..
|
||||
}),
|
||||
..
|
||||
}) if (ahead > 0) == (ahead_by > 0) && (behind > 0) == (behind_by > 0) => true,
|
||||
_ => false,
|
||||
}
|
||||
};
|
||||
|
||||
let change_count = |current_branch: &Branch| -> (usize, usize) {
|
||||
match current_branch {
|
||||
Branch {
|
||||
upstream:
|
||||
Some(Upstream {
|
||||
tracking:
|
||||
UpstreamTracking::Tracked(UpstreamTrackingStatus {
|
||||
ahead, behind, ..
|
||||
}),
|
||||
..
|
||||
}),
|
||||
..
|
||||
} => (*ahead as usize, *behind as usize),
|
||||
_ => (0, 0),
|
||||
}
|
||||
};
|
||||
|
||||
let not_ahead_or_behind = status_against_remote(0, 0);
|
||||
let ahead_of_remote = status_against_remote(1, 0);
|
||||
let branch_not_on_remote = if let Some(branch) = self.current_branch.as_ref() {
|
||||
branch.upstream.is_none()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let has_branch_container = |branch: &Branch| {
|
||||
h_flex()
|
||||
.max_w(px(420.))
|
||||
.bg(cx.theme().colors().text.opacity(0.05))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_sm()
|
||||
.gap_8()
|
||||
.px_6()
|
||||
.py_4()
|
||||
.map(|this| {
|
||||
if ahead_of_remote {
|
||||
let ahead_count = change_count(branch).0;
|
||||
let ahead_string = format!("{} Commits Ahead", ahead_count);
|
||||
this.child(
|
||||
v_flex()
|
||||
.child(Headline::new(ahead_string).size(HeadlineSize::Small))
|
||||
.child(
|
||||
Label::new(format!("Push your changes to {}", branch.name))
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(div().child(render_push_button(
|
||||
self.focus_handle,
|
||||
"push".into(),
|
||||
ahead_count as u32,
|
||||
)))
|
||||
} else if branch_not_on_remote {
|
||||
this.child(
|
||||
v_flex()
|
||||
.child(Headline::new("Publish Branch").size(HeadlineSize::Small))
|
||||
.child(
|
||||
Label::new(format!("Create {} on remote", branch.name))
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().child(render_publish_button(self.focus_handle, "publish".into())),
|
||||
)
|
||||
} else {
|
||||
this.child(Label::new("Remote status unknown").color(Color::Muted))
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
v_flex().size_full().items_center().justify_center().child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.when(self.no_repo, |this| {
|
||||
// TODO: add git init
|
||||
this.text_center()
|
||||
.child(Label::new("No Repository").color(Color::Muted))
|
||||
})
|
||||
.map(|this| {
|
||||
if not_ahead_or_behind && self.current_branch.is_some() {
|
||||
this.text_center()
|
||||
.child(Label::new("No Changes").color(Color::Muted))
|
||||
} else {
|
||||
this.when_some(self.current_branch.as_ref(), |this, branch| {
|
||||
this.child(has_branch_container(&branch))
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// .when(self.can_push_and_pull, |this| {
|
||||
// let remote_button = crate::render_remote_button(
|
||||
// "project-diff-remote-button",
|
||||
// &branch,
|
||||
// self.focus_handle.clone(),
|
||||
// false,
|
||||
// );
|
||||
|
||||
// match remote_button {
|
||||
// Some(button) => {
|
||||
// this.child(h_flex().justify_around().child(button))
|
||||
// }
|
||||
// None => this.child(
|
||||
// h_flex()
|
||||
// .justify_around()
|
||||
// .child(Label::new("Remote up to date")),
|
||||
// ),
|
||||
// }
|
||||
// }),
|
||||
//
|
||||
// // .map(|this| {
|
||||
// this.child(h_flex().justify_around().mt_1().child(
|
||||
// Button::new("project-diff-close-button", "Close").when_some(
|
||||
// self.focus_handle.clone(),
|
||||
// |this, focus_handle| {
|
||||
// this.key_binding(KeyBinding::for_action_in(
|
||||
// &CloseActiveItem::default(),
|
||||
// &focus_handle,
|
||||
// window,
|
||||
// cx,
|
||||
// ))
|
||||
// .on_click(move |_, window, cx| {
|
||||
// window.focus(&focus_handle);
|
||||
// window
|
||||
// .dispatch_action(Box::new(CloseActiveItem::default()), cx);
|
||||
// })
|
||||
// },
|
||||
// ),
|
||||
// ))
|
||||
// }),
|
||||
|
||||
mod preview {
|
||||
use git::repository::{
|
||||
Branch, CommitSummary, Upstream, UpstreamTracking, UpstreamTrackingStatus,
|
||||
};
|
||||
use ui::prelude::*;
|
||||
|
||||
use super::ProjectDiffEmptyState;
|
||||
|
||||
// View this component preview using `workspace: open component-preview`
|
||||
impl ComponentPreview for ProjectDiffEmptyState {
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
|
||||
let unknown_upstream: Option<UpstreamTracking> = None;
|
||||
let ahead_of_upstream: Option<UpstreamTracking> = Some(
|
||||
UpstreamTrackingStatus {
|
||||
ahead: 2,
|
||||
behind: 0,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
let not_ahead_or_behind_upstream: Option<UpstreamTracking> = Some(
|
||||
UpstreamTrackingStatus {
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
fn branch(upstream: Option<UpstreamTracking>) -> Branch {
|
||||
Branch {
|
||||
is_head: true,
|
||||
name: "some-branch".into(),
|
||||
upstream: upstream.map(|tracking| Upstream {
|
||||
ref_name: "origin/some-branch".into(),
|
||||
tracking,
|
||||
}),
|
||||
most_recent_commit: Some(CommitSummary {
|
||||
sha: "abc123".into(),
|
||||
subject: "Modify stuff".into(),
|
||||
commit_timestamp: 1710932954,
|
||||
has_parent: true,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
let no_repo_state = ProjectDiffEmptyState {
|
||||
no_repo: true,
|
||||
can_push_and_pull: false,
|
||||
focus_handle: None,
|
||||
current_branch: None,
|
||||
};
|
||||
|
||||
let no_changes_state = ProjectDiffEmptyState {
|
||||
no_repo: false,
|
||||
can_push_and_pull: true,
|
||||
focus_handle: None,
|
||||
current_branch: Some(branch(not_ahead_or_behind_upstream)),
|
||||
};
|
||||
|
||||
let ahead_of_upstream_state = ProjectDiffEmptyState {
|
||||
no_repo: false,
|
||||
can_push_and_pull: true,
|
||||
focus_handle: None,
|
||||
current_branch: Some(branch(ahead_of_upstream)),
|
||||
};
|
||||
|
||||
let unknown_upstream_state = ProjectDiffEmptyState {
|
||||
no_repo: false,
|
||||
can_push_and_pull: true,
|
||||
focus_handle: None,
|
||||
current_branch: Some(branch(unknown_upstream)),
|
||||
};
|
||||
|
||||
let (width, height) = (px(480.), px(320.));
|
||||
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.children(vec![example_group(vec![
|
||||
single_example(
|
||||
"No Repo",
|
||||
div()
|
||||
.w(width)
|
||||
.h(height)
|
||||
.child(no_repo_state)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"No Changes",
|
||||
div()
|
||||
.w(width)
|
||||
.h(height)
|
||||
.child(no_changes_state)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Unknown Upstream",
|
||||
div()
|
||||
.w(width)
|
||||
.h(height)
|
||||
.child(unknown_upstream_state)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Ahead of Remote",
|
||||
div()
|
||||
.w(width)
|
||||
.h(height)
|
||||
.child(ahead_of_upstream_state)
|
||||
.into_any_element(),
|
||||
),
|
||||
])
|
||||
.vertical()])
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
use anyhow::Context as _;
|
||||
use git::repository::{Remote, RemoteCommandOutput};
|
||||
use linkify::{LinkFinder, LinkKind};
|
||||
use ui::SharedString;
|
||||
use util::ResultExt as _;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum RemoteAction {
|
||||
Fetch,
|
||||
Pull(Remote),
|
||||
Push(SharedString, Remote),
|
||||
}
|
||||
|
||||
impl RemoteAction {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
RemoteAction::Fetch => "fetch",
|
||||
RemoteAction::Pull(_) => "pull",
|
||||
RemoteAction::Push(_, _) => "push",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SuccessStyle {
|
||||
Toast,
|
||||
ToastWithLog { output: RemoteCommandOutput },
|
||||
PushPrLink { link: String },
|
||||
}
|
||||
|
||||
pub struct SuccessMessage {
|
||||
pub message: String,
|
||||
pub style: SuccessStyle,
|
||||
}
|
||||
|
||||
pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> SuccessMessage {
|
||||
match action {
|
||||
RemoteAction::Fetch => {
|
||||
if output.stderr.is_empty() {
|
||||
SuccessMessage {
|
||||
message: "Already up to date".into(),
|
||||
style: SuccessStyle::Toast,
|
||||
}
|
||||
} else {
|
||||
SuccessMessage {
|
||||
message: "Synchronized with remotes".into(),
|
||||
style: SuccessStyle::ToastWithLog { output },
|
||||
}
|
||||
}
|
||||
}
|
||||
RemoteAction::Pull(remote_ref) => {
|
||||
let get_changes = |output: &RemoteCommandOutput| -> anyhow::Result<u32> {
|
||||
let last_line = output
|
||||
.stdout
|
||||
.lines()
|
||||
.last()
|
||||
.context("Failed to get last line of output")?
|
||||
.trim();
|
||||
|
||||
let files_changed = last_line
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.context("Failed to get first word of last line")?
|
||||
.parse()?;
|
||||
|
||||
Ok(files_changed)
|
||||
};
|
||||
|
||||
if output.stderr.starts_with("Everything up to date") {
|
||||
SuccessMessage {
|
||||
message: output.stderr.trim().to_owned(),
|
||||
style: SuccessStyle::Toast,
|
||||
}
|
||||
} else if output.stdout.starts_with("Updating") {
|
||||
let files_changed = get_changes(&output).log_err();
|
||||
let message = if let Some(files_changed) = files_changed {
|
||||
format!(
|
||||
"Received {} file change{} from {}",
|
||||
files_changed,
|
||||
if files_changed == 1 { "" } else { "s" },
|
||||
remote_ref.name
|
||||
)
|
||||
} else {
|
||||
format!("Fast forwarded from {}", remote_ref.name)
|
||||
};
|
||||
SuccessMessage {
|
||||
message,
|
||||
style: SuccessStyle::ToastWithLog { output },
|
||||
}
|
||||
} else if output.stdout.starts_with("Merge") {
|
||||
let files_changed = get_changes(&output).log_err();
|
||||
let message = if let Some(files_changed) = files_changed {
|
||||
format!(
|
||||
"Merged {} file change{} from {}",
|
||||
files_changed,
|
||||
if files_changed == 1 { "" } else { "s" },
|
||||
remote_ref.name
|
||||
)
|
||||
} else {
|
||||
format!("Merged from {}", remote_ref.name)
|
||||
};
|
||||
SuccessMessage {
|
||||
message,
|
||||
style: SuccessStyle::ToastWithLog { output },
|
||||
}
|
||||
} else if output.stdout.contains("Successfully rebased") {
|
||||
SuccessMessage {
|
||||
message: format!("Successfully rebased from {}", remote_ref.name),
|
||||
style: SuccessStyle::ToastWithLog { output },
|
||||
}
|
||||
} else {
|
||||
SuccessMessage {
|
||||
message: format!("Successfully pulled from {}", remote_ref.name),
|
||||
style: SuccessStyle::ToastWithLog { output },
|
||||
}
|
||||
}
|
||||
}
|
||||
RemoteAction::Push(branch_name, remote_ref) => {
|
||||
if output.stderr.contains("* [new branch]") {
|
||||
let style = if output.stderr.contains("Create a pull request") {
|
||||
let finder = LinkFinder::new();
|
||||
let first_link = finder
|
||||
.links(&output.stderr)
|
||||
.filter(|link| *link.kind() == LinkKind::Url)
|
||||
.map(|link| link.start()..link.end())
|
||||
.next();
|
||||
if let Some(link) = first_link {
|
||||
let link = output.stderr[link].to_string();
|
||||
SuccessStyle::PushPrLink { link }
|
||||
} else {
|
||||
SuccessStyle::ToastWithLog { output }
|
||||
}
|
||||
} else {
|
||||
SuccessStyle::ToastWithLog { output }
|
||||
};
|
||||
SuccessMessage {
|
||||
message: format!("Published {} to {}", branch_name, remote_ref.name),
|
||||
style,
|
||||
}
|
||||
} else if output.stderr.starts_with("Everything up to date") {
|
||||
SuccessMessage {
|
||||
message: output.stderr.trim().to_owned(),
|
||||
style: SuccessStyle::Toast,
|
||||
}
|
||||
} else {
|
||||
SuccessMessage {
|
||||
message: format!("Pushed {} to {}", branch_name, remote_ref.name),
|
||||
style: SuccessStyle::ToastWithLog { output },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
227
crates/git_ui/src/remote_output_toast.rs
Normal file
227
crates/git_ui/src/remote_output_toast.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
use std::{ops::Range, time::Duration};
|
||||
|
||||
use git::repository::{Remote, RemoteCommandOutput};
|
||||
use gpui::{
|
||||
DismissEvent, EventEmitter, FocusHandle, Focusable, HighlightStyle, InteractiveText,
|
||||
StyledText, Task, UnderlineStyle, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use linkify::{LinkFinder, LinkKind};
|
||||
use ui::{
|
||||
div, h_flex, px, v_flex, vh, Clickable, Color, Context, FluentBuilder, Icon, IconButton,
|
||||
IconName, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement,
|
||||
Render, SharedString, Styled, StyledExt, Window,
|
||||
};
|
||||
use workspace::{
|
||||
notifications::{Notification, NotificationId},
|
||||
Workspace,
|
||||
};
|
||||
|
||||
pub enum RemoteAction {
|
||||
Fetch,
|
||||
Pull,
|
||||
Push(Remote),
|
||||
}
|
||||
|
||||
struct InfoFromRemote {
|
||||
name: SharedString,
|
||||
remote_text: SharedString,
|
||||
links: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
pub struct RemoteOutputToast {
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
_id: NotificationId,
|
||||
message: SharedString,
|
||||
remote_info: Option<InfoFromRemote>,
|
||||
_dismiss_task: Task<()>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Focusable for RemoteOutputToast {
|
||||
fn focus_handle(&self, _cx: &ui::App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Notification for RemoteOutputToast {}
|
||||
|
||||
const REMOTE_OUTPUT_TOAST_SECONDS: u64 = 5;
|
||||
|
||||
impl RemoteOutputToast {
|
||||
pub fn new(
|
||||
action: RemoteAction,
|
||||
output: RemoteCommandOutput,
|
||||
id: NotificationId,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let task = cx.spawn({
|
||||
let workspace = workspace.clone();
|
||||
let id = id.clone();
|
||||
|_, mut cx| async move {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_secs(REMOTE_OUTPUT_TOAST_SECONDS))
|
||||
.await;
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.dismiss_notification(&id, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
|
||||
let mut message: SharedString;
|
||||
let remote;
|
||||
|
||||
match action {
|
||||
RemoteAction::Fetch | RemoteAction::Pull => {
|
||||
if output.is_empty() {
|
||||
message = "Up to date".into();
|
||||
} else {
|
||||
message = output.stderr.into();
|
||||
}
|
||||
remote = None;
|
||||
}
|
||||
|
||||
RemoteAction::Push(remote_ref) => {
|
||||
message = output.stdout.trim().to_string().into();
|
||||
if message.is_empty() {
|
||||
message = output.stderr.trim().to_string().into();
|
||||
if message.is_empty() {
|
||||
message = "Push Successful".into();
|
||||
}
|
||||
remote = None;
|
||||
} else {
|
||||
let remote_message = get_remote_lines(&output.stderr);
|
||||
|
||||
remote = if remote_message.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let finder = LinkFinder::new();
|
||||
let links = finder
|
||||
.links(&remote_message)
|
||||
.filter(|link| *link.kind() == LinkKind::Url)
|
||||
.map(|link| link.start()..link.end())
|
||||
.collect_vec();
|
||||
|
||||
Some(InfoFromRemote {
|
||||
name: remote_ref.name,
|
||||
remote_text: remote_message.into(),
|
||||
links,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
_workspace: workspace,
|
||||
_id: id,
|
||||
message,
|
||||
remote_info: remote,
|
||||
_dismiss_task: task,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for RemoteOutputToast {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
|
||||
div()
|
||||
.occlude()
|
||||
.w_full()
|
||||
.max_h(vh(0.8, window))
|
||||
.elevation_3(cx)
|
||||
.child(
|
||||
v_flex()
|
||||
.p_3()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.items_start()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::GitBranch).color(Color::Default))
|
||||
.child(Label::new("Git")),
|
||||
)
|
||||
.child(h_flex().child(
|
||||
IconButton::new("close", IconName::Close).on_click(
|
||||
cx.listener(|_, _, _, cx| cx.emit(gpui::DismissEvent)),
|
||||
),
|
||||
)),
|
||||
)
|
||||
.child(Label::new(self.message.clone()).size(LabelSize::Default))
|
||||
.when_some(self.remote_info.as_ref(), |this, remote_info| {
|
||||
this.child(
|
||||
div()
|
||||
.border_1()
|
||||
.border_color(Color::Muted.color(cx))
|
||||
.rounded_lg()
|
||||
.text_sm()
|
||||
.mt_1()
|
||||
.p_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::Cloud).color(Color::Default))
|
||||
.child(
|
||||
Label::new(remote_info.name.clone())
|
||||
.size(LabelSize::Default),
|
||||
),
|
||||
)
|
||||
.map(|div| {
|
||||
let styled_text =
|
||||
StyledText::new(remote_info.remote_text.clone())
|
||||
.with_highlights(remote_info.links.iter().map(
|
||||
|link| {
|
||||
(
|
||||
link.clone(),
|
||||
HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: px(1.0),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
},
|
||||
));
|
||||
let this = cx.weak_entity();
|
||||
let text = InteractiveText::new("remote-message", styled_text)
|
||||
.on_click(
|
||||
remote_info.links.clone(),
|
||||
move |ix, _window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(remote_info) = &this.remote_info {
|
||||
cx.open_url(
|
||||
&remote_info.remote_text
|
||||
[remote_info.links[ix].clone()],
|
||||
)
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
);
|
||||
|
||||
div.child(text)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for RemoteOutputToast {}
|
||||
|
||||
fn get_remote_lines(output: &str) -> String {
|
||||
output
|
||||
.lines()
|
||||
.filter_map(|line| line.strip_prefix("remote:"))
|
||||
.map(|line| line.trim())
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
@@ -402,7 +402,7 @@ impl Render for DataTable {
|
||||
.overflow_hidden()
|
||||
.border_1()
|
||||
.border_color(rgb(0xE0E0E0))
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
|
||||
@@ -69,7 +69,7 @@ struct ImageLoadingExample {}
|
||||
|
||||
impl ImageLoadingExample {
|
||||
fn loading_element() -> impl IntoElement {
|
||||
div().size_full().flex_none().p_0p5().rounded_sm().child(
|
||||
div().size_full().flex_none().p_0p5().rounded_xs().child(
|
||||
div().size_full().with_animation(
|
||||
"loading-bg",
|
||||
Animation::new(Duration::from_secs(3))
|
||||
@@ -89,7 +89,7 @@ impl ImageLoadingExample {
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded_sm()
|
||||
.rounded_xs()
|
||||
.text_sm()
|
||||
.text_color(fallback_color)
|
||||
.border_1()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user