Compare commits
22 Commits
v0.178.4
...
windows_ch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7aae2ab90 | ||
|
|
81af2c0bed | ||
|
|
ab199fda47 | ||
|
|
e60e8f3a0a | ||
|
|
edeed7b619 | ||
|
|
9be7934f12 | ||
|
|
009b90291e | ||
|
|
8b17dc66f6 | ||
|
|
de07b712fd | ||
|
|
be8f3b3791 | ||
|
|
3131b0459f | ||
|
|
3ec323ce0d | ||
|
|
c8b782d870 | ||
|
|
7bca15704b | ||
|
|
5268e74315 | ||
|
|
91c209900b | ||
|
|
74c29f1818 | ||
|
|
5858e61327 | ||
|
|
21cf2e38c5 | ||
|
|
a3ca5554fd | ||
|
|
acf9b22466 | ||
|
|
ffcd023f83 |
61
.github/workflows/ci.yml
vendored
61
.github/workflows/ci.yml
vendored
@@ -34,25 +34,21 @@ jobs:
|
||||
- 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
|
||||
fetch-depth: 350
|
||||
# 350 is arbitrary. Full fetch is ~18secs; 350 is ~5s
|
||||
# This will fail if your branch is >350 commits behind main
|
||||
- name: Fetch main branch (or PR target) branch
|
||||
run: git fetch origin ${{ github.event.pull_request.base.ref }} --depth=350
|
||||
- name:
|
||||
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
|
||||
MERGE_BASE=$(git merge-base origin/main HEAD)
|
||||
if [[ $(git diff --name-only $MERGE_BASE ${{ 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
|
||||
if [[ $(git diff --name-only $MERGE_BASE ${{ github.sha }} | grep '^Cargo.lock') ]]; then
|
||||
echo "run_license=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "run_license=false" >> $GITHUB_OUTPUT
|
||||
@@ -366,8 +362,7 @@ jobs:
|
||||
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' }}
|
||||
runs-on: windows-2025-64
|
||||
steps:
|
||||
# more info here:- https://github.com/rust-lang/cargo/issues/13020
|
||||
- name: Enable longer pathnames for git
|
||||
@@ -432,28 +427,22 @@ jobs:
|
||||
- macos_tests
|
||||
- windows_clippy
|
||||
- windows_tests
|
||||
if: always()
|
||||
if: |
|
||||
always() && (
|
||||
needs.style.result == 'success'
|
||||
&& (
|
||||
needs.job_spec.outputs.run_tests == 'false'
|
||||
|| (needs.macos_tests.result == 'success'
|
||||
&& needs.linux_tests.result == 'success'
|
||||
&& needs.windows_tests.result == 'success'
|
||||
&& needs.windows_clippy.result == 'success'
|
||||
&& needs.build_remote_server.result == 'success'
|
||||
&& needs.migration_checks.result == 'success')
|
||||
)
|
||||
)
|
||||
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
|
||||
- name: All tests passed
|
||||
run: echo "All tests passed successfully!"
|
||||
|
||||
bundle-mac:
|
||||
timeout-minutes: 120
|
||||
|
||||
38
.github/workflows/community_release_actions.yml
vendored
38
.github/workflows/community_release_actions.yml
vendored
@@ -13,12 +13,11 @@ jobs:
|
||||
id: get-release-url
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
URL="https://zed.dev/releases/preview/latest"
|
||||
URL="https://zed.dev/releases/preview/latest"
|
||||
else
|
||||
URL="https://zed.dev/releases/stable/latest"
|
||||
URL="https://zed.dev/releases/stable/latest"
|
||||
fi
|
||||
|
||||
echo "URL=$URL" >> $GITHUB_OUTPUT
|
||||
echo "::set-output name=URL::$URL"
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 # v1.4.1
|
||||
id: get-content
|
||||
@@ -34,34 +33,3 @@ jobs:
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
|
||||
send_release_notes_email:
|
||||
if: github.repository_owner == 'zed-industries' && !github.event.release.prerelease
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if release was promoted from preview
|
||||
id: check-promotion-from-preview
|
||||
run: |
|
||||
VERSION="${{ github.event.release.tag_name }}"
|
||||
PREVIEW_TAG="${VERSION}-pre"
|
||||
|
||||
if git rev-parse "$PREVIEW_TAG" > /dev/null 2>&1; then
|
||||
echo "was_promoted_from_preview=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "was_promoted_from_preview=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Send release notes email
|
||||
if: steps.check-promotion-from-preview.outputs.was_promoted_from_preview == 'true'
|
||||
run: |
|
||||
curl -X POST "https://zed.dev/api/send_release_notes_email" \
|
||||
-H "Authorization: Bearer ${{ secrets.RELEASE_NOTES_API_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"version": "${{ github.event.release.tag_name }}",
|
||||
"markdown_body": ${{ toJSON(github.event.release.body) }}
|
||||
}'
|
||||
|
||||
24
Cargo.lock
generated
24
Cargo.lock
generated
@@ -4639,6 +4639,7 @@ dependencies = [
|
||||
"collections",
|
||||
"db",
|
||||
"editor",
|
||||
"extension",
|
||||
"extension_host",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
@@ -17008,7 +17009,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.178.4"
|
||||
version = "0.179.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -17203,13 +17204,6 @@ dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_purescript"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_ruff"
|
||||
version = "0.1.0"
|
||||
@@ -17239,20 +17233,6 @@ dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_uiua"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_zig"
|
||||
version = "0.3.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeno"
|
||||
version = "0.2.3"
|
||||
|
||||
@@ -174,14 +174,11 @@ members = [
|
||||
"extensions/html",
|
||||
"extensions/perplexity",
|
||||
"extensions/proto",
|
||||
"extensions/purescript",
|
||||
"extensions/ruff",
|
||||
"extensions/slash-commands-example",
|
||||
"extensions/snippets",
|
||||
"extensions/test-extension",
|
||||
"extensions/toml",
|
||||
"extensions/uiua",
|
||||
"extensions/zig",
|
||||
|
||||
#
|
||||
# Tooling
|
||||
|
||||
@@ -336,14 +336,14 @@
|
||||
"active_line_width": 1,
|
||||
// Determines how indent guides are colored.
|
||||
// This setting can take the following three values:
|
||||
///
|
||||
//
|
||||
// 1. "disabled"
|
||||
// 2. "fixed"
|
||||
// 3. "indent_aware"
|
||||
"coloring": "fixed",
|
||||
// Determines how indent guide backgrounds are colored.
|
||||
// This setting can take the following two values:
|
||||
///
|
||||
//
|
||||
// 1. "disabled"
|
||||
// 2. "indent_aware"
|
||||
"background_coloring": "disabled"
|
||||
@@ -402,8 +402,8 @@
|
||||
// Time to wait after scrolling the buffer, before requesting the hints,
|
||||
// set to 0 to disable debouncing.
|
||||
"scroll_debounce_ms": 50,
|
||||
/// A set of modifiers which, when pressed, will toggle the visibility of inlay hints.
|
||||
/// If the set if empty or not all the modifiers specified are pressed, inlay hints will not be toggled.
|
||||
// A set of modifiers which, when pressed, will toggle the visibility of inlay hints.
|
||||
// If the set if empty or not all the modifiers specified are pressed, inlay hints will not be toggled.
|
||||
"toggle_on_modifiers_press": {
|
||||
"control": false,
|
||||
"shift": false,
|
||||
@@ -440,7 +440,7 @@
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the project panel.
|
||||
// This setting can take five values:
|
||||
///
|
||||
//
|
||||
// 1. null (default): Inherit editor settings
|
||||
// 2. Show the scrollbar if there's important information or
|
||||
// follow the system's configured behavior (default):
|
||||
@@ -455,7 +455,7 @@
|
||||
},
|
||||
// Which files containing diagnostic errors/warnings to mark in the project panel.
|
||||
// This setting can take the following three values:
|
||||
///
|
||||
//
|
||||
// 1. Do not mark any files:
|
||||
// "off"
|
||||
// 2. Only mark files with errors:
|
||||
@@ -512,7 +512,7 @@
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the project panel.
|
||||
// This setting can take five values:
|
||||
///
|
||||
//
|
||||
// 1. null (default): Inherit editor settings
|
||||
// 2. Show the scrollbar if there's important information or
|
||||
// follow the system's configured behavior (default):
|
||||
@@ -686,7 +686,7 @@
|
||||
// Which files containing diagnostic errors/warnings to mark in the tabs.
|
||||
// Diagnostics are only shown when file icons are also active.
|
||||
// This setting only works when can take the following three values:
|
||||
///
|
||||
//
|
||||
// 1. Do not mark any files:
|
||||
// "off"
|
||||
// 2. Only mark files with errors:
|
||||
@@ -850,15 +850,7 @@
|
||||
//
|
||||
// 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 filled and staged hunks hollow:
|
||||
// "hunk_style": "staged_hollow"
|
||||
// 2. Show unstaged hunks hollow and staged hunks filled:
|
||||
// "hunk_style": "unstaged_hollow"
|
||||
"hunk_style": "staged_hollow"
|
||||
}
|
||||
},
|
||||
// Configuration for how direnv configuration should be loaded. May take 2 values:
|
||||
// 1. Load direnv configuration using `direnv export json` directly.
|
||||
@@ -1022,7 +1014,7 @@
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the terminal.
|
||||
// This setting can take five values:
|
||||
///
|
||||
//
|
||||
// 1. null (default): Inherit editor settings
|
||||
// 2. Show the scrollbar if there's important information or
|
||||
// follow the system's configured behavior (default):
|
||||
@@ -1093,6 +1085,31 @@
|
||||
"auto_install_extensions": {
|
||||
"html": true
|
||||
},
|
||||
// Controls how completions are processed for this language.
|
||||
"completions": {
|
||||
// Controls how words are completed.
|
||||
// For large documents, not all words may be fetched for completion.
|
||||
//
|
||||
// May take 3 values:
|
||||
// 1. "enabled"
|
||||
// Always fetch document's words for completions.
|
||||
// 2. "fallback"
|
||||
// Only if LSP response errors/times out/is empty, use document's words to show completions.
|
||||
// 3. "disabled"
|
||||
// Never fetch or complete document's words for completions.
|
||||
//
|
||||
// Default: fallback
|
||||
"words": "fallback",
|
||||
// Whether to fetch LSP completions or not.
|
||||
//
|
||||
// Default: true
|
||||
"lsp": true,
|
||||
// When fetching LSP completions, determines how long to wait for a response of a particular server.
|
||||
// When set to 0, waits indefinitely.
|
||||
//
|
||||
// Default: 500
|
||||
"lsp_fetch_timeout_ms": 500
|
||||
},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"Astro": {
|
||||
|
||||
@@ -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",
|
||||
@@ -391,7 +399,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",
|
||||
@@ -482,9 +498,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",
|
||||
@@ -776,7 +792,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",
|
||||
@@ -867,9 +891,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",
|
||||
@@ -1161,7 +1185,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",
|
||||
@@ -1252,9 +1284,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",
|
||||
@@ -1546,7 +1578,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",
|
||||
@@ -1637,9 +1677,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",
|
||||
@@ -1931,7 +1971,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",
|
||||
@@ -2022,9 +2070,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",
|
||||
|
||||
@@ -46,56 +46,104 @@ impl ToolWorkingSet {
|
||||
}
|
||||
|
||||
pub fn tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||
let mut tools = ToolRegistry::global(cx).tools();
|
||||
tools.extend(
|
||||
self.state
|
||||
.lock()
|
||||
.context_server_tools_by_id
|
||||
.values()
|
||||
.cloned(),
|
||||
);
|
||||
self.state.lock().tools(cx)
|
||||
}
|
||||
|
||||
tools
|
||||
pub fn tools_by_source(&self, cx: &App) -> IndexMap<ToolSource, Vec<Arc<dyn Tool>>> {
|
||||
self.state.lock().tools_by_source(cx)
|
||||
}
|
||||
|
||||
pub fn are_all_tools_enabled(&self) -> bool {
|
||||
let state = self.state.lock();
|
||||
|
||||
state.disabled_tools_by_source.is_empty() && !state.is_scripting_tool_disabled
|
||||
}
|
||||
|
||||
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||
self.state.lock().enabled_tools(cx)
|
||||
}
|
||||
|
||||
pub fn enable_all_tools(&self) {
|
||||
let mut state = self.state.lock();
|
||||
|
||||
state.disabled_tools_by_source.clear();
|
||||
state.is_scripting_tool_disabled = false;
|
||||
state.enable_scripting_tool();
|
||||
}
|
||||
|
||||
pub fn disable_all_tools(&self, cx: &App) {
|
||||
let tools = self.tools_by_source(cx);
|
||||
|
||||
for (source, tools) in tools {
|
||||
let tool_names = tools
|
||||
.into_iter()
|
||||
.map(|tool| tool.name().into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.disable(source, &tool_names);
|
||||
}
|
||||
|
||||
self.disable_scripting_tool();
|
||||
let mut state = self.state.lock();
|
||||
state.disable_all_tools(cx);
|
||||
}
|
||||
|
||||
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||
let all_tools = self.tools(cx);
|
||||
|
||||
all_tools
|
||||
.into_iter()
|
||||
.filter(|tool| self.is_enabled(&tool.source(), &tool.name().into()))
|
||||
.collect()
|
||||
pub fn insert(&self, tool: Arc<dyn Tool>) -> ToolId {
|
||||
let mut state = self.state.lock();
|
||||
let tool_id = state.next_tool_id;
|
||||
state.next_tool_id.0 += 1;
|
||||
state
|
||||
.context_server_tools_by_id
|
||||
.insert(tool_id, tool.clone());
|
||||
state.tools_changed();
|
||||
tool_id
|
||||
}
|
||||
|
||||
pub fn tools_by_source(&self, cx: &App) -> IndexMap<ToolSource, Vec<Arc<dyn Tool>>> {
|
||||
pub fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
self.state.lock().is_enabled(source, name)
|
||||
}
|
||||
|
||||
pub fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
self.state.lock().is_disabled(source, name)
|
||||
}
|
||||
|
||||
pub fn enable(&self, source: ToolSource, tools_to_enable: &[Arc<str>]) {
|
||||
let mut state = self.state.lock();
|
||||
state.enable(source, tools_to_enable);
|
||||
}
|
||||
|
||||
pub fn disable(&self, source: ToolSource, tools_to_disable: &[Arc<str>]) {
|
||||
let mut state = self.state.lock();
|
||||
state.disable(source, tools_to_disable);
|
||||
}
|
||||
|
||||
pub fn remove(&self, tool_ids_to_remove: &[ToolId]) {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.context_server_tools_by_id
|
||||
.retain(|id, _| !tool_ids_to_remove.contains(id));
|
||||
state.tools_changed();
|
||||
}
|
||||
|
||||
pub fn is_scripting_tool_enabled(&self) -> bool {
|
||||
let state = self.state.lock();
|
||||
!state.is_scripting_tool_disabled
|
||||
}
|
||||
|
||||
pub fn enable_scripting_tool(&self) {
|
||||
let mut state = self.state.lock();
|
||||
state.enable_scripting_tool();
|
||||
}
|
||||
|
||||
pub fn disable_scripting_tool(&self) {
|
||||
let mut state = self.state.lock();
|
||||
state.disable_scripting_tool();
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkingSetState {
|
||||
fn tools_changed(&mut self) {
|
||||
self.context_server_tools_by_name.clear();
|
||||
self.context_server_tools_by_name.extend(
|
||||
self.context_server_tools_by_id
|
||||
.values()
|
||||
.map(|tool| (tool.name(), tool.clone())),
|
||||
);
|
||||
}
|
||||
|
||||
fn tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||
let mut tools = ToolRegistry::global(cx).tools();
|
||||
tools.extend(self.context_server_tools_by_id.values().cloned());
|
||||
|
||||
tools
|
||||
}
|
||||
|
||||
fn tools_by_source(&self, cx: &App) -> IndexMap<ToolSource, Vec<Arc<dyn Tool>>> {
|
||||
let mut tools_by_source = IndexMap::default();
|
||||
|
||||
for tool in self.tools(cx) {
|
||||
@@ -114,78 +162,59 @@ impl ToolWorkingSet {
|
||||
tools_by_source
|
||||
}
|
||||
|
||||
pub fn insert(&self, tool: Arc<dyn Tool>) -> ToolId {
|
||||
let mut state = self.state.lock();
|
||||
let tool_id = state.next_tool_id;
|
||||
state.next_tool_id.0 += 1;
|
||||
state
|
||||
.context_server_tools_by_id
|
||||
.insert(tool_id, tool.clone());
|
||||
state.tools_changed();
|
||||
tool_id
|
||||
fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||
let all_tools = self.tools(cx);
|
||||
|
||||
all_tools
|
||||
.into_iter()
|
||||
.filter(|tool| self.is_enabled(&tool.source(), &tool.name().into()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
!self.is_disabled(source, name)
|
||||
}
|
||||
|
||||
pub fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
let state = self.state.lock();
|
||||
state
|
||||
.disabled_tools_by_source
|
||||
fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
self.disabled_tools_by_source
|
||||
.get(source)
|
||||
.map_or(false, |disabled_tools| disabled_tools.contains(name))
|
||||
}
|
||||
|
||||
pub fn enable(&self, source: ToolSource, tools_to_enable: &[Arc<str>]) {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.disabled_tools_by_source
|
||||
fn enable(&mut self, source: ToolSource, tools_to_enable: &[Arc<str>]) {
|
||||
self.disabled_tools_by_source
|
||||
.entry(source)
|
||||
.or_default()
|
||||
.retain(|name| !tools_to_enable.contains(name));
|
||||
}
|
||||
|
||||
pub fn disable(&self, source: ToolSource, tools_to_disable: &[Arc<str>]) {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.disabled_tools_by_source
|
||||
fn disable(&mut self, source: ToolSource, tools_to_disable: &[Arc<str>]) {
|
||||
self.disabled_tools_by_source
|
||||
.entry(source)
|
||||
.or_default()
|
||||
.extend(tools_to_disable.into_iter().cloned());
|
||||
}
|
||||
|
||||
pub fn remove(&self, tool_ids_to_remove: &[ToolId]) {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.context_server_tools_by_id
|
||||
.retain(|id, _| !tool_ids_to_remove.contains(id));
|
||||
state.tools_changed();
|
||||
fn disable_all_tools(&mut self, cx: &App) {
|
||||
let tools = self.tools_by_source(cx);
|
||||
|
||||
for (source, tools) in tools {
|
||||
let tool_names = tools
|
||||
.into_iter()
|
||||
.map(|tool| tool.name().into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.disable(source, &tool_names);
|
||||
}
|
||||
|
||||
self.disable_scripting_tool();
|
||||
}
|
||||
|
||||
pub fn is_scripting_tool_enabled(&self) -> bool {
|
||||
let state = self.state.lock();
|
||||
!state.is_scripting_tool_disabled
|
||||
fn enable_scripting_tool(&mut self) {
|
||||
self.is_scripting_tool_disabled = false;
|
||||
}
|
||||
|
||||
pub fn enable_scripting_tool(&self) {
|
||||
let mut state = self.state.lock();
|
||||
state.is_scripting_tool_disabled = false;
|
||||
}
|
||||
|
||||
pub fn disable_scripting_tool(&self) {
|
||||
let mut state = self.state.lock();
|
||||
state.is_scripting_tool_disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkingSetState {
|
||||
fn tools_changed(&mut self) {
|
||||
self.context_server_tools_by_name.clear();
|
||||
self.context_server_tools_by_name.extend(
|
||||
self.context_server_tools_by_id
|
||||
.values()
|
||||
.map(|tool| (tool.name(), tool.clone())),
|
||||
);
|
||||
fn disable_scripting_tool(&mut self) {
|
||||
self.is_scripting_tool_disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
mod bash_tool;
|
||||
mod delete_path_tool;
|
||||
mod edit_files_tool;
|
||||
mod list_directory_tool;
|
||||
mod now_tool;
|
||||
@@ -7,6 +9,8 @@ mod regex_search;
|
||||
use assistant_tool::ToolRegistry;
|
||||
use gpui::App;
|
||||
|
||||
use crate::bash_tool::BashTool;
|
||||
use crate::delete_path_tool::DeletePathTool;
|
||||
use crate::edit_files_tool::EditFilesTool;
|
||||
use crate::list_directory_tool::ListDirectoryTool;
|
||||
use crate::now_tool::NowTool;
|
||||
@@ -22,4 +26,6 @@ pub fn init(cx: &mut App) {
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(EditFilesTool);
|
||||
registry.register_tool(RegexSearchTool);
|
||||
registry.register_tool(DeletePathTool);
|
||||
registry.register_tool(BashTool);
|
||||
}
|
||||
|
||||
70
crates/assistant_tools/src/bash_tool.rs
Normal file
70
crates/assistant_tools/src/bash_tool.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct BashToolInput {
|
||||
/// The bash command to execute as a one-liner.
|
||||
command: String,
|
||||
}
|
||||
|
||||
pub struct BashTool;
|
||||
|
||||
impl Tool for BashTool {
|
||||
fn name(&self) -> String {
|
||||
"bash".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./bash_tool/description.md").to_string()
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(BashToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input: BashToolInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
cx.spawn(|_| async move {
|
||||
// Add 2>&1 to merge stderr into stdout for proper interleaving
|
||||
let command = format!("{} 2>&1", input.command);
|
||||
|
||||
// Spawn a blocking task to execute the command
|
||||
let output = futures::executor::block_on(async {
|
||||
std::process::Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg(&command)
|
||||
.output()
|
||||
.map_err(|err| anyhow!("Failed to execute bash command: {}", err))
|
||||
})?;
|
||||
|
||||
let output_string = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
|
||||
if output.status.success() {
|
||||
Ok(output_string)
|
||||
} else {
|
||||
Ok(format!(
|
||||
"Command failed with exit code {}\n{}",
|
||||
output.status.code().unwrap_or(-1),
|
||||
&output_string
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
1
crates/assistant_tools/src/bash_tool/description.md
Normal file
1
crates/assistant_tools/src/bash_tool/description.md
Normal file
@@ -0,0 +1 @@
|
||||
Executes a bash one-liner and returns the combined output. This tool spawns a bash process, combines stdout and stderr into one interleaved stream as they are produced (preserving the order of writes), and captures that stream into a string which is returned. Use this tool when you need to run shell commands to get information about the system or process files.
|
||||
165
crates/assistant_tools/src/delete_path_tool.rs
Normal file
165
crates/assistant_tools/src/delete_path_tool.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
use util::paths::PathMatcher;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DeletePathToolInput {
|
||||
/// The glob to match files in the project to delete.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following files:
|
||||
///
|
||||
/// - directory1/a/something.txt
|
||||
/// - directory2/a/things.txt
|
||||
/// - directory3/a/other.txt
|
||||
///
|
||||
/// You can delete the first two files by providing a glob of "*thing*.txt"
|
||||
/// </example>
|
||||
pub glob: String,
|
||||
}
|
||||
|
||||
pub struct DeletePathTool;
|
||||
|
||||
impl Tool for DeletePathTool {
|
||||
fn name(&self) -> String {
|
||||
"delete-path".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./delete_path_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(DeletePathToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let glob = match serde_json::from_value::<DeletePathToolInput>(input) {
|
||||
Ok(input) => input.glob,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
let path_matcher = match PathMatcher::new(&[glob.clone()]) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {}", err))),
|
||||
};
|
||||
|
||||
struct Match {
|
||||
display_path: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
let mut matches = Vec::new();
|
||||
let mut deleted_paths = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for worktree_handle in project.read(cx).worktrees(cx) {
|
||||
let worktree = worktree_handle.read(cx);
|
||||
let worktree_root = worktree.abs_path().to_path_buf();
|
||||
|
||||
// Don't consider ignored entries.
|
||||
for entry in worktree.entries(false, 0) {
|
||||
if path_matcher.is_match(&entry.path) {
|
||||
matches.push(Match {
|
||||
path: worktree_root.join(&entry.path),
|
||||
display_path: entry.path.display().to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches.is_empty() {
|
||||
return Task::ready(Ok(format!("No paths in the project matched {glob:?}")));
|
||||
}
|
||||
|
||||
let paths_matched = matches.len();
|
||||
|
||||
// Delete the files
|
||||
for Match { path, display_path } in matches {
|
||||
match fs::remove_file(&path) {
|
||||
Ok(()) => {
|
||||
deleted_paths.push(display_path);
|
||||
}
|
||||
Err(file_err) => {
|
||||
// Try to remove directory if it's not a file. Retrying as a directory
|
||||
// on error saves a syscall compared to checking whether it's
|
||||
// a directory up front for every single file.
|
||||
if let Err(dir_err) = fs::remove_dir_all(&path) {
|
||||
let error = if path.is_dir() {
|
||||
format!("Failed to delete directory {}: {dir_err}", display_path)
|
||||
} else {
|
||||
format!("Failed to delete file {}: {file_err}", display_path)
|
||||
};
|
||||
|
||||
errors.push(error);
|
||||
} else {
|
||||
deleted_paths.push(display_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
// 0 deleted paths should never happen if there were no errors;
|
||||
// we already returned if matches was empty.
|
||||
let answer = if deleted_paths.len() == 1 {
|
||||
format!(
|
||||
"Deleted {}",
|
||||
deleted_paths.first().unwrap_or(&String::new())
|
||||
)
|
||||
} else {
|
||||
// Sort to group entries in the same directory together
|
||||
deleted_paths.sort();
|
||||
|
||||
let mut buf = format!("Deleted these {} paths:\n", deleted_paths.len());
|
||||
|
||||
for path in deleted_paths.iter() {
|
||||
buf.push('\n');
|
||||
buf.push_str(path);
|
||||
}
|
||||
|
||||
buf
|
||||
};
|
||||
|
||||
Task::ready(Ok(answer))
|
||||
} else {
|
||||
if deleted_paths.is_empty() {
|
||||
Task::ready(Err(anyhow!(
|
||||
"{glob:?} matched {} deleted because of {}:\n{}",
|
||||
if paths_matched == 1 {
|
||||
"1 path, but it was not".to_string()
|
||||
} else {
|
||||
format!("{} paths, but none were", paths_matched)
|
||||
},
|
||||
if errors.len() == 1 {
|
||||
"this error".to_string()
|
||||
} else {
|
||||
format!("{} errors", errors.len())
|
||||
},
|
||||
errors.join("\n")
|
||||
)))
|
||||
} else {
|
||||
// Sort to group entries in the same directory together
|
||||
deleted_paths.sort();
|
||||
Task::ready(Ok(format!(
|
||||
"Deleted {} paths matching glob {glob:?}:\n{}\n\nErrors:\n{}",
|
||||
deleted_paths.len(),
|
||||
deleted_paths.join("\n"),
|
||||
errors.join("\n")
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Deletes all files and directories in the project which match the given glob, and returns a list of the paths that were deleted.
|
||||
@@ -12,6 +12,7 @@ use language_model::{
|
||||
use project::{Project, ProjectPath};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -145,17 +146,29 @@ impl Tool for EditFilesTool {
|
||||
}
|
||||
}
|
||||
|
||||
let mut answer = match changed_buffers.len() {
|
||||
0 => "No files were edited.".to_string(),
|
||||
1 => "Successfully edited ".to_string(),
|
||||
_ => "Successfully edited these files:\n\n".to_string(),
|
||||
};
|
||||
|
||||
// Save each buffer once at the end
|
||||
for buffer in changed_buffers {
|
||||
project
|
||||
.update(&mut cx, |project, cx| project.save_buffer(buffer, cx))?
|
||||
.update(&mut cx, |project, cx| {
|
||||
if let Some(file) = buffer.read(&cx).file() {
|
||||
let _ = write!(&mut answer, "{}\n\n", &file.path().display());
|
||||
}
|
||||
|
||||
project.save_buffer(buffer, cx)
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
|
||||
let errors = parser.errors();
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok("Successfully applied all edits".into())
|
||||
Ok(answer.trim_end().to_string())
|
||||
} else {
|
||||
let error_message = errors
|
||||
.iter()
|
||||
|
||||
@@ -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(
|
||||
@@ -986,9 +916,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;
|
||||
@@ -1221,21 +1149,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1243,14 +1171,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| {
|
||||
@@ -1270,14 +1197,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)]
|
||||
@@ -1336,7 +1263,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,
|
||||
@@ -1674,10 +1601,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
|
||||
@@ -1956,10 +1880,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
|
||||
}
|
||||
|
||||
@@ -2038,7 +2038,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,10 @@ mod tests {
|
||||
use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
|
||||
language_settings::{
|
||||
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
|
||||
WordsCompletionMode,
|
||||
},
|
||||
Point,
|
||||
};
|
||||
use project::Project;
|
||||
@@ -286,7 +289,13 @@ mod tests {
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
// flaky
|
||||
init_test(cx, |_| {});
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Disabled,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
});
|
||||
});
|
||||
|
||||
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
@@ -511,7 +520,13 @@ mod tests {
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
// flaky
|
||||
init_test(cx, |_| {});
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Disabled,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
});
|
||||
});
|
||||
|
||||
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
|
||||
@@ -412,6 +412,7 @@ gpui::actions!(
|
||||
Tab,
|
||||
Backtab,
|
||||
ToggleAutoSignatureHelp,
|
||||
ToggleGitBlame,
|
||||
ToggleGitBlameInline,
|
||||
ToggleIndentGuides,
|
||||
ToggleInlayHints,
|
||||
|
||||
@@ -101,6 +101,7 @@ use itertools::Itertools;
|
||||
use language::{
|
||||
language_settings::{
|
||||
self, all_language_settings, language_settings, InlayHintSettings, RewrapBehavior,
|
||||
WordsCompletionMode,
|
||||
},
|
||||
point_from_lsp, text_diff_with_options, AutoindentMode, BracketMatch, BracketPair, Buffer,
|
||||
Capability, CharKind, CodeLabel, CursorShape, Diagnostic, DiffOptions, EditPredictionsMode,
|
||||
@@ -4021,9 +4022,8 @@ impl Editor {
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let show_completion_documentation = buffer
|
||||
.read(cx)
|
||||
.snapshot()
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let show_completion_documentation = buffer_snapshot
|
||||
.settings_at(buffer_position, cx)
|
||||
.show_completion_documentation;
|
||||
|
||||
@@ -4047,6 +4047,51 @@ impl Editor {
|
||||
};
|
||||
let completions =
|
||||
provider.completions(&buffer, buffer_position, completion_context, window, cx);
|
||||
let (old_range, word_kind) = buffer_snapshot.surrounding_word(buffer_position);
|
||||
let (old_range, word_to_exclude) = if word_kind == Some(CharKind::Word) {
|
||||
let word_to_exclude = buffer_snapshot
|
||||
.text_for_range(old_range.clone())
|
||||
.collect::<String>();
|
||||
(
|
||||
buffer_snapshot.anchor_before(old_range.start)
|
||||
..buffer_snapshot.anchor_after(old_range.end),
|
||||
Some(word_to_exclude),
|
||||
)
|
||||
} else {
|
||||
(buffer_position..buffer_position, None)
|
||||
};
|
||||
|
||||
let completion_settings = language_settings(
|
||||
buffer_snapshot
|
||||
.language_at(buffer_position)
|
||||
.map(|language| language.name()),
|
||||
buffer_snapshot.file(),
|
||||
cx,
|
||||
)
|
||||
.completions;
|
||||
|
||||
// The document can be large, so stay in reasonable bounds when searching for words,
|
||||
// otherwise completion pop-up might be slow to appear.
|
||||
const WORD_LOOKUP_ROWS: u32 = 5_000;
|
||||
let buffer_row = text::ToPoint::to_point(&buffer_position, &buffer_snapshot).row;
|
||||
let min_word_search = buffer_snapshot.clip_point(
|
||||
Point::new(buffer_row.saturating_sub(WORD_LOOKUP_ROWS), 0),
|
||||
Bias::Left,
|
||||
);
|
||||
let max_word_search = buffer_snapshot.clip_point(
|
||||
Point::new(buffer_row + WORD_LOOKUP_ROWS, 0).min(buffer_snapshot.max_point()),
|
||||
Bias::Right,
|
||||
);
|
||||
let word_search_range = buffer_snapshot.point_to_offset(min_word_search)
|
||||
..buffer_snapshot.point_to_offset(max_word_search);
|
||||
let words = match completion_settings.words {
|
||||
WordsCompletionMode::Disabled => Task::ready(HashMap::default()),
|
||||
WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => {
|
||||
cx.background_spawn(async move {
|
||||
buffer_snapshot.words_in_range(None, word_search_range)
|
||||
})
|
||||
}
|
||||
};
|
||||
let sort_completions = provider.sort_completions();
|
||||
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
@@ -4055,8 +4100,55 @@ impl Editor {
|
||||
editor.update(&mut cx, |this, _| {
|
||||
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
|
||||
})?;
|
||||
let completions = completions.await.log_err();
|
||||
let menu = if let Some(completions) = completions {
|
||||
let mut completions = completions.await.log_err().unwrap_or_default();
|
||||
|
||||
match completion_settings.words {
|
||||
WordsCompletionMode::Enabled => {
|
||||
completions.extend(
|
||||
words
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|(word, _)| word_to_exclude.as_ref() != Some(word))
|
||||
.map(|(word, word_range)| Completion {
|
||||
old_range: old_range.clone(),
|
||||
new_text: word.clone(),
|
||||
label: CodeLabel::plain(word, None),
|
||||
documentation: None,
|
||||
source: CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved: false,
|
||||
},
|
||||
confirm: None,
|
||||
}),
|
||||
);
|
||||
}
|
||||
WordsCompletionMode::Fallback => {
|
||||
if completions.is_empty() {
|
||||
completions.extend(
|
||||
words
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|(word, _)| word_to_exclude.as_ref() != Some(word))
|
||||
.map(|(word, word_range)| Completion {
|
||||
old_range: old_range.clone(),
|
||||
new_text: word.clone(),
|
||||
label: CodeLabel::plain(word, None),
|
||||
documentation: None,
|
||||
source: CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved: false,
|
||||
},
|
||||
confirm: None,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
WordsCompletionMode::Disabled => {}
|
||||
}
|
||||
|
||||
let menu = if completions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut menu = CompletionsMenu::new(
|
||||
id,
|
||||
sort_completions,
|
||||
@@ -4070,8 +4162,6 @@ impl Editor {
|
||||
.await;
|
||||
|
||||
menu.visible().then_some(menu)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
@@ -14515,7 +14605,7 @@ impl Editor {
|
||||
|
||||
pub fn toggle_git_blame(
|
||||
&mut self,
|
||||
_: &::git::Blame,
|
||||
_: &ToggleGitBlame,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
|
||||
@@ -16,7 +16,8 @@ use gpui::{
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{
|
||||
AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings,
|
||||
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
|
||||
LanguageSettingsContent, PrettierSettings,
|
||||
},
|
||||
BracketPairConfig,
|
||||
Capability::ReadWrite,
|
||||
@@ -30,7 +31,7 @@ use pretty_assertions::{assert_eq, assert_ne};
|
||||
use project::project_settings::{LspSettings, ProjectSettings};
|
||||
use project::FakeFs;
|
||||
use serde_json::{self, json};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
|
||||
use std::{
|
||||
iter,
|
||||
sync::atomic::{self, AtomicUsize},
|
||||
@@ -9194,6 +9195,101 @@ async fn test_completion(cx: &mut TestAppContext) {
|
||||
apply_additional_edits.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_words_completion(cx: &mut TestAppContext) {
|
||||
let lsp_fetch_timeout_ms = 10;
|
||||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Fallback,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 10,
|
||||
});
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..lsp::CompletionOptions::default()
|
||||
}),
|
||||
signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let throttle_completions = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let lsp_throttle_completions = throttle_completions.clone();
|
||||
let _completion_requests_handler =
|
||||
cx.lsp
|
||||
.server
|
||||
.on_request::<lsp::request::Completion, _, _>(move |_, cx| {
|
||||
let lsp_throttle_completions = lsp_throttle_completions.clone();
|
||||
async move {
|
||||
if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
|
||||
.await;
|
||||
}
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "first".into(),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "last".into(),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
}
|
||||
});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
cx.executor().run_until_parked();
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(
|
||||
completion_menu_entries(&menu),
|
||||
&["first", "last"],
|
||||
"When LSP server is fast to reply, no fallback word completions are used"
|
||||
);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
editor.cancel(&Cancel, window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
cx.condition(|editor, _| !editor.context_menu_visible())
|
||||
.await;
|
||||
|
||||
throttle_completions.store(true, atomic::Ordering::Release);
|
||||
cx.simulate_keystroke(".");
|
||||
cx.executor()
|
||||
.advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
|
||||
cx.executor().run_until_parked();
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.update_editor(|editor, _, _| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
|
||||
"When LSP server is slow, document words can be shown instead, if configured accordingly");
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multiline_completion(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -55,7 +55,7 @@ use multi_buffer::{
|
||||
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
|
||||
RowInfo,
|
||||
};
|
||||
use project::project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings};
|
||||
use project::project_settings::{self, GitGutterSetting, ProjectSettings};
|
||||
use settings::Settings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::{
|
||||
@@ -4415,7 +4415,7 @@ impl EditorElement {
|
||||
}),
|
||||
};
|
||||
|
||||
if let Some((hunk_bounds, background_color, corner_radii, status)) = hunk_to_paint {
|
||||
if let Some((hunk_bounds, background_color, corner_radii, _)) = hunk_to_paint {
|
||||
// Flatten the background color with the editor color to prevent
|
||||
// elements below transparent hunks from showing through
|
||||
let flattened_background_color = cx
|
||||
@@ -4424,29 +4424,13 @@ impl EditorElement {
|
||||
.editor_background
|
||||
.blend(background_color);
|
||||
|
||||
if !Self::diff_hunk_hollow(status, cx) {
|
||||
window.paint_quad(quad(
|
||||
hunk_bounds,
|
||||
corner_radii,
|
||||
flattened_background_color,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
));
|
||||
} else {
|
||||
let flattened_unstaged_background_color = cx
|
||||
.theme()
|
||||
.colors()
|
||||
.editor_background
|
||||
.blend(background_color.opacity(0.3));
|
||||
|
||||
window.paint_quad(quad(
|
||||
hunk_bounds,
|
||||
corner_radii,
|
||||
flattened_unstaged_background_color,
|
||||
Edges::all(Pixels(1.0)),
|
||||
flattened_background_color,
|
||||
));
|
||||
}
|
||||
window.paint_quad(quad(
|
||||
hunk_bounds,
|
||||
corner_radii,
|
||||
flattened_background_color,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -5651,18 +5635,6 @@ impl EditorElement {
|
||||
&[run],
|
||||
)
|
||||
}
|
||||
|
||||
fn diff_hunk_hollow(status: DiffHunkStatus, cx: &mut App) -> bool {
|
||||
let unstaged = status.has_secondary_hunk();
|
||||
let unstaged_hollow = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.hunk_style
|
||||
.map_or(false, |style| {
|
||||
matches!(style, GitHunkStyleSetting::UnstagedHollow)
|
||||
});
|
||||
|
||||
unstaged == unstaged_hollow
|
||||
}
|
||||
}
|
||||
|
||||
fn header_jump_data(
|
||||
@@ -6814,9 +6786,10 @@ impl Element for EditorElement {
|
||||
}
|
||||
};
|
||||
|
||||
let unstaged = diff_status.has_secondary_hunk();
|
||||
let hunk_opacity = if is_light { 0.16 } else { 0.12 };
|
||||
|
||||
let hollow_highlight = LineHighlight {
|
||||
let staged_highlight = LineHighlight {
|
||||
background: (background_color.opacity(if is_light {
|
||||
0.08
|
||||
} else {
|
||||
@@ -6830,13 +6803,13 @@ impl Element for EditorElement {
|
||||
}),
|
||||
};
|
||||
|
||||
let filled_highlight =
|
||||
let unstaged_highlight =
|
||||
solid_background(background_color.opacity(hunk_opacity)).into();
|
||||
|
||||
let background = if Self::diff_hunk_hollow(diff_status, cx) {
|
||||
hollow_highlight
|
||||
let background = if unstaged {
|
||||
unstaged_highlight
|
||||
} else {
|
||||
filled_highlight
|
||||
staged_highlight
|
||||
};
|
||||
|
||||
highlighted_rows
|
||||
|
||||
@@ -488,7 +488,7 @@ async fn parse_commit_messages(
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
continue;
|
||||
};
|
||||
|
||||
let remote = parsed_remote_url
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod extension_builder;
|
||||
mod extension_events;
|
||||
mod extension_host_proxy;
|
||||
mod extension_manifest;
|
||||
mod types;
|
||||
@@ -14,12 +15,14 @@ use gpui::{App, Task};
|
||||
use language::LanguageName;
|
||||
use semantic_version::SemanticVersion;
|
||||
|
||||
pub use crate::extension_events::*;
|
||||
pub use crate::extension_host_proxy::*;
|
||||
pub use crate::extension_manifest::*;
|
||||
pub use crate::types::*;
|
||||
|
||||
/// Initializes the `extension` crate.
|
||||
pub fn init(cx: &mut App) {
|
||||
extension_events::init(cx);
|
||||
ExtensionHostProxy::default_global(cx);
|
||||
}
|
||||
|
||||
|
||||
35
crates/extension/src/extension_events.rs
Normal file
35
crates/extension/src/extension_events.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, ReadGlobal as _};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let extension_events = cx.new(ExtensionEvents::new);
|
||||
cx.set_global(GlobalExtensionEvents(extension_events));
|
||||
}
|
||||
|
||||
struct GlobalExtensionEvents(Entity<ExtensionEvents>);
|
||||
|
||||
impl Global for GlobalExtensionEvents {}
|
||||
|
||||
/// An event bus for broadcasting extension-related events throughout the app.
|
||||
pub struct ExtensionEvents;
|
||||
|
||||
impl ExtensionEvents {
|
||||
/// Returns the global [`ExtensionEvents`].
|
||||
pub fn global(cx: &App) -> Entity<Self> {
|
||||
GlobalExtensionEvents::global(cx).0.clone()
|
||||
}
|
||||
|
||||
fn new(_cx: &mut Context<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn emit(&mut self, event: Event, cx: &mut Context<Self>) {
|
||||
cx.emit(event)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Event {
|
||||
ExtensionsUpdated,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ExtensionEvents {}
|
||||
@@ -14,7 +14,7 @@ use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
|
||||
pub use extension::ExtensionManifest;
|
||||
use extension::{
|
||||
ExtensionContextServerProxy, ExtensionGrammarProxy, ExtensionHostProxy,
|
||||
ExtensionContextServerProxy, ExtensionEvents, ExtensionGrammarProxy, ExtensionHostProxy,
|
||||
ExtensionIndexedDocsProviderProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy,
|
||||
ExtensionSlashCommandProxy, ExtensionSnippetProxy, ExtensionThemeProxy,
|
||||
};
|
||||
@@ -127,7 +127,6 @@ pub enum ExtensionOperation {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Event {
|
||||
ExtensionsUpdated,
|
||||
StartedReloading,
|
||||
ExtensionInstalled(Arc<str>),
|
||||
ExtensionFailedToLoad(Arc<str>),
|
||||
@@ -1214,7 +1213,9 @@ impl ExtensionStore {
|
||||
|
||||
self.extension_index = new_index;
|
||||
cx.notify();
|
||||
cx.emit(Event::ExtensionsUpdated);
|
||||
ExtensionEvents::global(cx).update(cx, |this, cx| {
|
||||
this.emit(extension::Event::ExtensionsUpdated, cx)
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.background_spawn({
|
||||
|
||||
@@ -780,6 +780,7 @@ fn init_test(cx: &mut TestAppContext) {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
extension::init(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
Project::init_settings(cx);
|
||||
ExtensionSettings::register(cx);
|
||||
|
||||
@@ -17,6 +17,7 @@ client.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
extension.workspace = true
|
||||
extension_host.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::{ops::Range, sync::Arc};
|
||||
use client::{ExtensionMetadata, ExtensionProvides};
|
||||
use collections::{BTreeMap, BTreeSet};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use extension::ExtensionEvents;
|
||||
use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
@@ -212,7 +213,7 @@ pub struct ExtensionsPage {
|
||||
query_editor: Entity<Editor>,
|
||||
query_contains_error: bool,
|
||||
provides_filter: Option<ExtensionProvides>,
|
||||
_subscriptions: [gpui::Subscription; 2],
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
extension_fetch_task: Option<Task<()>>,
|
||||
upsells: BTreeSet<Feature>,
|
||||
}
|
||||
@@ -226,15 +227,12 @@ impl ExtensionsPage {
|
||||
cx.new(|cx| {
|
||||
let store = ExtensionStore::global(cx);
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let subscriptions = [
|
||||
let subscriptions = vec![
|
||||
cx.observe(&store, |_: &mut Self, _, cx| cx.notify()),
|
||||
cx.subscribe_in(
|
||||
&store,
|
||||
window,
|
||||
move |this, _, event, window, cx| match event {
|
||||
extension_host::Event::ExtensionsUpdated => {
|
||||
this.fetch_extensions_debounced(cx)
|
||||
}
|
||||
extension_host::Event::ExtensionInstalled(extension_id) => this
|
||||
.on_extension_installed(
|
||||
workspace_handle.clone(),
|
||||
@@ -245,6 +243,15 @@ impl ExtensionsPage {
|
||||
_ => {}
|
||||
},
|
||||
),
|
||||
cx.subscribe_in(
|
||||
&ExtensionEvents::global(cx),
|
||||
window,
|
||||
move |this, _, event, _window, cx| match event {
|
||||
extension::Event::ExtensionsUpdated => {
|
||||
this.fetch_extensions_debounced(cx);
|
||||
}
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
let query_editor = cx.new(|cx| {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use futures::channel::oneshot;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::DirectoryLister;
|
||||
use std::{
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use ui::{prelude::*, LabelLike, ListItemSpacing};
|
||||
use ui::{prelude::*, HighlightedLabel, ListItemSpacing};
|
||||
use ui::{Context, ListItem, Window};
|
||||
use util::{maybe, paths::compare_paths};
|
||||
use workspace::Workspace;
|
||||
@@ -22,6 +22,7 @@ pub struct OpenPathDelegate {
|
||||
selected_index: usize,
|
||||
directory_state: Option<DirectoryState>,
|
||||
matches: Vec<usize>,
|
||||
string_matches: Vec<StringMatch>,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
should_dismiss: bool,
|
||||
}
|
||||
@@ -34,6 +35,7 @@ impl OpenPathDelegate {
|
||||
selected_index: 0,
|
||||
directory_state: None,
|
||||
matches: Vec::new(),
|
||||
string_matches: Vec::new(),
|
||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
should_dismiss: true,
|
||||
}
|
||||
@@ -223,6 +225,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
if suffix == "" {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.matches.clear();
|
||||
this.delegate.string_matches.clear();
|
||||
this.delegate
|
||||
.matches
|
||||
.extend(match_candidates.iter().map(|m| m.path.id));
|
||||
@@ -249,6 +252,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.matches.clear();
|
||||
this.delegate.string_matches = matches.clone();
|
||||
this.delegate
|
||||
.matches
|
||||
.extend(matches.into_iter().map(|m| m.candidate_id));
|
||||
@@ -337,13 +341,22 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
let m = self.matches.get(ix)?;
|
||||
let directory_state = self.directory_state.as_ref()?;
|
||||
let candidate = directory_state.match_candidates.get(*m)?;
|
||||
let highlight_positions = self
|
||||
.string_matches
|
||||
.iter()
|
||||
.find(|string_match| string_match.candidate_id == *m)
|
||||
.map(|string_match| string_match.positions.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(LabelLike::new().child(candidate.path.string.clone())),
|
||||
.child(HighlightedLabel::new(
|
||||
candidate.path.string.clone(),
|
||||
highlight_positions,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -54,10 +54,8 @@ actions!(
|
||||
Init,
|
||||
]
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
@@ -795,8 +795,9 @@ impl GitRepository for RealGitRepository {
|
||||
cx: AsyncApp,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
cx.background_spawn(async move {
|
||||
let mut cmd = new_smol_command("git");
|
||||
let mut cmd = new_smol_command(&git_binary_path);
|
||||
cmd.current_dir(&working_directory?)
|
||||
.envs(env)
|
||||
.args(["commit", "--quiet", "-m"])
|
||||
|
||||
@@ -146,7 +146,7 @@ impl CommitModal {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let panel = git_panel.read(cx);
|
||||
let suggested_commit_message = panel.suggest_commit_message(cx);
|
||||
let suggested_commit_message = panel.suggest_commit_message();
|
||||
|
||||
let commit_editor = git_panel.update(cx, |git_panel, cx| {
|
||||
git_panel.set_modal_open(true, cx);
|
||||
|
||||
@@ -1413,7 +1413,7 @@ impl GitPanel {
|
||||
return Some(message.to_string());
|
||||
}
|
||||
|
||||
self.suggest_commit_message(cx)
|
||||
self.suggest_commit_message()
|
||||
.filter(|message| !message.trim().is_empty())
|
||||
}
|
||||
|
||||
@@ -1582,15 +1582,7 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
/// Suggests a commit message based on the changed files and their statuses
|
||||
pub fn suggest_commit_message(&self, cx: &App) -> Option<String> {
|
||||
if let Some(merge_message) = self
|
||||
.active_repository
|
||||
.as_ref()
|
||||
.and_then(|repo| repo.read(cx).merge_message.as_ref())
|
||||
{
|
||||
return Some(merge_message.clone());
|
||||
}
|
||||
|
||||
pub fn suggest_commit_message(&self) -> Option<String> {
|
||||
let git_status_entry = if let Some(staged_entry) = &self.single_staged_entry {
|
||||
Some(staged_entry)
|
||||
} else if let Some(single_tracked_entry) = &self.single_tracked_entry {
|
||||
@@ -1726,6 +1718,19 @@ impl GitPanel {
|
||||
}));
|
||||
}
|
||||
|
||||
fn update_editor_placeholder(&mut self, cx: &mut Context<Self>) {
|
||||
let suggested_commit_message = self.suggest_commit_message();
|
||||
let placeholder_text = suggested_commit_message
|
||||
.as_deref()
|
||||
.unwrap_or("Enter commit message");
|
||||
|
||||
self.commit_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text(Arc::from(placeholder_text), cx)
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn fetch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.can_push_and_pull(cx) {
|
||||
return;
|
||||
@@ -2182,6 +2187,7 @@ impl GitPanel {
|
||||
git_panel.clear_pending();
|
||||
}
|
||||
git_panel.update_visible_entries(cx);
|
||||
git_panel.update_editor_placeholder(cx);
|
||||
git_panel.update_scrollbar_properties(window, cx);
|
||||
})
|
||||
.ok();
|
||||
@@ -2217,7 +2223,7 @@ impl GitPanel {
|
||||
git_panel.commit_editor = cx.new(|cx| {
|
||||
commit_message_editor(
|
||||
buffer,
|
||||
git_panel.suggest_commit_message(cx).as_deref(),
|
||||
git_panel.suggest_commit_message().as_deref(),
|
||||
git_panel.project.clone(),
|
||||
true,
|
||||
window,
|
||||
@@ -2237,15 +2243,7 @@ impl GitPanel {
|
||||
fn update_visible_entries(&mut self, cx: &mut Context<Self>) {
|
||||
self.entries.clear();
|
||||
self.single_staged_entry.take();
|
||||
self.single_tracked_entry.take();
|
||||
self.conflicted_count = 0;
|
||||
self.conflicted_staged_count = 0;
|
||||
self.new_count = 0;
|
||||
self.tracked_count = 0;
|
||||
self.new_staged_count = 0;
|
||||
self.tracked_staged_count = 0;
|
||||
self.entry_count = 0;
|
||||
|
||||
self.single_staged_entry.take();
|
||||
let mut changed_entries = Vec::new();
|
||||
let mut new_entries = Vec::new();
|
||||
let mut conflict_entries = Vec::new();
|
||||
@@ -2397,15 +2395,6 @@ impl GitPanel {
|
||||
|
||||
self.select_first_entry_if_none(cx);
|
||||
|
||||
let suggested_commit_message = self.suggest_commit_message(cx);
|
||||
let placeholder_text = suggested_commit_message
|
||||
.as_deref()
|
||||
.unwrap_or("Enter commit message");
|
||||
|
||||
self.commit_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text(Arc::from(placeholder_text), cx)
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -2929,10 +2918,6 @@ impl GitPanel {
|
||||
.disabled(!can_commit || self.modal_open)
|
||||
.on_click({
|
||||
cx.listener(move |this, _: &ClickEvent, window, cx| {
|
||||
telemetry::event!(
|
||||
"Git Committed",
|
||||
source = "Git Panel"
|
||||
);
|
||||
this.commit_changes(window, cx)
|
||||
})
|
||||
}),
|
||||
@@ -3063,24 +3048,21 @@ impl GitPanel {
|
||||
"No Git repositories"
|
||||
},
|
||||
))
|
||||
.children({
|
||||
let worktree_count = self.project.read(cx).visible_worktrees(cx).count();
|
||||
(worktree_count > 0 && self.active_repository.is_none()).then(|| {
|
||||
h_flex().w_full().justify_around().child(
|
||||
panel_filled_button("Initialize Repository")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"git init",
|
||||
&git::Init,
|
||||
&self.focus_handle,
|
||||
))
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(&git::Init);
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
.children(self.active_repository.is_none().then(|| {
|
||||
h_flex().w_full().justify_around().child(
|
||||
panel_filled_button("Initialize Repository")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"git init",
|
||||
&git::Init,
|
||||
&self.focus_handle,
|
||||
))
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(&git::Init);
|
||||
})
|
||||
}),
|
||||
)
|
||||
}))
|
||||
.text_ui_sm(cx)
|
||||
.mx_auto()
|
||||
.text_color(Color::Placeholder.color(cx)),
|
||||
|
||||
@@ -278,7 +278,7 @@ impl ProjectDiff {
|
||||
has_staged_hunks = true;
|
||||
has_unstaged_hunks = true;
|
||||
}
|
||||
DiffHunkSecondaryStatus::NoSecondaryHunk
|
||||
DiffHunkSecondaryStatus::None
|
||||
| DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
|
||||
has_staged_hunks = true;
|
||||
}
|
||||
@@ -308,7 +308,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 +330,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>>> {
|
||||
@@ -924,6 +919,12 @@ impl Render for ProjectDiffToolbar {
|
||||
&StageAndNext,
|
||||
&focus_handle,
|
||||
))
|
||||
// don't actually disable the button so it's mashable
|
||||
.color(if button_states.stage {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Disabled
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&StageAndNext, window, cx)
|
||||
})),
|
||||
@@ -935,6 +936,11 @@ impl Render for ProjectDiffToolbar {
|
||||
&UnstageAndNext,
|
||||
&focus_handle,
|
||||
))
|
||||
.color(if button_states.unstage {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Disabled
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&UnstageAndNext, window, cx)
|
||||
})),
|
||||
|
||||
@@ -1948,6 +1948,14 @@ impl Interactivity {
|
||||
if pending_mouse_down.is_some() && hitbox.is_hovered(window) {
|
||||
captured_mouse_down = pending_mouse_down.take();
|
||||
window.refresh();
|
||||
} else if pending_mouse_down.is_some() {
|
||||
// Clear the pending mouse down event (without firing click handlers)
|
||||
// if the hitbox is not being hovered.
|
||||
// This avoids dragging elements that changed their position
|
||||
// immediately after being clicked.
|
||||
// See https://github.com/zed-industries/zed/issues/24600 for more details
|
||||
pending_mouse_down.take();
|
||||
window.refresh();
|
||||
}
|
||||
}
|
||||
// Fire click handlers during the bubble phase.
|
||||
|
||||
@@ -4145,6 +4145,63 @@ impl BufferSnapshot {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn words_in_range(
|
||||
&self,
|
||||
query: Option<&str>,
|
||||
range: Range<usize>,
|
||||
) -> HashMap<String, Range<Anchor>> {
|
||||
if query.map_or(false, |query| query.is_empty()) {
|
||||
return HashMap::default();
|
||||
}
|
||||
|
||||
let classifier = CharClassifier::new(self.language.clone().map(|language| LanguageScope {
|
||||
language,
|
||||
override_id: None,
|
||||
}));
|
||||
|
||||
let mut query_ix = 0;
|
||||
let query = query.map(|query| query.chars().collect::<Vec<_>>());
|
||||
let query_len = query.as_ref().map_or(0, |query| query.len());
|
||||
|
||||
let mut words = HashMap::default();
|
||||
let mut current_word_start_ix = None;
|
||||
let mut chunk_ix = range.start;
|
||||
for chunk in self.chunks(range, false) {
|
||||
for (i, c) in chunk.text.char_indices() {
|
||||
let ix = chunk_ix + i;
|
||||
if classifier.is_word(c) {
|
||||
if current_word_start_ix.is_none() {
|
||||
current_word_start_ix = Some(ix);
|
||||
}
|
||||
|
||||
if let Some(query) = &query {
|
||||
if query_ix < query_len {
|
||||
let query_c = query.get(query_ix).expect(
|
||||
"query_ix is a vec of chars, which we access only if before the end",
|
||||
);
|
||||
if c.to_lowercase().eq(query_c.to_lowercase()) {
|
||||
query_ix += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
} else if let Some(word_start) = current_word_start_ix.take() {
|
||||
if query_ix == query_len {
|
||||
let word_range = self.anchor_before(word_start)..self.anchor_after(ix);
|
||||
words.insert(
|
||||
self.text_for_range(word_start..ix).collect::<String>(),
|
||||
word_range,
|
||||
);
|
||||
}
|
||||
}
|
||||
query_ix = 0;
|
||||
}
|
||||
chunk_ix += chunk.text.len();
|
||||
}
|
||||
|
||||
words
|
||||
}
|
||||
}
|
||||
|
||||
fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
|
||||
|
||||
@@ -13,6 +13,7 @@ use proto::deserialize_operation;
|
||||
use rand::prelude::*;
|
||||
use regex::RegexBuilder;
|
||||
use settings::SettingsStore;
|
||||
use std::collections::BTreeSet;
|
||||
use std::{
|
||||
env,
|
||||
ops::Range,
|
||||
@@ -3140,6 +3141,93 @@ fn test_trailing_whitespace_ranges(mut rng: StdRng) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_words_in_range(cx: &mut gpui::App) {
|
||||
init_settings(cx, |_| {});
|
||||
|
||||
let contents = r#"let word=öäpple.bar你 Öäpple word2-öÄpPlE-Pizza-word ÖÄPPLE word"#;
|
||||
|
||||
let buffer = cx.new(|cx| {
|
||||
let buffer = Buffer::local(contents, cx).with_language(Arc::new(rust_lang()), cx);
|
||||
assert_eq!(buffer.text(), contents);
|
||||
buffer.check_invariants();
|
||||
buffer
|
||||
});
|
||||
|
||||
buffer.update(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
assert_eq!(
|
||||
BTreeSet::from_iter(["Pizza".to_string()]),
|
||||
snapshot
|
||||
.words_in_range(Some("piz"), 0..snapshot.len())
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
BTreeSet::from_iter([
|
||||
"öäpple".to_string(),
|
||||
"Öäpple".to_string(),
|
||||
"öÄpPlE".to_string(),
|
||||
"ÖÄPPLE".to_string(),
|
||||
]),
|
||||
snapshot
|
||||
.words_in_range(Some("öp"), 0..snapshot.len())
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
BTreeSet::from_iter([
|
||||
"öÄpPlE".to_string(),
|
||||
"Öäpple".to_string(),
|
||||
"ÖÄPPLE".to_string(),
|
||||
"öäpple".to_string(),
|
||||
]),
|
||||
snapshot
|
||||
.words_in_range(Some("öÄ"), 0..snapshot.len())
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
BTreeSet::default(),
|
||||
snapshot
|
||||
.words_in_range(Some("öÄ好"), 0..snapshot.len())
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
BTreeSet::from_iter(["bar你".to_string(),]),
|
||||
snapshot
|
||||
.words_in_range(Some("你"), 0..snapshot.len())
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
BTreeSet::default(),
|
||||
snapshot
|
||||
.words_in_range(Some(""), 0..snapshot.len())
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
BTreeSet::from_iter([
|
||||
"bar你".to_string(),
|
||||
"öÄpPlE".to_string(),
|
||||
"Öäpple".to_string(),
|
||||
"ÖÄPPLE".to_string(),
|
||||
"öäpple".to_string(),
|
||||
"let".to_string(),
|
||||
"Pizza".to_string(),
|
||||
"word".to_string(),
|
||||
"word2".to_string(),
|
||||
]),
|
||||
snapshot
|
||||
.words_in_range(None, 0..snapshot.len())
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn ruby_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
|
||||
@@ -79,10 +79,10 @@ pub struct LanguageSettings {
|
||||
/// The column at which to soft-wrap lines, for buffers where soft-wrap
|
||||
/// is enabled.
|
||||
pub preferred_line_length: u32,
|
||||
// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
// if softwrap is set to 'preferred_line_length', and will show any
|
||||
// additional guides as specified by the 'wrap_guides' setting.
|
||||
/// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
/// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
/// if softwrap is set to 'preferred_line_length', and will show any
|
||||
/// additional guides as specified by the 'wrap_guides' setting.
|
||||
pub show_wrap_guides: bool,
|
||||
/// Character counts at which to show wrap guides (vertical rulers) in the editor.
|
||||
pub wrap_guides: Vec<usize>,
|
||||
@@ -137,7 +137,7 @@ pub struct LanguageSettings {
|
||||
pub use_on_type_format: bool,
|
||||
/// Whether indentation of pasted content should be adjusted based on the context.
|
||||
pub auto_indent_on_paste: bool,
|
||||
// Controls how the editor handles the autoclosed characters.
|
||||
/// Controls how the editor handles the autoclosed characters.
|
||||
pub always_treat_brackets_as_autoclosed: bool,
|
||||
/// Which code actions to run on save
|
||||
pub code_actions_on_format: HashMap<String, bool>,
|
||||
@@ -151,6 +151,8 @@ pub struct LanguageSettings {
|
||||
/// Whether to display inline and alongside documentation for items in the
|
||||
/// completions menu.
|
||||
pub show_completion_documentation: bool,
|
||||
/// Completion settings for this language.
|
||||
pub completions: CompletionSettings,
|
||||
}
|
||||
|
||||
impl LanguageSettings {
|
||||
@@ -306,6 +308,50 @@ pub struct AllLanguageSettingsContent {
|
||||
pub file_types: HashMap<Arc<str>, Vec<String>>,
|
||||
}
|
||||
|
||||
/// Controls how completions are processed for this language.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct CompletionSettings {
|
||||
/// Controls how words are completed.
|
||||
/// For large documents, not all words may be fetched for completion.
|
||||
///
|
||||
/// Default: `fallback`
|
||||
#[serde(default = "default_words_completion_mode")]
|
||||
pub words: WordsCompletionMode,
|
||||
/// Whether to fetch LSP completions or not.
|
||||
///
|
||||
/// Default: true
|
||||
#[serde(default = "default_true")]
|
||||
pub lsp: bool,
|
||||
/// When fetching LSP completions, determines how long to wait for a response of a particular server.
|
||||
/// When set to 0, waits indefinitely.
|
||||
///
|
||||
/// Default: 500
|
||||
#[serde(default = "lsp_fetch_timeout_ms")]
|
||||
pub lsp_fetch_timeout_ms: u64,
|
||||
}
|
||||
|
||||
/// Controls how document's words are completed.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WordsCompletionMode {
|
||||
/// Always fetch document's words for completions.
|
||||
Enabled,
|
||||
/// Only if LSP response errors/times out/is empty,
|
||||
/// use document's words to show completions.
|
||||
Fallback,
|
||||
/// Never fetch or complete document's words for completions.
|
||||
Disabled,
|
||||
}
|
||||
|
||||
fn default_words_completion_mode() -> WordsCompletionMode {
|
||||
WordsCompletionMode::Fallback
|
||||
}
|
||||
|
||||
fn lsp_fetch_timeout_ms() -> u64 {
|
||||
500
|
||||
}
|
||||
|
||||
/// The settings for a particular language.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct LanguageSettingsContent {
|
||||
@@ -478,6 +524,8 @@ pub struct LanguageSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub show_completion_documentation: Option<bool>,
|
||||
/// Controls how completions are processed for this language.
|
||||
pub completions: Option<CompletionSettings>,
|
||||
}
|
||||
|
||||
/// The behavior of `editor::Rewrap`.
|
||||
@@ -1381,6 +1429,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
|
||||
&mut settings.show_completion_documentation,
|
||||
src.show_completion_documentation,
|
||||
);
|
||||
merge(&mut settings.completions, src.completions);
|
||||
}
|
||||
|
||||
/// Allows to enable/disable formatting with Prettier
|
||||
|
||||
@@ -2007,17 +2007,22 @@ impl MultiBuffer {
|
||||
cx: &App,
|
||||
) -> Option<(Entity<Buffer>, Point, ExcerptId)> {
|
||||
let snapshot = self.read(cx);
|
||||
let (buffer, point, is_main_buffer) =
|
||||
snapshot.point_to_buffer_point(point.to_point(&snapshot))?;
|
||||
Some((
|
||||
self.buffers
|
||||
.borrow()
|
||||
.get(&buffer.remote_id())?
|
||||
let point = point.to_point(&snapshot);
|
||||
let mut cursor = snapshot.cursor::<Point>();
|
||||
cursor.seek(&point);
|
||||
|
||||
cursor.region().and_then(|region| {
|
||||
if !region.is_main_buffer {
|
||||
return None;
|
||||
}
|
||||
|
||||
let overshoot = point - region.range.start;
|
||||
let buffer_point = region.buffer_range.start + overshoot;
|
||||
let buffer = self.buffers.borrow()[®ion.buffer.remote_id()]
|
||||
.buffer
|
||||
.clone(),
|
||||
point,
|
||||
is_main_buffer,
|
||||
))
|
||||
.clone();
|
||||
Some((buffer, buffer_point, region.excerpt.id))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn buffer_point_to_anchor(
|
||||
@@ -4171,36 +4176,22 @@ impl MultiBufferSnapshot {
|
||||
let region = cursor.region()?;
|
||||
let overshoot = offset - region.range.start;
|
||||
let buffer_offset = region.buffer_range.start + overshoot;
|
||||
if buffer_offset == region.buffer.len() + 1
|
||||
&& region.has_trailing_newline
|
||||
&& !region.is_main_buffer
|
||||
{
|
||||
return Some((&cursor.excerpt()?.buffer, cursor.main_buffer_position()?));
|
||||
} else if buffer_offset > region.buffer.len() {
|
||||
if buffer_offset > region.buffer.len() {
|
||||
return None;
|
||||
}
|
||||
Some((region.buffer, buffer_offset))
|
||||
}
|
||||
|
||||
pub fn point_to_buffer_point(
|
||||
&self,
|
||||
point: Point,
|
||||
) -> Option<(&BufferSnapshot, Point, ExcerptId)> {
|
||||
pub fn point_to_buffer_point(&self, point: Point) -> Option<(&BufferSnapshot, Point, bool)> {
|
||||
let mut cursor = self.cursor::<Point>();
|
||||
cursor.seek(&point);
|
||||
let region = cursor.region()?;
|
||||
let overshoot = point - region.range.start;
|
||||
let buffer_point = region.buffer_range.start + overshoot;
|
||||
let excerpt = cursor.excerpt()?;
|
||||
if buffer_point == region.buffer.max_point() + Point::new(1, 0)
|
||||
&& region.has_trailing_newline
|
||||
&& !region.is_main_buffer
|
||||
{
|
||||
return Some((&excerpt.buffer, cursor.main_buffer_position()?, excerpt.id));
|
||||
} else if buffer_point > region.buffer.max_point() {
|
||||
if buffer_point > region.buffer.max_point() {
|
||||
return None;
|
||||
}
|
||||
Some((region.buffer, buffer_point, excerpt.id))
|
||||
Some((region.buffer, buffer_point, region.is_main_buffer))
|
||||
}
|
||||
|
||||
pub fn suggested_indents(
|
||||
@@ -4742,9 +4733,6 @@ impl MultiBufferSnapshot {
|
||||
.buffer
|
||||
.text_summary_for_range(region.buffer_range.start.key..buffer_point),
|
||||
);
|
||||
if point == region.range.end.key && region.has_trailing_newline {
|
||||
position.add_assign(&D::from_text_summary(&TextSummary::newline()));
|
||||
}
|
||||
return Some(position);
|
||||
} else {
|
||||
return Some(D::from_text_summary(&self.text_summary()));
|
||||
|
||||
@@ -3121,100 +3121,6 @@ fn test_summaries_for_anchors(cx: &mut TestAppContext) {
|
||||
assert_eq!(point_2, Point::new(3, 0));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_trailing_deletion_without_newline(cx: &mut TestAppContext) {
|
||||
let base_text_1 = "one\ntwo".to_owned();
|
||||
let text_1 = "one\n".to_owned();
|
||||
|
||||
let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
|
||||
let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(&base_text_1, &buffer_1, cx));
|
||||
cx.run_until_parked();
|
||||
|
||||
let multibuffer = cx.new(|cx| {
|
||||
let mut multibuffer = MultiBuffer::singleton(buffer_1.clone(), cx);
|
||||
multibuffer.add_diff(diff_1.clone(), cx);
|
||||
multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
|
||||
multibuffer
|
||||
});
|
||||
|
||||
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
|
||||
(multibuffer.snapshot(cx), multibuffer.subscribe())
|
||||
});
|
||||
|
||||
assert_new_snapshot(
|
||||
&multibuffer,
|
||||
&mut snapshot,
|
||||
&mut subscription,
|
||||
cx,
|
||||
indoc!(
|
||||
"
|
||||
one
|
||||
- two
|
||||
"
|
||||
),
|
||||
);
|
||||
|
||||
assert_eq!(snapshot.max_point(), Point::new(2, 0));
|
||||
assert_eq!(snapshot.len(), 8);
|
||||
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.dimensions_from_points::<Point>([Point::new(2, 0)])
|
||||
.collect::<Vec<_>>(),
|
||||
vec![Point::new(2, 0)]
|
||||
);
|
||||
|
||||
let (_, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
|
||||
assert_eq!(translated_offset, "one\n".len());
|
||||
let (_, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
|
||||
assert_eq!(translated_point, Point::new(1, 0));
|
||||
|
||||
// The same, for an excerpt that's not at the end of the multibuffer.
|
||||
|
||||
let text_2 = "foo\n".to_owned();
|
||||
let buffer_2 = cx.new(|cx| Buffer::local(&text_2, cx));
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[ExcerptRange {
|
||||
context: Point::new(0, 0)..Point::new(1, 0),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
assert_new_snapshot(
|
||||
&multibuffer,
|
||||
&mut snapshot,
|
||||
&mut subscription,
|
||||
cx,
|
||||
indoc!(
|
||||
"
|
||||
one
|
||||
- two
|
||||
|
||||
foo
|
||||
"
|
||||
),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.dimensions_from_points::<Point>([Point::new(2, 0)])
|
||||
.collect::<Vec<_>>(),
|
||||
vec![Point::new(2, 0)]
|
||||
);
|
||||
|
||||
let buffer_1_id = buffer_1.read_with(cx, |buffer_1, _| buffer_1.remote_id());
|
||||
let (buffer, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
|
||||
assert_eq!(buffer.remote_id(), buffer_1_id);
|
||||
assert_eq!(translated_offset, "one\n".len());
|
||||
let (buffer, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
|
||||
assert_eq!(buffer.remote_id(), buffer_1_id);
|
||||
assert_eq!(translated_point, Point::new(1, 0));
|
||||
}
|
||||
|
||||
fn format_diff(
|
||||
text: &str,
|
||||
row_infos: &Vec<RowInfo>,
|
||||
@@ -3473,12 +3379,16 @@ fn assert_position_translation(snapshot: &MultiBufferSnapshot) {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((buffer, offset)) = snapshot.point_to_buffer_offset(snapshot.max_point()) {
|
||||
assert!(offset <= buffer.len());
|
||||
}
|
||||
if let Some((buffer, point, _)) = snapshot.point_to_buffer_point(snapshot.max_point()) {
|
||||
assert!(point <= buffer.max_point());
|
||||
}
|
||||
let point = snapshot.max_point();
|
||||
let Some((buffer, offset)) = snapshot.point_to_buffer_offset(point) else {
|
||||
return;
|
||||
};
|
||||
assert!(offset <= buffer.len(),);
|
||||
|
||||
let Some((buffer, point, _)) = snapshot.point_to_buffer_point(point) else {
|
||||
return;
|
||||
};
|
||||
assert!(point <= buffer.max_point(),);
|
||||
}
|
||||
|
||||
fn assert_line_indents(snapshot: &MultiBufferSnapshot) {
|
||||
|
||||
@@ -318,9 +318,20 @@ impl GitStore {
|
||||
}
|
||||
// Update the statuses and merge message but keep everything else.
|
||||
let existing_handle = handle.clone();
|
||||
existing_handle.update(cx, |existing_handle, _| {
|
||||
existing_handle.update(cx, |existing_handle, cx| {
|
||||
existing_handle.repository_entry = repo.clone();
|
||||
if matches!(git_repo, GitRepo::Local { .. }) {
|
||||
if matches!(git_repo, GitRepo::Local { .. })
|
||||
&& existing_handle.merge_message != merge_message
|
||||
{
|
||||
if let (Some(merge_message), Some(buffer)) =
|
||||
(&merge_message, &existing_handle.commit_message_buffer)
|
||||
{
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
if buffer.is_empty() {
|
||||
buffer.set_text(merge_message.as_str(), cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
existing_handle.merge_message = merge_message;
|
||||
}
|
||||
});
|
||||
@@ -1246,6 +1257,7 @@ impl Repository {
|
||||
buffer_store: Entity<BufferStore>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<Buffer>>> {
|
||||
let merge_message = self.merge_message.clone();
|
||||
cx.spawn(|repository, mut cx| async move {
|
||||
let buffer = buffer_store
|
||||
.update(&mut cx, |buffer_store, cx| buffer_store.create_buffer(cx))?
|
||||
@@ -1258,6 +1270,12 @@ impl Repository {
|
||||
})?;
|
||||
}
|
||||
|
||||
if let Some(merge_message) = merge_message {
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_text(merge_message.as_str(), cx)
|
||||
})?;
|
||||
}
|
||||
|
||||
repository.update(&mut cx, |repository, _| {
|
||||
repository.commit_message_buffer = Some(buffer.clone());
|
||||
})?;
|
||||
|
||||
@@ -23,13 +23,13 @@ use client::{proto, TypedEnvelope};
|
||||
use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use futures::{
|
||||
future::{join_all, Shared},
|
||||
select,
|
||||
select, select_biased,
|
||||
stream::FuturesUnordered,
|
||||
AsyncWriteExt, Future, FutureExt, StreamExt,
|
||||
};
|
||||
use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder};
|
||||
use gpui::{
|
||||
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString, Task,
|
||||
App, AppContext, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
use http_client::HttpClient;
|
||||
@@ -4325,6 +4325,15 @@ impl LspStore {
|
||||
let offset = position.to_offset(&snapshot);
|
||||
let scope = snapshot.language_scope_at(offset);
|
||||
let language = snapshot.language().cloned();
|
||||
let completion_settings = language_settings(
|
||||
language.as_ref().map(|language| language.name()),
|
||||
buffer.read(cx).file(),
|
||||
cx,
|
||||
)
|
||||
.completions;
|
||||
if !completion_settings.lsp {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
}
|
||||
|
||||
let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| {
|
||||
local
|
||||
@@ -4341,23 +4350,51 @@ impl LspStore {
|
||||
});
|
||||
|
||||
let buffer = buffer.clone();
|
||||
let lsp_timeout = completion_settings.lsp_fetch_timeout_ms;
|
||||
let lsp_timeout = if lsp_timeout > 0 {
|
||||
Some(Duration::from_millis(lsp_timeout))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let mut tasks = Vec::with_capacity(server_ids.len());
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |lsp_store, cx| {
|
||||
for server_id in server_ids {
|
||||
let lsp_adapter = this.language_server_adapter_for_id(server_id);
|
||||
tasks.push((
|
||||
lsp_adapter,
|
||||
this.request_lsp(
|
||||
buffer.clone(),
|
||||
LanguageServerToQuery::Other(server_id),
|
||||
GetCompletions {
|
||||
position,
|
||||
context: context.clone(),
|
||||
let lsp_adapter = lsp_store.language_server_adapter_for_id(server_id);
|
||||
let lsp_timeout = lsp_timeout
|
||||
.map(|lsp_timeout| cx.background_executor().timer(lsp_timeout));
|
||||
let mut timeout = cx.background_spawn(async move {
|
||||
match lsp_timeout {
|
||||
Some(lsp_timeout) => {
|
||||
lsp_timeout.await;
|
||||
true
|
||||
},
|
||||
cx,
|
||||
),
|
||||
));
|
||||
None => false,
|
||||
}
|
||||
}).fuse();
|
||||
let mut lsp_request = lsp_store.request_lsp(
|
||||
buffer.clone(),
|
||||
LanguageServerToQuery::Other(server_id),
|
||||
GetCompletions {
|
||||
position,
|
||||
context: context.clone(),
|
||||
},
|
||||
cx,
|
||||
).fuse();
|
||||
let new_task = cx.background_spawn(async move {
|
||||
select_biased! {
|
||||
response = lsp_request => response,
|
||||
timeout_happened = timeout => {
|
||||
if timeout_happened {
|
||||
log::warn!("Fetching completions from server {server_id} timed out, timeout ms: {}", completion_settings.lsp_fetch_timeout_ms);
|
||||
return anyhow::Ok(Vec::new())
|
||||
} else {
|
||||
lsp_request.await
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
tasks.push((lsp_adapter, new_task));
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -4416,47 +4453,58 @@ impl LspStore {
|
||||
{
|
||||
did_resolve = true;
|
||||
}
|
||||
} else {
|
||||
resolve_word_completion(
|
||||
&buffer_snapshot,
|
||||
&mut completions.borrow_mut()[completion_index],
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for completion_index in completion_indices {
|
||||
let Some(server_id) = completions.borrow()[completion_index].source.server_id()
|
||||
else {
|
||||
continue;
|
||||
let server_id = {
|
||||
let completion = &completions.borrow()[completion_index];
|
||||
completion.source.server_id()
|
||||
};
|
||||
if let Some(server_id) = server_id {
|
||||
let server_and_adapter = this
|
||||
.read_with(&cx, |lsp_store, _| {
|
||||
let server = lsp_store.language_server_for_id(server_id)?;
|
||||
let adapter =
|
||||
lsp_store.language_server_adapter_for_id(server.server_id())?;
|
||||
Some((server, adapter))
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let Some((server, adapter)) = server_and_adapter else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let server_and_adapter = this
|
||||
.read_with(&cx, |lsp_store, _| {
|
||||
let server = lsp_store.language_server_for_id(server_id)?;
|
||||
let adapter =
|
||||
lsp_store.language_server_adapter_for_id(server.server_id())?;
|
||||
Some((server, adapter))
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let Some((server, adapter)) = server_and_adapter else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let resolved = Self::resolve_completion_local(
|
||||
server,
|
||||
&buffer_snapshot,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
)
|
||||
.await
|
||||
.log_err()
|
||||
.is_some();
|
||||
if resolved {
|
||||
Self::regenerate_completion_labels(
|
||||
adapter,
|
||||
let resolved = Self::resolve_completion_local(
|
||||
server,
|
||||
&buffer_snapshot,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
did_resolve = true;
|
||||
.log_err()
|
||||
.is_some();
|
||||
if resolved {
|
||||
Self::regenerate_completion_labels(
|
||||
adapter,
|
||||
&buffer_snapshot,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
did_resolve = true;
|
||||
}
|
||||
} else {
|
||||
resolve_word_completion(
|
||||
&buffer_snapshot,
|
||||
&mut completions.borrow_mut()[completion_index],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4500,7 +4548,9 @@ impl LspStore {
|
||||
);
|
||||
server.request::<lsp::request::ResolveCompletionItem>(*lsp_completion.clone())
|
||||
}
|
||||
CompletionSource::Custom => return Ok(()),
|
||||
CompletionSource::BufferWord { .. } | CompletionSource::Custom => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
let resolved_completion = request.await?;
|
||||
@@ -4641,7 +4691,9 @@ impl LspStore {
|
||||
}
|
||||
serde_json::to_string(lsp_completion).unwrap().into_bytes()
|
||||
}
|
||||
CompletionSource::Custom => return Ok(()),
|
||||
CompletionSource::Custom | CompletionSource::BufferWord { .. } => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
let request = proto::ResolveCompletionDocumentation {
|
||||
@@ -8172,51 +8224,54 @@ impl LspStore {
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_completion(completion: &CoreCompletion) -> proto::Completion {
|
||||
let (source, server_id, lsp_completion, lsp_defaults, resolved) = match &completion.source {
|
||||
let mut serialized_completion = proto::Completion {
|
||||
old_start: Some(serialize_anchor(&completion.old_range.start)),
|
||||
old_end: Some(serialize_anchor(&completion.old_range.end)),
|
||||
new_text: completion.new_text.clone(),
|
||||
..proto::Completion::default()
|
||||
};
|
||||
match &completion.source {
|
||||
CompletionSource::Lsp {
|
||||
server_id,
|
||||
lsp_completion,
|
||||
lsp_defaults,
|
||||
resolved,
|
||||
} => (
|
||||
proto::completion::Source::Lsp as i32,
|
||||
server_id.0 as u64,
|
||||
serde_json::to_vec(lsp_completion).unwrap(),
|
||||
lsp_defaults
|
||||
} => {
|
||||
serialized_completion.source = proto::completion::Source::Lsp as i32;
|
||||
serialized_completion.server_id = server_id.0 as u64;
|
||||
serialized_completion.lsp_completion = serde_json::to_vec(lsp_completion).unwrap();
|
||||
serialized_completion.lsp_defaults = lsp_defaults
|
||||
.as_deref()
|
||||
.map(|lsp_defaults| serde_json::to_vec(lsp_defaults).unwrap()),
|
||||
*resolved,
|
||||
),
|
||||
CompletionSource::Custom => (
|
||||
proto::completion::Source::Custom as i32,
|
||||
0,
|
||||
Vec::new(),
|
||||
None,
|
||||
true,
|
||||
),
|
||||
};
|
||||
|
||||
proto::Completion {
|
||||
old_start: Some(serialize_anchor(&completion.old_range.start)),
|
||||
old_end: Some(serialize_anchor(&completion.old_range.end)),
|
||||
new_text: completion.new_text.clone(),
|
||||
server_id,
|
||||
lsp_completion,
|
||||
lsp_defaults,
|
||||
resolved,
|
||||
source,
|
||||
.map(|lsp_defaults| serde_json::to_vec(lsp_defaults).unwrap());
|
||||
serialized_completion.resolved = *resolved;
|
||||
}
|
||||
CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved,
|
||||
} => {
|
||||
serialized_completion.source = proto::completion::Source::BufferWord as i32;
|
||||
serialized_completion.buffer_word_start = Some(serialize_anchor(&word_range.start));
|
||||
serialized_completion.buffer_word_end = Some(serialize_anchor(&word_range.end));
|
||||
serialized_completion.resolved = *resolved;
|
||||
}
|
||||
CompletionSource::Custom => {
|
||||
serialized_completion.source = proto::completion::Source::Custom as i32;
|
||||
serialized_completion.resolved = true;
|
||||
}
|
||||
}
|
||||
|
||||
serialized_completion
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize_completion(completion: proto::Completion) -> Result<CoreCompletion> {
|
||||
let old_start = completion
|
||||
.old_start
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid old start"))?;
|
||||
.context("invalid old start")?;
|
||||
let old_end = completion
|
||||
.old_end
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid old end"))?;
|
||||
.context("invalid old end")?;
|
||||
Ok(CoreCompletion {
|
||||
old_range: old_start..old_end,
|
||||
new_text: completion.new_text,
|
||||
@@ -8232,6 +8287,20 @@ impl LspStore {
|
||||
.transpose()?,
|
||||
resolved: completion.resolved,
|
||||
},
|
||||
Some(proto::completion::Source::BufferWord) => {
|
||||
let word_range = completion
|
||||
.buffer_word_start
|
||||
.and_then(deserialize_anchor)
|
||||
.context("invalid buffer word start")?
|
||||
..completion
|
||||
.buffer_word_end
|
||||
.and_then(deserialize_anchor)
|
||||
.context("invalid buffer word end")?;
|
||||
CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved: completion.resolved,
|
||||
}
|
||||
}
|
||||
_ => anyhow::bail!("Unexpected completion source {}", completion.source),
|
||||
},
|
||||
})
|
||||
@@ -8296,6 +8365,40 @@ impl LspStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_word_completion(snapshot: &BufferSnapshot, completion: &mut Completion) {
|
||||
let CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved,
|
||||
} = &mut completion.source
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if *resolved {
|
||||
return;
|
||||
}
|
||||
|
||||
if completion.new_text
|
||||
!= snapshot
|
||||
.text_for_range(word_range.clone())
|
||||
.collect::<String>()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let mut offset = 0;
|
||||
for chunk in snapshot.chunks(word_range.clone(), true) {
|
||||
let end_offset = offset + chunk.text.len();
|
||||
if let Some(highlight_id) = chunk.syntax_highlight_id {
|
||||
completion
|
||||
.label
|
||||
.runs
|
||||
.push((offset..end_offset, highlight_id));
|
||||
}
|
||||
offset = end_offset;
|
||||
}
|
||||
*resolved = true;
|
||||
}
|
||||
|
||||
impl EventEmitter<LspStoreEvent> for LspStore {}
|
||||
|
||||
fn remove_empty_hover_blocks(mut hover: Hover) -> Option<Hover> {
|
||||
|
||||
@@ -388,6 +388,10 @@ pub enum CompletionSource {
|
||||
resolved: bool,
|
||||
},
|
||||
Custom,
|
||||
BufferWord {
|
||||
word_range: Range<Anchor>,
|
||||
resolved: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl CompletionSource {
|
||||
|
||||
@@ -168,10 +168,6 @@ pub struct GitSettings {
|
||||
///
|
||||
/// Default: on
|
||||
pub inline_blame: Option<InlineBlameSettings>,
|
||||
/// How hunks are displayed visually in the editor.
|
||||
///
|
||||
/// Default: staged_hollow
|
||||
pub hunk_style: Option<GitHunkStyleSetting>,
|
||||
}
|
||||
|
||||
impl GitSettings {
|
||||
@@ -207,11 +203,20 @@ impl GitSettings {
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GitHunkStyleSetting {
|
||||
/// Show unstaged hunks with a filled background and staged hunks hollow.
|
||||
/// Show unstaged hunks with a transparent background
|
||||
#[default]
|
||||
StagedHollow,
|
||||
/// Show unstaged hunks hollow and staged hunks with a filled background.
|
||||
UnstagedHollow,
|
||||
Transparent,
|
||||
/// Show unstaged hunks with a pattern background
|
||||
Pattern,
|
||||
/// Show unstaged hunks with a border background
|
||||
Border,
|
||||
|
||||
/// Show staged hunks with a pattern background
|
||||
StagedPattern,
|
||||
/// Show staged hunks with a pattern background
|
||||
StagedTransparent,
|
||||
/// Show staged hunks with a pattern background
|
||||
StagedBorder,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
|
||||
@@ -941,7 +941,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Start the language server by opening a buffer with a compatible file extension.
|
||||
project
|
||||
let _ = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp(path!("/the-root/src/a.rs"), cx)
|
||||
})
|
||||
@@ -6008,7 +6008,7 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
|
||||
0..0,
|
||||
"// the-deleted-contents\n",
|
||||
"",
|
||||
DiffHunkStatus::deleted(DiffHunkSecondaryStatus::NoSecondaryHunk),
|
||||
DiffHunkStatus::deleted(DiffHunkSecondaryStatus::None),
|
||||
)],
|
||||
);
|
||||
});
|
||||
@@ -6168,12 +6168,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
||||
"",
|
||||
DiffHunkStatus::deleted(HasSecondaryHunk),
|
||||
),
|
||||
(
|
||||
1..2,
|
||||
"two\n",
|
||||
"TWO\n",
|
||||
DiffHunkStatus::modified(NoSecondaryHunk),
|
||||
),
|
||||
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
|
||||
(
|
||||
3..4,
|
||||
"four\n",
|
||||
@@ -6222,12 +6217,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
||||
"",
|
||||
DiffHunkStatus::deleted(HasSecondaryHunk),
|
||||
),
|
||||
(
|
||||
1..2,
|
||||
"two\n",
|
||||
"TWO\n",
|
||||
DiffHunkStatus::modified(NoSecondaryHunk),
|
||||
),
|
||||
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
|
||||
(
|
||||
3..4,
|
||||
"four\n",
|
||||
@@ -6266,12 +6256,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
||||
"",
|
||||
DiffHunkStatus::deleted(HasSecondaryHunk),
|
||||
),
|
||||
(
|
||||
1..2,
|
||||
"two\n",
|
||||
"TWO\n",
|
||||
DiffHunkStatus::modified(NoSecondaryHunk),
|
||||
),
|
||||
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
|
||||
(
|
||||
3..4,
|
||||
"four\n",
|
||||
@@ -6292,223 +6277,6 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
||||
} else {
|
||||
panic!("Unexpected event {event:?}");
|
||||
}
|
||||
|
||||
// Allow writing to the git index to succeed again.
|
||||
fs.set_error_message_for_index_write("/dir/.git".as_ref(), None);
|
||||
|
||||
// Stage two hunks with separate operations.
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
let hunks = diff.hunks(&snapshot, cx).collect::<Vec<_>>();
|
||||
diff.stage_or_unstage_hunks(true, &hunks[0..1], &snapshot, true, cx);
|
||||
diff.stage_or_unstage_hunks(true, &hunks[2..3], &snapshot, true, cx);
|
||||
});
|
||||
|
||||
// Both staged hunks appear as pending.
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&[
|
||||
(
|
||||
0..0,
|
||||
"zero\n",
|
||||
"",
|
||||
DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
|
||||
),
|
||||
(
|
||||
1..2,
|
||||
"two\n",
|
||||
"TWO\n",
|
||||
DiffHunkStatus::modified(NoSecondaryHunk),
|
||||
),
|
||||
(
|
||||
3..4,
|
||||
"four\n",
|
||||
"FOUR\n",
|
||||
DiffHunkStatus::modified(SecondaryHunkRemovalPending),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
// Both staging operations take effect.
|
||||
cx.run_until_parked();
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&[
|
||||
(0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)),
|
||||
(
|
||||
1..2,
|
||||
"two\n",
|
||||
"TWO\n",
|
||||
DiffHunkStatus::modified(NoSecondaryHunk),
|
||||
),
|
||||
(
|
||||
3..4,
|
||||
"four\n",
|
||||
"FOUR\n",
|
||||
DiffHunkStatus::modified(NoSecondaryHunk),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::format_collect)]
|
||||
#[gpui::test]
|
||||
async fn test_staging_lots_of_hunks_fast(cx: &mut gpui::TestAppContext) {
|
||||
use DiffHunkSecondaryStatus::*;
|
||||
init_test(cx);
|
||||
|
||||
let different_lines = (0..500)
|
||||
.step_by(5)
|
||||
.map(|i| format!("diff {}\n", i))
|
||||
.collect::<Vec<String>>();
|
||||
let committed_contents = (0..500).map(|i| format!("{}\n", i)).collect::<String>();
|
||||
let file_contents = (0..500)
|
||||
.map(|i| {
|
||||
if i % 5 == 0 {
|
||||
different_lines[i / 5].clone()
|
||||
} else {
|
||||
format!("{}\n", i)
|
||||
}
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
".git": {},
|
||||
"file.txt": file_contents.clone()
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_head_for_repo(
|
||||
"/dir/.git".as_ref(),
|
||||
&[("file.txt".into(), committed_contents.clone())],
|
||||
);
|
||||
fs.set_index_for_repo(
|
||||
"/dir/.git".as_ref(),
|
||||
&[("file.txt".into(), committed_contents.clone())],
|
||||
);
|
||||
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/dir/file.txt", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
let uncommitted_diff = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_uncommitted_diff(buffer.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let range = Anchor::MIN..snapshot.anchor_after(snapshot.max_point());
|
||||
|
||||
let mut expected_hunks: Vec<(Range<u32>, String, String, DiffHunkStatus)> = (0..500)
|
||||
.step_by(5)
|
||||
.map(|i| {
|
||||
(
|
||||
i as u32..i as u32 + 1,
|
||||
format!("{}\n", i),
|
||||
different_lines[i / 5].clone(),
|
||||
DiffHunkStatus::modified(HasSecondaryHunk),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// The hunks are initially unstaged
|
||||
uncommitted_diff.read_with(cx, |diff, cx| {
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&expected_hunks,
|
||||
);
|
||||
});
|
||||
|
||||
for (_, _, _, status) in expected_hunks.iter_mut() {
|
||||
*status = DiffHunkStatus::modified(SecondaryHunkRemovalPending);
|
||||
}
|
||||
|
||||
// Stage every hunk with a different call
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
let hunks = diff
|
||||
.hunks_intersecting_range(range.clone(), &snapshot, cx)
|
||||
.collect::<Vec<_>>();
|
||||
for hunk in hunks {
|
||||
diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
|
||||
}
|
||||
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&expected_hunks,
|
||||
);
|
||||
});
|
||||
|
||||
// If we wait, we'll have no pending hunks
|
||||
cx.run_until_parked();
|
||||
for (_, _, _, status) in expected_hunks.iter_mut() {
|
||||
*status = DiffHunkStatus::modified(NoSecondaryHunk);
|
||||
}
|
||||
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&expected_hunks,
|
||||
);
|
||||
});
|
||||
|
||||
for (_, _, _, status) in expected_hunks.iter_mut() {
|
||||
*status = DiffHunkStatus::modified(SecondaryHunkAdditionPending);
|
||||
}
|
||||
|
||||
// Unstage every hunk with a different call
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
let hunks = diff
|
||||
.hunks_intersecting_range(range, &snapshot, cx)
|
||||
.collect::<Vec<_>>();
|
||||
for hunk in hunks {
|
||||
diff.stage_or_unstage_hunks(false, &[hunk], &snapshot, true, cx);
|
||||
}
|
||||
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&expected_hunks,
|
||||
);
|
||||
});
|
||||
|
||||
// If we wait, we'll have no pending hunks, again
|
||||
cx.run_until_parked();
|
||||
for (_, _, _, status) in expected_hunks.iter_mut() {
|
||||
*status = DiffHunkStatus::modified(HasSecondaryHunk);
|
||||
}
|
||||
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&expected_hunks,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -1002,10 +1002,13 @@ message Completion {
|
||||
bool resolved = 6;
|
||||
Source source = 7;
|
||||
optional bytes lsp_defaults = 8;
|
||||
optional Anchor buffer_word_start = 9;
|
||||
optional Anchor buffer_word_end = 10;
|
||||
|
||||
enum Source {
|
||||
Lsp = 0;
|
||||
Custom = 1;
|
||||
BufferWord = 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -406,7 +406,6 @@ where
|
||||
self.seek_internal(pos, bias, &mut (), cx)
|
||||
}
|
||||
|
||||
/// Advances the cursor and returns traversed items as a tree.
|
||||
#[track_caller]
|
||||
pub fn slice<Target>(
|
||||
&mut self,
|
||||
|
||||
@@ -225,15 +225,6 @@ impl<T: Item> SumTree<T> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Useful in cases where the item type has a non-trivial context type, but the zero value of the summary type doesn't depend on that context.
|
||||
pub fn from_summary(summary: T::Summary) -> Self {
|
||||
SumTree(Arc::new(Node::Leaf {
|
||||
summary,
|
||||
items: ArrayVec::new(),
|
||||
item_summaries: ArrayVec::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn from_item(item: T, cx: &<T::Summary as Summary>::Context) -> Self {
|
||||
let mut tree = Self::new(cx);
|
||||
tree.push(item, cx);
|
||||
|
||||
@@ -935,19 +935,12 @@ impl Terminal {
|
||||
|
||||
if is_path_surrounded_by_common_symbols(&file_path) {
|
||||
word_match = Match::new(
|
||||
word_match.start().add(term, Boundary::Grid, 1),
|
||||
word_match.end().sub(term, Boundary::Grid, 1),
|
||||
word_match.start().add(term, Boundary::Cursor, 1),
|
||||
word_match.end().sub(term, Boundary::Cursor, 1),
|
||||
);
|
||||
file_path = file_path[1..file_path.len() - 1].to_owned();
|
||||
}
|
||||
|
||||
while file_path.ends_with(':') {
|
||||
file_path.pop();
|
||||
word_match = Match::new(
|
||||
*word_match.start(),
|
||||
word_match.end().sub(term, Boundary::Grid, 1),
|
||||
);
|
||||
}
|
||||
let mut colon_count = 0;
|
||||
for c in file_path.chars() {
|
||||
if c == ':' {
|
||||
@@ -973,7 +966,7 @@ impl Terminal {
|
||||
let stripped_len = file_path.len() - last_index;
|
||||
word_match = Match::new(
|
||||
*word_match.start(),
|
||||
word_match.end().sub(term, Boundary::Grid, stripped_len),
|
||||
word_match.end().sub(term, Boundary::Cursor, stripped_len),
|
||||
);
|
||||
file_path = file_path[0..last_index].to_owned();
|
||||
}
|
||||
|
||||
@@ -136,7 +136,6 @@ where
|
||||
|
||||
pub trait AnchorRangeExt {
|
||||
fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Ordering;
|
||||
fn overlaps(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> bool;
|
||||
}
|
||||
|
||||
impl AnchorRangeExt for Range<Anchor> {
|
||||
@@ -146,8 +145,4 @@ impl AnchorRangeExt for Range<Anchor> {
|
||||
ord => ord,
|
||||
}
|
||||
}
|
||||
|
||||
fn overlaps(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> bool {
|
||||
self.start.cmp(&other.end, buffer).is_lt() && other.start.cmp(&self.end, buffer).is_lt()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use gpui::{
|
||||
AnyView, DismissEvent, Entity, FocusHandle, Focusable as _, ManagedView, MouseButton,
|
||||
Subscription,
|
||||
};
|
||||
use gpui::{AnyView, DismissEvent, Entity, FocusHandle, Focusable as _, ManagedView, Subscription};
|
||||
use ui::prelude::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -175,13 +172,11 @@ impl Render for ModalLayer {
|
||||
let mut background = cx.theme().colors().elevated_surface_background;
|
||||
background.fade_out(0.2);
|
||||
el.bg(background)
|
||||
.occlude()
|
||||
.on_mouse_down_out(cx.listener(|this, _, window, cx| {
|
||||
this.hide_modal(window, cx);
|
||||
}))
|
||||
})
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.hide_modal(window, cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.h(px(0.0))
|
||||
@@ -190,14 +185,7 @@ impl Render for ModalLayer {
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.track_focus(&active_modal.focus_handle)
|
||||
.child(
|
||||
h_flex()
|
||||
.occlude()
|
||||
.child(active_modal.modal.view())
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
),
|
||||
.child(h_flex().occlude().child(active_modal.modal.view())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use ui::{animation::DefaultAnimations, prelude::*};
|
||||
|
||||
use crate::Workspace;
|
||||
|
||||
const DEFAULT_TOAST_DURATION: Duration = Duration::from_secs(10);
|
||||
const DEFAULT_TOAST_DURATION: Duration = Duration::from_millis(2400);
|
||||
const MINIMUM_RESUME_DURATION: Duration = Duration::from_millis(800);
|
||||
|
||||
actions!(toast, [RunAction]);
|
||||
|
||||
@@ -61,7 +61,7 @@ use std::{
|
||||
path::{Component, Path, PathBuf},
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{self, AtomicI32, AtomicUsize, Ordering::SeqCst},
|
||||
atomic::{self, AtomicU32, AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
@@ -1525,7 +1525,6 @@ impl LocalWorktree {
|
||||
fs,
|
||||
fs_case_sensitive,
|
||||
status_updates_tx: scan_states_tx,
|
||||
scans_running: Arc::new(AtomicI32::new(0)),
|
||||
executor: background,
|
||||
scan_requests_rx,
|
||||
path_prefixes_to_scan_rx,
|
||||
@@ -4250,6 +4249,11 @@ struct PathEntry {
|
||||
scan_id: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct FsScanned {
|
||||
status_scans: Arc<AtomicU32>,
|
||||
}
|
||||
|
||||
impl sum_tree::Item for PathEntry {
|
||||
type Summary = PathEntrySummary;
|
||||
|
||||
@@ -4317,7 +4321,6 @@ struct BackgroundScanner {
|
||||
fs: Arc<dyn Fs>,
|
||||
fs_case_sensitive: bool,
|
||||
status_updates_tx: UnboundedSender<ScanState>,
|
||||
scans_running: Arc<AtomicI32>,
|
||||
executor: BackgroundExecutor,
|
||||
scan_requests_rx: channel::Receiver<ScanRequest>,
|
||||
path_prefixes_to_scan_rx: channel::Receiver<PathPrefixScanRequest>,
|
||||
@@ -4425,13 +4428,13 @@ impl BackgroundScanner {
|
||||
|
||||
// Perform an initial scan of the directory.
|
||||
drop(scan_job_tx);
|
||||
self.scan_dirs(true, scan_job_rx).await;
|
||||
let scans_running = self.scan_dirs(true, scan_job_rx).await;
|
||||
{
|
||||
let mut state = self.state.lock();
|
||||
state.snapshot.completed_scan_id = state.snapshot.scan_id;
|
||||
}
|
||||
|
||||
let scanning = self.scans_running.load(atomic::Ordering::Acquire) > 0;
|
||||
let scanning = scans_running.status_scans.load(atomic::Ordering::Acquire) > 0;
|
||||
self.send_status_update(scanning, SmallVec::new());
|
||||
|
||||
// Process any any FS events that occurred while performing the initial scan.
|
||||
@@ -4458,7 +4461,7 @@ impl BackgroundScanner {
|
||||
// these before handling changes reported by the filesystem.
|
||||
request = self.next_scan_request().fuse() => {
|
||||
let Ok(request) = request else { break };
|
||||
let scanning = self.scans_running.load(atomic::Ordering::Acquire) > 0;
|
||||
let scanning = scans_running.status_scans.load(atomic::Ordering::Acquire) > 0;
|
||||
if !self.process_scan_request(request, scanning).await {
|
||||
return;
|
||||
}
|
||||
@@ -4481,7 +4484,7 @@ impl BackgroundScanner {
|
||||
self.process_events(vec![abs_path]).await;
|
||||
}
|
||||
}
|
||||
let scanning = self.scans_running.load(atomic::Ordering::Acquire) > 0;
|
||||
let scanning = scans_running.status_scans.load(atomic::Ordering::Acquire) > 0;
|
||||
self.send_status_update(scanning, request.done);
|
||||
}
|
||||
|
||||
@@ -4675,7 +4678,7 @@ impl BackgroundScanner {
|
||||
.await;
|
||||
|
||||
self.update_ignore_statuses(scan_job_tx).await;
|
||||
self.scan_dirs(false, scan_job_rx).await;
|
||||
let scans_running = self.scan_dirs(false, scan_job_rx).await;
|
||||
|
||||
let status_update = if !dot_git_abs_paths.is_empty() {
|
||||
Some(self.update_git_repositories(dot_git_abs_paths))
|
||||
@@ -4686,7 +4689,6 @@ impl BackgroundScanner {
|
||||
let phase = self.phase;
|
||||
let status_update_tx = self.status_updates_tx.clone();
|
||||
let state = self.state.clone();
|
||||
let scans_running = self.scans_running.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
if let Some(status_update) = status_update {
|
||||
@@ -4702,7 +4704,7 @@ impl BackgroundScanner {
|
||||
#[cfg(test)]
|
||||
state.snapshot.check_git_invariants();
|
||||
}
|
||||
let scanning = scans_running.load(atomic::Ordering::Acquire) > 0;
|
||||
let scanning = scans_running.status_scans.load(atomic::Ordering::Acquire) > 0;
|
||||
send_status_update_inner(phase, state, status_update_tx, scanning, SmallVec::new());
|
||||
})
|
||||
.detach();
|
||||
@@ -4727,8 +4729,9 @@ impl BackgroundScanner {
|
||||
}
|
||||
drop(scan_job_tx);
|
||||
}
|
||||
let scans_running = Arc::new(AtomicU32::new(0));
|
||||
while let Ok(job) = scan_job_rx.recv().await {
|
||||
self.scan_dir(&job).await.log_err();
|
||||
self.scan_dir(&scans_running, &job).await.log_err();
|
||||
}
|
||||
|
||||
!mem::take(&mut self.state.lock().paths_to_scan).is_empty()
|
||||
@@ -4738,16 +4741,16 @@ impl BackgroundScanner {
|
||||
&self,
|
||||
enable_progress_updates: bool,
|
||||
scan_jobs_rx: channel::Receiver<ScanJob>,
|
||||
) {
|
||||
) -> FsScanned {
|
||||
if self
|
||||
.status_updates_tx
|
||||
.unbounded_send(ScanState::Started)
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
return FsScanned::default();
|
||||
}
|
||||
|
||||
inc_scans_running(&self.scans_running);
|
||||
let scans_running = Arc::new(AtomicU32::new(1));
|
||||
let progress_update_count = AtomicUsize::new(0);
|
||||
self.executor
|
||||
.scoped(|scope| {
|
||||
@@ -4792,7 +4795,7 @@ impl BackgroundScanner {
|
||||
// Recursively load directories from the file system.
|
||||
job = scan_jobs_rx.recv().fuse() => {
|
||||
let Ok(job) = job else { break };
|
||||
if let Err(err) = self.scan_dir(&job).await {
|
||||
if let Err(err) = self.scan_dir(&scans_running, &job).await {
|
||||
if job.path.as_ref() != Path::new("") {
|
||||
log::error!("error scanning directory {:?}: {}", job.abs_path, err);
|
||||
}
|
||||
@@ -4805,7 +4808,10 @@ impl BackgroundScanner {
|
||||
})
|
||||
.await;
|
||||
|
||||
dec_scans_running(&self.scans_running, 1);
|
||||
scans_running.fetch_sub(1, atomic::Ordering::Release);
|
||||
FsScanned {
|
||||
status_scans: scans_running,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_status_update(&self, scanning: bool, barrier: SmallVec<[barrier::Sender; 1]>) -> bool {
|
||||
@@ -4818,7 +4824,7 @@ impl BackgroundScanner {
|
||||
)
|
||||
}
|
||||
|
||||
async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
|
||||
async fn scan_dir(&self, scans_running: &Arc<AtomicU32>, job: &ScanJob) -> Result<()> {
|
||||
let root_abs_path;
|
||||
let root_char_bag;
|
||||
{
|
||||
@@ -4873,7 +4879,7 @@ impl BackgroundScanner {
|
||||
self.watcher.as_ref(),
|
||||
);
|
||||
if let Some(local_repo) = repo {
|
||||
inc_scans_running(&self.scans_running);
|
||||
scans_running.fetch_add(1, atomic::Ordering::Release);
|
||||
git_status_update_jobs
|
||||
.push(self.schedule_git_statuses_update(&mut state, local_repo));
|
||||
}
|
||||
@@ -4996,7 +5002,7 @@ impl BackgroundScanner {
|
||||
let task_state = self.state.clone();
|
||||
let phase = self.phase;
|
||||
let status_updates_tx = self.status_updates_tx.clone();
|
||||
let scans_running = self.scans_running.clone();
|
||||
let scans_running = scans_running.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
if !git_status_update_jobs.is_empty() {
|
||||
@@ -5004,7 +5010,7 @@ impl BackgroundScanner {
|
||||
let status_updated = status_updates
|
||||
.iter()
|
||||
.any(|update_result| update_result.is_ok());
|
||||
dec_scans_running(&scans_running, status_updates.len() as i32);
|
||||
scans_running.fetch_sub(status_updates.len() as u32, atomic::Ordering::Release);
|
||||
if status_updated {
|
||||
let scanning = scans_running.load(atomic::Ordering::Acquire) > 0;
|
||||
send_status_update_inner(
|
||||
@@ -5464,7 +5470,6 @@ impl BackgroundScanner {
|
||||
}
|
||||
};
|
||||
|
||||
inc_scans_running(&self.scans_running);
|
||||
status_updates
|
||||
.push(self.schedule_git_statuses_update(&mut state, local_repository));
|
||||
}
|
||||
@@ -5497,12 +5502,9 @@ impl BackgroundScanner {
|
||||
});
|
||||
}
|
||||
|
||||
let scans_running = self.scans_running.clone();
|
||||
self.executor.spawn(async move {
|
||||
let updates_finished: Vec<Result<(), oneshot::Canceled>> =
|
||||
let _updates_finished: Vec<Result<(), oneshot::Canceled>> =
|
||||
join_all(status_updates).await;
|
||||
let n = updates_finished.len();
|
||||
dec_scans_running(&scans_running, n as i32);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5510,15 +5512,106 @@ impl BackgroundScanner {
|
||||
fn schedule_git_statuses_update(
|
||||
&self,
|
||||
state: &mut BackgroundScannerState,
|
||||
local_repository: LocalRepositoryEntry,
|
||||
mut local_repository: LocalRepositoryEntry,
|
||||
) -> oneshot::Receiver<()> {
|
||||
let repository_name = local_repository.work_directory.display_name();
|
||||
let path_key = local_repository.work_directory.path_key();
|
||||
|
||||
let job_state = self.state.clone();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
state.repository_scans.insert(
|
||||
local_repository.work_directory.path_key(),
|
||||
self.executor
|
||||
.spawn(do_git_status_update(job_state, local_repository, tx)),
|
||||
path_key.clone(),
|
||||
self.executor.spawn(async move {
|
||||
update_branches(&job_state, &mut local_repository)
|
||||
.await
|
||||
.log_err();
|
||||
log::trace!("updating git statuses for repo {repository_name}",);
|
||||
let t0 = Instant::now();
|
||||
|
||||
let Some(statuses) = local_repository
|
||||
.repo()
|
||||
.status(&[git::WORK_DIRECTORY_REPO_PATH.clone()])
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
log::trace!(
|
||||
"computed git statuses for repo {repository_name} in {:?}",
|
||||
t0.elapsed()
|
||||
);
|
||||
|
||||
let t0 = Instant::now();
|
||||
let mut changed_paths = Vec::new();
|
||||
let snapshot = job_state.lock().snapshot.snapshot.clone();
|
||||
|
||||
let Some(mut repository) = snapshot
|
||||
.repository(path_key)
|
||||
.context(
|
||||
"Tried to update git statuses for a repository that isn't in the snapshot",
|
||||
)
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let merge_head_shas = local_repository.repo().merge_head_shas();
|
||||
if merge_head_shas != local_repository.current_merge_head_shas {
|
||||
mem::take(&mut repository.current_merge_conflicts);
|
||||
}
|
||||
|
||||
let mut new_entries_by_path = SumTree::new(&());
|
||||
for (repo_path, status) in statuses.entries.iter() {
|
||||
let project_path = repository.work_directory.try_unrelativize(repo_path);
|
||||
|
||||
new_entries_by_path.insert_or_replace(
|
||||
StatusEntry {
|
||||
repo_path: repo_path.clone(),
|
||||
status: *status,
|
||||
},
|
||||
&(),
|
||||
);
|
||||
if status.is_conflicted() {
|
||||
repository.current_merge_conflicts.insert(repo_path.clone());
|
||||
}
|
||||
|
||||
if let Some(path) = project_path {
|
||||
changed_paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
repository.statuses_by_path = new_entries_by_path;
|
||||
let mut state = job_state.lock();
|
||||
state
|
||||
.snapshot
|
||||
.repositories
|
||||
.insert_or_replace(repository, &());
|
||||
state.snapshot.git_repositories.update(
|
||||
&local_repository.work_directory_id,
|
||||
|entry| {
|
||||
entry.current_merge_head_shas = merge_head_shas;
|
||||
entry.merge_message = std::fs::read_to_string(
|
||||
local_repository.dot_git_dir_abs_path.join("MERGE_MSG"),
|
||||
)
|
||||
.ok()
|
||||
.and_then(|merge_msg| Some(merge_msg.lines().next()?.to_owned()));
|
||||
entry.status_scan_id += 1;
|
||||
},
|
||||
);
|
||||
|
||||
util::extend_sorted(
|
||||
&mut state.changed_paths,
|
||||
changed_paths,
|
||||
usize::MAX,
|
||||
Ord::cmp,
|
||||
);
|
||||
|
||||
log::trace!(
|
||||
"applied git status updates for repo {repository_name} in {:?}",
|
||||
t0.elapsed(),
|
||||
);
|
||||
tx.send(()).ok();
|
||||
}),
|
||||
);
|
||||
rx
|
||||
}
|
||||
@@ -5550,15 +5643,6 @@ impl BackgroundScanner {
|
||||
}
|
||||
}
|
||||
|
||||
fn inc_scans_running(scans_running: &AtomicI32) {
|
||||
scans_running.fetch_add(1, atomic::Ordering::Release);
|
||||
}
|
||||
|
||||
fn dec_scans_running(scans_running: &AtomicI32, by: i32) {
|
||||
let old = scans_running.fetch_sub(by, atomic::Ordering::Release);
|
||||
debug_assert!(old >= by);
|
||||
}
|
||||
|
||||
fn send_status_update_inner(
|
||||
phase: BackgroundScannerPhase,
|
||||
state: Arc<Mutex<BackgroundScannerState>>,
|
||||
@@ -5606,100 +5690,6 @@ async fn update_branches(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn do_git_status_update(
|
||||
job_state: Arc<Mutex<BackgroundScannerState>>,
|
||||
mut local_repository: LocalRepositoryEntry,
|
||||
tx: oneshot::Sender<()>,
|
||||
) {
|
||||
let repository_name = local_repository.work_directory.display_name();
|
||||
log::trace!("updating git branches for repo {repository_name}");
|
||||
update_branches(&job_state, &mut local_repository)
|
||||
.await
|
||||
.log_err();
|
||||
let t0 = Instant::now();
|
||||
|
||||
log::trace!("updating git statuses for repo {repository_name}");
|
||||
let Some(statuses) = local_repository
|
||||
.repo()
|
||||
.status(&[git::WORK_DIRECTORY_REPO_PATH.clone()])
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
log::trace!(
|
||||
"computed git statuses for repo {repository_name} in {:?}",
|
||||
t0.elapsed()
|
||||
);
|
||||
|
||||
let t0 = Instant::now();
|
||||
let mut changed_paths = Vec::new();
|
||||
let snapshot = job_state.lock().snapshot.snapshot.clone();
|
||||
|
||||
let Some(mut repository) = snapshot
|
||||
.repository(local_repository.work_directory.path_key())
|
||||
.context("Tried to update git statuses for a repository that isn't in the snapshot")
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let merge_head_shas = local_repository.repo().merge_head_shas();
|
||||
if merge_head_shas != local_repository.current_merge_head_shas {
|
||||
mem::take(&mut repository.current_merge_conflicts);
|
||||
}
|
||||
|
||||
let mut new_entries_by_path = SumTree::new(&());
|
||||
for (repo_path, status) in statuses.entries.iter() {
|
||||
let project_path = repository.work_directory.try_unrelativize(repo_path);
|
||||
|
||||
new_entries_by_path.insert_or_replace(
|
||||
StatusEntry {
|
||||
repo_path: repo_path.clone(),
|
||||
status: *status,
|
||||
},
|
||||
&(),
|
||||
);
|
||||
if status.is_conflicted() {
|
||||
repository.current_merge_conflicts.insert(repo_path.clone());
|
||||
}
|
||||
|
||||
if let Some(path) = project_path {
|
||||
changed_paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
repository.statuses_by_path = new_entries_by_path;
|
||||
let mut state = job_state.lock();
|
||||
state
|
||||
.snapshot
|
||||
.repositories
|
||||
.insert_or_replace(repository, &());
|
||||
state
|
||||
.snapshot
|
||||
.git_repositories
|
||||
.update(&local_repository.work_directory_id, |entry| {
|
||||
entry.current_merge_head_shas = merge_head_shas;
|
||||
entry.merge_message =
|
||||
std::fs::read_to_string(local_repository.dot_git_dir_abs_path.join("MERGE_MSG"))
|
||||
.ok()
|
||||
.and_then(|merge_msg| Some(merge_msg.lines().next()?.to_owned()));
|
||||
entry.status_scan_id += 1;
|
||||
});
|
||||
|
||||
util::extend_sorted(
|
||||
&mut state.changed_paths,
|
||||
changed_paths,
|
||||
usize::MAX,
|
||||
Ord::cmp,
|
||||
);
|
||||
|
||||
log::trace!(
|
||||
"applied git status updates for repo {repository_name} in {:?}",
|
||||
t0.elapsed(),
|
||||
);
|
||||
tx.send(()).ok();
|
||||
}
|
||||
|
||||
fn build_diff(
|
||||
phase: BackgroundScannerPhase,
|
||||
old_snapshot: &Snapshot,
|
||||
|
||||
@@ -2426,10 +2426,11 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: This test always fails on Windows, because on Windows, unlike on Unix,
|
||||
// you can't rename a directory which some program has already open. This is a
|
||||
// limitation of the Windows. See:
|
||||
// https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
|
||||
// NOTE:
|
||||
// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
|
||||
// a directory which some program has already open.
|
||||
// This is a limitation of the Windows.
|
||||
// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)]
|
||||
async fn test_file_status(cx: &mut TestAppContext) {
|
||||
@@ -3574,6 +3575,7 @@ fn git_checkout(name: &str, repo: &git2::Repository) {
|
||||
repo.checkout_head(None).expect("Failed to check out head");
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[track_caller]
|
||||
fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
|
||||
repo.statuses(None)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.178.4"
|
||||
version = "0.179.0"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
stable
|
||||
dev
|
||||
|
||||
@@ -383,14 +383,14 @@ impl Render for QuickActionBar {
|
||||
"Column Git Blame",
|
||||
show_git_blame_gutter,
|
||||
IconPosition::Start,
|
||||
Some(git::Blame.boxed_clone()),
|
||||
Some(editor::actions::ToggleGitBlame.boxed_clone()),
|
||||
{
|
||||
let editor = editor.clone();
|
||||
move |window, cx| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.toggle_git_blame(
|
||||
&git::Blame,
|
||||
&editor::actions::ToggleGitBlame,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -63,6 +63,31 @@ You can install a local build on your machine with:
|
||||
|
||||
This will build zed and the cli in release mode and make them available at `~/.local/bin/zed`, installing .desktop files to `~/.local/share`.
|
||||
|
||||
> **_Note_**: If you encounter linker errors similar to the following:
|
||||
>
|
||||
> ```bash
|
||||
> error: linking with `cc` failed: exit status: 1 ...
|
||||
> = note: /usr/bin/ld: /tmp/rustcISMaod/libaws_lc_sys-79f08eb6d32e546e.rlib(f8e4fd781484bd36-bcm.o): in function `aws_lc_0_25_0_handle_cpu_env':
|
||||
> /aws-lc/crypto/fipsmodule/cpucap/cpu_intel.c:(.text.aws_lc_0_25_0_handle_cpu_env+0x63): undefined reference to `__isoc23_sscanf'
|
||||
> /usr/bin/ld: /tmp/rustcISMaod/libaws_lc_sys-79f08eb6d32e546e.rlib(f8e4fd781484bd36-bcm.o): in function `pkey_rsa_ctrl_str':
|
||||
> /aws-lc/crypto/fipsmodule/evp/p_rsa.c:741:(.text.pkey_rsa_ctrl_str+0x20d): undefined reference to `__isoc23_strtol'
|
||||
> /usr/bin/ld: /aws-lc/crypto/fipsmodule/evp/p_rsa.c:752:(.text.pkey_rsa_ctrl_str+0x258): undefined reference to `__isoc23_strtol'
|
||||
> collect2: error: ld returned 1 exit status
|
||||
> = note: some `extern` functions couldn't be found; some native libraries may need to be installed or have their path specified
|
||||
> = note: use the `-l` flag to specify native libraries to link
|
||||
> = note: use the `cargo:rustc-link-lib` directive to specify the native libraries to link with Cargo (see https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-lib)
|
||||
> error: could not compile `remote_server` (bin "remote_server") due to 1 previous error
|
||||
> ```
|
||||
>
|
||||
> **Cause**:
|
||||
> this is caused by known bugs in aws-lc-rs(doesn't support GCC >= 14): [FIPS fails to build with GCC >= 14](https://github.com/aws/aws-lc-rs/issues/569)
|
||||
> & [GCC-14 - build failure for FIPS module](https://github.com/aws/aws-lc/issues/2010)
|
||||
>
|
||||
> You can refer to [linux: Linker error for remote_server when using script/install-linux](https://github.com/zed-industries/zed/issues/24880) for more information.
|
||||
>
|
||||
> **Workarounds**:
|
||||
> Set the remote server target to `x86_64-unknown-linux-gnu` like so `export REMOTE_SERVER_TARGET=x86_64-unknown-linux-gnu; script/install-linux`
|
||||
|
||||
## Wayland & X11
|
||||
|
||||
Zed supports both X11 and Wayland. By default, we pick whichever we can find at runtime. If you're on Wayland and want to run in X11 mode, use the environment variable `WAYLAND_DISPLAY=''`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# PureScript
|
||||
|
||||
PureScript support is available through the [PureScript extension](https://github.com/zed-industries/zed/tree/main/extensions/purescript).
|
||||
PureScript support is available through the [PureScript extension](https://github.com/zed-extensions/purescript).
|
||||
|
||||
- Tree-sitter: [postsolar/tree-sitter-purescript](https://github.com/postsolar/tree-sitter-purescript)
|
||||
- Language-Server: [nwolverson/purescript-language-server](https://github.com/nwolverson/purescript-language-server)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[Uiua](https://www.uiua.org/) is a general purpose, stack-based, array-oriented programming language with a focus on simplicity, beauty, and tacit code.
|
||||
|
||||
Uiua support is available through the [Uiua extension](https://github.com/zed-industries/zed/tree/main/extensions/uiua).
|
||||
Uiua support is available through the [Uiua extension](https://github.com/zed-extensions/uiua).
|
||||
|
||||
- Tree-sitter: [shnarazk/tree-sitter-uiua](https://github.com/shnarazk/tree-sitter-uiua)
|
||||
- Language Server: [uiua-lang/uiua](https://github.com/uiua-lang/uiua/)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Zig
|
||||
|
||||
Zig support is available through the [Zig extension](https://github.com/zed-industries/zed/tree/main/extensions/zig).
|
||||
Zig support is available through the [Zig extension](https://github.com/zed-extensions/zig).
|
||||
|
||||
- Tree-sitter: [tree-sitter-zig](https://github.com/tree-sitter-grammars/tree-sitter-zig)
|
||||
- Language Server: [zls](https://github.com/zigtools/zls)
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "zed_purescript"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/purescript.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
zed_extension_api = "0.1.0"
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -1,15 +0,0 @@
|
||||
id = "purescript"
|
||||
name = "PureScript"
|
||||
description = "PureScript support."
|
||||
version = "0.1.0"
|
||||
schema_version = 1
|
||||
authors = ["Iván Molina Rebolledo <ivanmolinarebolledo@gmail.com>"]
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
|
||||
[language_servers.purescript-language-server]
|
||||
name = "PureScript Language Server"
|
||||
language = "PureScript"
|
||||
|
||||
[grammars.purescript]
|
||||
repository = "https://github.com/postsolar/tree-sitter-purescript"
|
||||
commit = "0554811a512b9cec08b5a83ce9096eb22da18213"
|
||||
@@ -1,3 +0,0 @@
|
||||
("(" @open ")" @close)
|
||||
("[" @open "]" @close)
|
||||
("{" @open "}" @close)
|
||||
@@ -1,14 +0,0 @@
|
||||
name = "PureScript"
|
||||
grammar = "purescript"
|
||||
path_suffixes = ["purs"]
|
||||
autoclose_before = ",=)}]"
|
||||
line_comments = ["-- "]
|
||||
block_comment = ["{- ", " -}"]
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
{ start = "(", end = ")", close = true, newline = true },
|
||||
{ start = "\"", end = "\"", close = true, newline = false },
|
||||
{ start = "'", end = "'", close = true, newline = false },
|
||||
{ start = "`", end = "`", close = true, newline = false },
|
||||
]
|
||||
@@ -1,144 +0,0 @@
|
||||
;; Copyright 2022 nvim-treesitter
|
||||
;;
|
||||
;; Licensed under the Apache License, Version 2.0 (the "License");
|
||||
;; you may not use this file except in compliance with the License.
|
||||
;; You may obtain a copy of the License at
|
||||
;;
|
||||
;; http://www.apache.org/licenses/LICENSE-2.0
|
||||
;;
|
||||
;; Unless required by applicable law or agreed to in writing, software
|
||||
;; distributed under the License is distributed on an "AS IS" BASIS,
|
||||
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
;; See the License for the specific language governing permissions and
|
||||
;; limitations under the License.
|
||||
|
||||
;; ----------------------------------------------------------------------------
|
||||
;; Literals and comments
|
||||
|
||||
(integer) @number
|
||||
(exp_negation) @number
|
||||
(exp_literal (number)) @float
|
||||
(char) @string
|
||||
[
|
||||
(string)
|
||||
(triple_quote_string)
|
||||
] @string
|
||||
|
||||
(comment) @comment
|
||||
|
||||
|
||||
;; ----------------------------------------------------------------------------
|
||||
;; Punctuation
|
||||
|
||||
[
|
||||
"("
|
||||
")"
|
||||
"{"
|
||||
"}"
|
||||
"["
|
||||
"]"
|
||||
] @punctuation.bracket
|
||||
|
||||
[
|
||||
(comma)
|
||||
";"
|
||||
] @punctuation.delimiter
|
||||
|
||||
|
||||
;; ----------------------------------------------------------------------------
|
||||
;; Keywords, operators, includes
|
||||
|
||||
[
|
||||
"forall"
|
||||
"∀"
|
||||
] @keyword
|
||||
|
||||
;; (pragma) @constant
|
||||
|
||||
[
|
||||
"if"
|
||||
"then"
|
||||
"else"
|
||||
"case"
|
||||
"of"
|
||||
] @keyword
|
||||
|
||||
[
|
||||
"import"
|
||||
"module"
|
||||
] @keyword
|
||||
|
||||
[
|
||||
(operator)
|
||||
(constructor_operator)
|
||||
(type_operator)
|
||||
(qualified_module) ; grabs the `.` (dot), ex: import System.IO
|
||||
(all_names)
|
||||
(wildcard)
|
||||
"="
|
||||
"|"
|
||||
"::"
|
||||
"=>"
|
||||
"->"
|
||||
"<-"
|
||||
"\\"
|
||||
"`"
|
||||
"@"
|
||||
"∷"
|
||||
"⇒"
|
||||
"<="
|
||||
"⇐"
|
||||
"→"
|
||||
"←"
|
||||
] @operator
|
||||
|
||||
(module) @title
|
||||
|
||||
[
|
||||
(where)
|
||||
"let"
|
||||
"in"
|
||||
"class"
|
||||
"instance"
|
||||
"derive"
|
||||
"foreign"
|
||||
"data"
|
||||
"newtype"
|
||||
"type"
|
||||
"as"
|
||||
"hiding"
|
||||
"do"
|
||||
"ado"
|
||||
"infix"
|
||||
"infixl"
|
||||
"infixr"
|
||||
] @keyword
|
||||
|
||||
|
||||
;; ----------------------------------------------------------------------------
|
||||
;; Functions and variables
|
||||
|
||||
(variable) @variable
|
||||
(pat_wildcard) @variable
|
||||
|
||||
(signature name: (variable) @type)
|
||||
(function
|
||||
name: (variable) @function
|
||||
patterns: (patterns))
|
||||
|
||||
|
||||
(exp_infix (exp_name) @function (#set! "priority" 101))
|
||||
(exp_apply . (exp_name (variable) @function))
|
||||
(exp_apply . (exp_name (qualified_variable (variable) @function)))
|
||||
|
||||
|
||||
;; ----------------------------------------------------------------------------
|
||||
;; Types
|
||||
|
||||
(type) @type
|
||||
(type_variable) @type
|
||||
|
||||
(constructor) @constructor
|
||||
|
||||
; True or False
|
||||
((constructor) @_bool (#match? @_bool "(True|False)")) @boolean
|
||||
@@ -1,3 +0,0 @@
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
@@ -1,97 +0,0 @@
|
||||
use std::{env, fs};
|
||||
use zed_extension_api::{self as zed, serde_json, Result};
|
||||
|
||||
const SERVER_PATH: &str = "node_modules/.bin/purescript-language-server";
|
||||
const PACKAGE_NAME: &str = "purescript-language-server";
|
||||
|
||||
struct PurescriptExtension {
|
||||
did_find_server: bool,
|
||||
}
|
||||
|
||||
impl PurescriptExtension {
|
||||
fn server_exists(&self) -> bool {
|
||||
fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file())
|
||||
}
|
||||
|
||||
fn server_script_path(&mut self, language_server_id: &zed::LanguageServerId) -> Result<String> {
|
||||
let server_exists = self.server_exists();
|
||||
if self.did_find_server && server_exists {
|
||||
return Ok(SERVER_PATH.to_string());
|
||||
}
|
||||
|
||||
zed::set_language_server_installation_status(
|
||||
language_server_id,
|
||||
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
|
||||
);
|
||||
let version = zed::npm_package_latest_version(PACKAGE_NAME)?;
|
||||
|
||||
if !server_exists
|
||||
|| zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version)
|
||||
{
|
||||
zed::set_language_server_installation_status(
|
||||
language_server_id,
|
||||
&zed::LanguageServerInstallationStatus::Downloading,
|
||||
);
|
||||
let result = zed::npm_install_package(PACKAGE_NAME, &version);
|
||||
match result {
|
||||
Ok(()) => {
|
||||
if !self.server_exists() {
|
||||
Err(format!(
|
||||
"installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'",
|
||||
))?;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
if !self.server_exists() {
|
||||
Err(error)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.did_find_server = true;
|
||||
Ok(SERVER_PATH.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl zed::Extension for PurescriptExtension {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
did_find_server: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn language_server_command(
|
||||
&mut self,
|
||||
language_server_id: &zed::LanguageServerId,
|
||||
_worktree: &zed::Worktree,
|
||||
) -> Result<zed::Command> {
|
||||
let server_path = self.server_script_path(language_server_id)?;
|
||||
Ok(zed::Command {
|
||||
command: zed::node_binary_path()?,
|
||||
args: vec![
|
||||
env::current_dir()
|
||||
.unwrap()
|
||||
.join(&server_path)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
"--stdio".to_string(),
|
||||
],
|
||||
env: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn language_server_initialization_options(
|
||||
&mut self,
|
||||
_language_server_id: &zed::LanguageServerId,
|
||||
_worktree: &zed::Worktree,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(Some(serde_json::json!({
|
||||
"purescript": {
|
||||
"addSpagoSources": true
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
zed::register_extension!(PurescriptExtension);
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "zed_uiua"
|
||||
version = "0.0.1"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/uiua.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
zed_extension_api = "0.1.0"
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -1,15 +0,0 @@
|
||||
id = "uiua"
|
||||
name = "Uiua"
|
||||
description = "Uiua support."
|
||||
version = "0.0.1"
|
||||
schema_version = 1
|
||||
authors = ["Max Brunsfeld <max@zed.dev>"]
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
|
||||
[language_servers.uiua]
|
||||
name = "Uiua LSP"
|
||||
language = "Uiua"
|
||||
|
||||
[grammars.uiua]
|
||||
repository = "https://github.com/shnarazk/tree-sitter-uiua"
|
||||
commit = "21dc2db39494585bf29a3f86d5add6e9d11a22ba"
|
||||
@@ -1,11 +0,0 @@
|
||||
name = "Uiua"
|
||||
grammar = "uiua"
|
||||
path_suffixes = ["ua"]
|
||||
line_comments = ["# "]
|
||||
autoclose_before = ")]}\""
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = false},
|
||||
{ start = "[", end = "]", close = true, newline = false },
|
||||
{ start = "(", end = ")", close = true, newline = false },
|
||||
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
|
||||
]
|
||||
@@ -1,50 +0,0 @@
|
||||
[
|
||||
(openParen)
|
||||
(closeParen)
|
||||
(openCurly)
|
||||
(closeCurly)
|
||||
(openBracket)
|
||||
(closeBracket)
|
||||
] @punctuation.bracket
|
||||
|
||||
[
|
||||
(branchSeparator)
|
||||
(underscore)
|
||||
] @constructor
|
||||
; ] @punctuation.delimiter
|
||||
|
||||
[ (character) ] @constant.character
|
||||
[ (comment) ] @comment
|
||||
[ (constant) ] @constant.numeric
|
||||
[ (identifier) ] @variable
|
||||
[ (leftArrow) ] @keyword
|
||||
[ (function) ] @function
|
||||
[ (modifier1) ] @operator
|
||||
[ (modifier2) ] @operator
|
||||
[ (number) ] @constant.numeric
|
||||
[ (placeHolder) ] @special
|
||||
[ (otherConstant) ] @string.special
|
||||
[ (signature) ] @type
|
||||
[ (system) ] @function.builtin
|
||||
[ (tripleMinus) ] @module
|
||||
|
||||
; planet
|
||||
[
|
||||
"id"
|
||||
"identity"
|
||||
"∘"
|
||||
"dip"
|
||||
"⊙"
|
||||
"gap"
|
||||
"⋅"
|
||||
] @tag
|
||||
|
||||
[
|
||||
(string)
|
||||
(multiLineString)
|
||||
] @string
|
||||
|
||||
; [
|
||||
; (deprecated)
|
||||
; (identifierDeprecated)
|
||||
; ] @warning
|
||||
@@ -1,3 +0,0 @@
|
||||
[
|
||||
(array)
|
||||
] @indent
|
||||
@@ -1,27 +0,0 @@
|
||||
use zed_extension_api::{self as zed, Result};
|
||||
|
||||
struct UiuaExtension;
|
||||
|
||||
impl zed::Extension for UiuaExtension {
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn language_server_command(
|
||||
&mut self,
|
||||
_language_server_id: &zed::LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<zed::Command> {
|
||||
let path = worktree
|
||||
.which("uiua")
|
||||
.ok_or_else(|| "uiua is not installed".to_string())?;
|
||||
|
||||
Ok(zed::Command {
|
||||
command: path,
|
||||
args: vec!["lsp".to_string()],
|
||||
env: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
zed::register_extension!(UiuaExtension);
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "zed_zig"
|
||||
version = "0.3.3"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/zig.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
zed_extension_api = "0.1.0"
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -1,15 +0,0 @@
|
||||
id = "zig"
|
||||
name = "Zig"
|
||||
description = "Zig support."
|
||||
version = "0.3.3"
|
||||
schema_version = 1
|
||||
authors = ["Allan Calix <contact@acx.dev>"]
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
|
||||
[language_servers.zls]
|
||||
name = "zls"
|
||||
language = "Zig"
|
||||
|
||||
[grammars.zig]
|
||||
repository = "https://github.com/tree-sitter-grammars/tree-sitter-zig"
|
||||
commit = "eb7d58c2dc4fbeea4745019dee8df013034ae66b"
|
||||
@@ -1,3 +0,0 @@
|
||||
("(" @open ")" @close)
|
||||
("[" @open "]" @close)
|
||||
("{" @open "}" @close)
|
||||
@@ -1,12 +0,0 @@
|
||||
name = "Zig"
|
||||
grammar = "zig"
|
||||
path_suffixes = ["zig", "zon"]
|
||||
line_comments = ["// ", "/// ", "//! "]
|
||||
autoclose_before = ";:.,=}])"
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
{ start = "(", end = ")", close = true, newline = true },
|
||||
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
|
||||
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
|
||||
]
|
||||
@@ -1,295 +0,0 @@
|
||||
; Variables
|
||||
|
||||
(identifier) @variable
|
||||
|
||||
; Parameters
|
||||
|
||||
(parameter
|
||||
name: (identifier) @variable.parameter)
|
||||
|
||||
; Types
|
||||
|
||||
(parameter
|
||||
type: (identifier) @type)
|
||||
|
||||
((identifier) @type
|
||||
(#match? @type "^[A-Z_][a-zA-Z0-9_]*"))
|
||||
|
||||
(variable_declaration
|
||||
(identifier) @type
|
||||
"="
|
||||
[
|
||||
(struct_declaration)
|
||||
(enum_declaration)
|
||||
(union_declaration)
|
||||
(opaque_declaration)
|
||||
])
|
||||
|
||||
[
|
||||
(builtin_type)
|
||||
"anyframe"
|
||||
] @type.builtin
|
||||
|
||||
; Constants
|
||||
|
||||
((identifier) @constant
|
||||
(#match? @constant "^[A-Z][A-Z_0-9]+$"))
|
||||
|
||||
[
|
||||
"null"
|
||||
"unreachable"
|
||||
"undefined"
|
||||
] @constant.builtin
|
||||
|
||||
(field_expression
|
||||
.
|
||||
member: (identifier) @constant)
|
||||
|
||||
(enum_declaration
|
||||
(container_field
|
||||
type: (identifier) @constant))
|
||||
|
||||
; Labels
|
||||
|
||||
(block_label (identifier) @label)
|
||||
|
||||
(break_label (identifier) @label)
|
||||
|
||||
; Fields
|
||||
|
||||
(field_initializer
|
||||
.
|
||||
(identifier) @variable.member)
|
||||
|
||||
(field_expression
|
||||
(_)
|
||||
member: (identifier) @property)
|
||||
|
||||
(field_expression
|
||||
(_)
|
||||
member: (identifier) @type (#match? @type "^[A-Z_][a-zA-Z0-9_]*"))
|
||||
|
||||
(container_field
|
||||
name: (identifier) @property)
|
||||
|
||||
(initializer_list
|
||||
(assignment_expression
|
||||
left: (field_expression
|
||||
.
|
||||
member: (identifier) @property)))
|
||||
|
||||
; Functions
|
||||
|
||||
(builtin_identifier) @function.builtin
|
||||
|
||||
(call_expression
|
||||
function: (identifier) @function.call)
|
||||
|
||||
(call_expression
|
||||
function: (field_expression
|
||||
member: (identifier) @function.call))
|
||||
|
||||
(function_declaration
|
||||
name: (identifier) @function)
|
||||
|
||||
; Modules
|
||||
|
||||
(variable_declaration
|
||||
(identifier) @module
|
||||
(builtin_function
|
||||
(builtin_identifier) @keyword.import
|
||||
(#any-of? @keyword.import "@import" "@cImport")))
|
||||
|
||||
; Builtins
|
||||
|
||||
[
|
||||
"c"
|
||||
"..."
|
||||
] @variable.builtin
|
||||
|
||||
((identifier) @variable.builtin
|
||||
(#eq? @variable.builtin "_"))
|
||||
|
||||
(calling_convention
|
||||
(identifier) @variable.builtin)
|
||||
|
||||
; Keywords
|
||||
|
||||
[
|
||||
"asm"
|
||||
"defer"
|
||||
"errdefer"
|
||||
"test"
|
||||
"error"
|
||||
"const"
|
||||
"var"
|
||||
] @keyword
|
||||
|
||||
[
|
||||
"struct"
|
||||
"union"
|
||||
"enum"
|
||||
"opaque"
|
||||
] @keyword.type
|
||||
|
||||
[
|
||||
"async"
|
||||
"await"
|
||||
"suspend"
|
||||
"nosuspend"
|
||||
"resume"
|
||||
] @keyword.coroutine
|
||||
|
||||
"fn" @keyword.function
|
||||
|
||||
[
|
||||
"and"
|
||||
"or"
|
||||
"orelse"
|
||||
] @keyword.operator
|
||||
|
||||
"return" @keyword.return
|
||||
|
||||
[
|
||||
"if"
|
||||
"else"
|
||||
"switch"
|
||||
] @keyword.conditional
|
||||
|
||||
[
|
||||
"for"
|
||||
"while"
|
||||
"break"
|
||||
"continue"
|
||||
] @keyword.repeat
|
||||
|
||||
[
|
||||
"usingnamespace"
|
||||
"export"
|
||||
] @keyword.import
|
||||
|
||||
[
|
||||
"try"
|
||||
"catch"
|
||||
] @keyword.exception
|
||||
|
||||
[
|
||||
"volatile"
|
||||
"allowzero"
|
||||
"noalias"
|
||||
"addrspace"
|
||||
"align"
|
||||
"callconv"
|
||||
"linksection"
|
||||
"pub"
|
||||
"inline"
|
||||
"noinline"
|
||||
"extern"
|
||||
"comptime"
|
||||
"packed"
|
||||
"threadlocal"
|
||||
] @keyword.modifier
|
||||
|
||||
; Operator
|
||||
|
||||
[
|
||||
"="
|
||||
"*="
|
||||
"*%="
|
||||
"*|="
|
||||
"/="
|
||||
"%="
|
||||
"+="
|
||||
"+%="
|
||||
"+|="
|
||||
"-="
|
||||
"-%="
|
||||
"-|="
|
||||
"<<="
|
||||
"<<|="
|
||||
">>="
|
||||
"&="
|
||||
"^="
|
||||
"|="
|
||||
"!"
|
||||
"~"
|
||||
"-"
|
||||
"-%"
|
||||
"&"
|
||||
"=="
|
||||
"!="
|
||||
">"
|
||||
">="
|
||||
"<="
|
||||
"<"
|
||||
"&"
|
||||
"^"
|
||||
"|"
|
||||
"<<"
|
||||
">>"
|
||||
"<<|"
|
||||
"+"
|
||||
"++"
|
||||
"+%"
|
||||
"-%"
|
||||
"+|"
|
||||
"-|"
|
||||
"*"
|
||||
"/"
|
||||
"%"
|
||||
"**"
|
||||
"*%"
|
||||
"*|"
|
||||
"||"
|
||||
".*"
|
||||
".?"
|
||||
"?"
|
||||
".."
|
||||
] @operator
|
||||
|
||||
; Literals
|
||||
|
||||
(character) @string
|
||||
|
||||
([
|
||||
(string)
|
||||
(multiline_string)
|
||||
] @string
|
||||
(#set! "priority" 95))
|
||||
|
||||
(integer) @number
|
||||
|
||||
(float) @number.float
|
||||
|
||||
(boolean) @boolean
|
||||
|
||||
(escape_sequence) @string.escape
|
||||
|
||||
; Punctuation
|
||||
|
||||
[
|
||||
"["
|
||||
"]"
|
||||
"("
|
||||
")"
|
||||
"{"
|
||||
"}"
|
||||
] @punctuation.bracket
|
||||
|
||||
[
|
||||
";"
|
||||
"."
|
||||
","
|
||||
":"
|
||||
"=>"
|
||||
"->"
|
||||
] @punctuation.delimiter
|
||||
|
||||
(payload "|" @punctuation.bracket)
|
||||
|
||||
; Comments
|
||||
|
||||
(comment) @comment
|
||||
|
||||
((comment) @comment.documentation
|
||||
(#match? @comment.documentation "^//(/|!)"))
|
||||
@@ -1,17 +0,0 @@
|
||||
[
|
||||
(block)
|
||||
(switch_expression)
|
||||
(initializer_list)
|
||||
] @indent.begin
|
||||
|
||||
(block
|
||||
"}" @indent.end)
|
||||
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
|
||||
[
|
||||
(comment)
|
||||
(multiline_string)
|
||||
] @indent.ignore
|
||||
@@ -1,10 +0,0 @@
|
||||
((comment) @injection.content
|
||||
(#set! injection.language "comment"))
|
||||
|
||||
; TODO: add when asm is added
|
||||
; (asm_output_item (string) @injection.content
|
||||
; (#set! injection.language "asm"))
|
||||
; (asm_input_item (string) @injection.content
|
||||
; (#set! injection.language "asm"))
|
||||
; (asm_clobbers (string) @injection.content
|
||||
; (#set! injection.language "asm"))
|
||||
@@ -1,50 +0,0 @@
|
||||
(test_declaration
|
||||
"test" @context
|
||||
[
|
||||
(string)
|
||||
(identifier)
|
||||
] @name) @item
|
||||
|
||||
(function_declaration
|
||||
"pub"? @context
|
||||
[
|
||||
"extern"
|
||||
"export"
|
||||
"inline"
|
||||
"noinline"
|
||||
]? @context
|
||||
"fn" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(source_file
|
||||
(variable_declaration
|
||||
"pub"? @context
|
||||
(identifier) @name
|
||||
"=" (_) @context) @item)
|
||||
|
||||
(struct_declaration
|
||||
(variable_declaration
|
||||
"pub"? @context
|
||||
(identifier) @name
|
||||
"=" (_) @context) @item)
|
||||
|
||||
(union_declaration
|
||||
(variable_declaration
|
||||
"pub"? @context
|
||||
(identifier) @name
|
||||
"=" (_) @context) @item)
|
||||
|
||||
(enum_declaration
|
||||
(variable_declaration
|
||||
"pub"? @context
|
||||
(identifier) @name
|
||||
"=" (_) @context) @item)
|
||||
|
||||
(opaque_declaration
|
||||
(variable_declaration
|
||||
"pub"? @context
|
||||
(identifier) @name
|
||||
"=" (_) @context) @item)
|
||||
|
||||
(container_field
|
||||
. (_) @name) @item
|
||||
@@ -1,27 +0,0 @@
|
||||
(function_declaration
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(test_declaration
|
||||
(block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(variable_declaration
|
||||
(struct_declaration
|
||||
"struct"
|
||||
"{"
|
||||
[(_) ","]* @class.inside
|
||||
"}")) @class.around
|
||||
|
||||
(variable_declaration
|
||||
(enum_declaration
|
||||
"enum"
|
||||
"{"
|
||||
(_)* @class.inside
|
||||
"}")) @class.around
|
||||
|
||||
(comment)+ @comment.around
|
||||
@@ -1,171 +0,0 @@
|
||||
use std::fs;
|
||||
use zed_extension_api::{self as zed, serde_json, settings::LspSettings, LanguageServerId, Result};
|
||||
|
||||
struct ZigExtension {
|
||||
cached_binary_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ZlsBinary {
|
||||
path: String,
|
||||
args: Option<Vec<String>>,
|
||||
environment: Option<Vec<(String, String)>>,
|
||||
}
|
||||
|
||||
impl ZigExtension {
|
||||
fn language_server_binary(
|
||||
&mut self,
|
||||
language_server_id: &LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<ZlsBinary> {
|
||||
let mut args: Option<Vec<String>> = None;
|
||||
|
||||
let (platform, arch) = zed::current_platform();
|
||||
let environment = match platform {
|
||||
zed::Os::Mac | zed::Os::Linux => Some(worktree.shell_env()),
|
||||
zed::Os::Windows => None,
|
||||
};
|
||||
|
||||
if let Ok(lsp_settings) = LspSettings::for_worktree("zls", worktree) {
|
||||
if let Some(binary) = lsp_settings.binary {
|
||||
args = binary.arguments;
|
||||
if let Some(path) = binary.path {
|
||||
return Ok(ZlsBinary {
|
||||
path: path.clone(),
|
||||
args,
|
||||
environment,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = worktree.which("zls") {
|
||||
return Ok(ZlsBinary {
|
||||
path,
|
||||
args,
|
||||
environment,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(path) = &self.cached_binary_path {
|
||||
if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
|
||||
return Ok(ZlsBinary {
|
||||
path: path.clone(),
|
||||
args,
|
||||
environment,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
zed::set_language_server_installation_status(
|
||||
language_server_id,
|
||||
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
|
||||
);
|
||||
|
||||
// Note that in github releases and on zlstools.org the tar.gz asset is not shown
|
||||
// but is available at https://builds.zigtools.org/zls-{os}-{arch}-{version}.tar.gz
|
||||
let release = zed::latest_github_release(
|
||||
"zigtools/zls",
|
||||
zed::GithubReleaseOptions {
|
||||
require_assets: true,
|
||||
pre_release: false,
|
||||
},
|
||||
)?;
|
||||
|
||||
let arch: &str = match arch {
|
||||
zed::Architecture::Aarch64 => "aarch64",
|
||||
zed::Architecture::X86 => "x86",
|
||||
zed::Architecture::X8664 => "x86_64",
|
||||
};
|
||||
|
||||
let os: &str = match platform {
|
||||
zed::Os::Mac => "macos",
|
||||
zed::Os::Linux => "linux",
|
||||
zed::Os::Windows => "windows",
|
||||
};
|
||||
|
||||
let extension: &str = match platform {
|
||||
zed::Os::Mac | zed::Os::Linux => "tar.gz",
|
||||
zed::Os::Windows => "zip",
|
||||
};
|
||||
|
||||
let asset_name: String = format!("zls-{}-{}-{}.{}", os, arch, release.version, extension);
|
||||
let download_url = format!("https://builds.zigtools.org/{}", asset_name);
|
||||
|
||||
let version_dir = format!("zls-{}", release.version);
|
||||
let binary_path = match platform {
|
||||
zed::Os::Mac | zed::Os::Linux => format!("{version_dir}/zls"),
|
||||
zed::Os::Windows => format!("{version_dir}/zls.exe"),
|
||||
};
|
||||
|
||||
if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
|
||||
zed::set_language_server_installation_status(
|
||||
language_server_id,
|
||||
&zed::LanguageServerInstallationStatus::Downloading,
|
||||
);
|
||||
|
||||
zed::download_file(
|
||||
&download_url,
|
||||
&version_dir,
|
||||
match platform {
|
||||
zed::Os::Mac | zed::Os::Linux => zed::DownloadedFileType::GzipTar,
|
||||
zed::Os::Windows => zed::DownloadedFileType::Zip,
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("failed to download file: {e}"))?;
|
||||
|
||||
zed::make_file_executable(&binary_path)?;
|
||||
|
||||
let entries =
|
||||
fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
|
||||
if entry.file_name().to_str() != Some(&version_dir) {
|
||||
fs::remove_dir_all(entry.path()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.cached_binary_path = Some(binary_path.clone());
|
||||
Ok(ZlsBinary {
|
||||
path: binary_path,
|
||||
args,
|
||||
environment,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl zed::Extension for ZigExtension {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cached_binary_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn language_server_command(
|
||||
&mut self,
|
||||
language_server_id: &LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<zed::Command> {
|
||||
let zls_binary = self.language_server_binary(language_server_id, worktree)?;
|
||||
Ok(zed::Command {
|
||||
command: zls_binary.path,
|
||||
args: zls_binary.args.unwrap_or_default(),
|
||||
env: zls_binary.environment.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn language_server_workspace_configuration(
|
||||
&mut self,
|
||||
_language_server_id: &zed::LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
let settings = LspSettings::for_worktree("zls", worktree)
|
||||
.ok()
|
||||
.and_then(|lsp_settings| lsp_settings.settings.clone())
|
||||
.unwrap_or_default();
|
||||
Ok(Some(settings))
|
||||
}
|
||||
}
|
||||
|
||||
zed::register_extension!(ZigExtension);
|
||||
Reference in New Issue
Block a user