Compare commits
52 Commits
monthly-sp
...
ssh-reconn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ba60d1a8d | ||
|
|
06f18bc365 | ||
|
|
f5833010aa | ||
|
|
f86f06c81a | ||
|
|
9b0ee7d30b | ||
|
|
47a764553d | ||
|
|
47380001cc | ||
|
|
98ecb43b2d | ||
|
|
be474a6d6f | ||
|
|
b44bed0115 | ||
|
|
11a82e3347 | ||
|
|
be81e29b0f | ||
|
|
6a463be1ae | ||
|
|
64a6e9cafb | ||
|
|
fa738ee5e1 | ||
|
|
15449cdf30 | ||
|
|
2db9090a2f | ||
|
|
34b8655bf6 | ||
|
|
5b745a82e1 | ||
|
|
c59a75db1d | ||
|
|
b3c93130ec | ||
|
|
73a6c542f3 | ||
|
|
2cd6c19873 | ||
|
|
6f24c1da79 | ||
|
|
5508832ba6 | ||
|
|
35f2f2aac4 | ||
|
|
9e27b6694a | ||
|
|
f5124c21d1 | ||
|
|
ea460014ab | ||
|
|
5168fc27a1 | ||
|
|
d7ff85e2a1 | ||
|
|
b2b6b1e8a1 | ||
|
|
023186b7a0 | ||
|
|
e5ec08e8f8 | ||
|
|
2bcf9fc490 | ||
|
|
2f6c7a2939 | ||
|
|
3c32f01a8f | ||
|
|
2f26a74c83 | ||
|
|
9d61cd5120 | ||
|
|
411f64b374 | ||
|
|
4ae2f93086 | ||
|
|
65fb2782eb | ||
|
|
e6b9a8ef9b | ||
|
|
398d0396b6 | ||
|
|
e9e4c770ca | ||
|
|
4be9da2641 | ||
|
|
c186e99a3d | ||
|
|
4df882c295 | ||
|
|
17f2929b4c | ||
|
|
5ad392035e | ||
|
|
8c910540ed | ||
|
|
455f241c6a |
6
.github/actions/check_style/action.yml
vendored
6
.github/actions/check_style/action.yml
vendored
@@ -7,9 +7,3 @@ runs:
|
||||
- name: cargo fmt
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Find modified migrations
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
export SQUAWK_GITHUB_TOKEN=${{ github.token }}
|
||||
. ./script/squawk
|
||||
|
||||
63
.github/workflows/ci.yml
vendored
63
.github/workflows/ci.yml
vendored
@@ -14,6 +14,7 @@ on:
|
||||
- "**"
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- ".github/workflows/community_*"
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
@@ -26,9 +27,10 @@ env:
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
style:
|
||||
migration_checks:
|
||||
name: Check Postgres and Protobuf migrations, mergability
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
timeout-minutes: 60
|
||||
name: Check formatting and spelling
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
@@ -37,25 +39,16 @@ jobs:
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
clean: false
|
||||
fetch-depth: 0
|
||||
fetch-depth: 0 # fetch full history
|
||||
|
||||
- name: Remove untracked files
|
||||
run: git clean -df
|
||||
|
||||
- name: Check spelling
|
||||
run: script/check-spelling
|
||||
|
||||
- name: Run style checks
|
||||
uses: ./.github/actions/check_style
|
||||
|
||||
- name: Check unused dependencies
|
||||
uses: bnjbvr/cargo-machete@main
|
||||
|
||||
- name: Check licenses are present
|
||||
run: script/check-licenses
|
||||
|
||||
- name: Check license generation
|
||||
run: script/generate-licenses /tmp/zed_licenses_output
|
||||
- name: Find modified migrations
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
export SQUAWK_GITHUB_TOKEN=${{ github.token }}
|
||||
. ./script/squawk
|
||||
|
||||
- name: Ensure fresh merge
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -77,6 +70,24 @@ jobs:
|
||||
input: "crates/proto/proto/"
|
||||
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/"
|
||||
|
||||
style:
|
||||
timeout-minutes: 60
|
||||
name: Check formatting and spelling
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- buildjet-8vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
|
||||
- name: Run style checks
|
||||
uses: ./.github/actions/check_style
|
||||
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.24.6
|
||||
with:
|
||||
config: ./typos.toml
|
||||
|
||||
macos_tests:
|
||||
timeout-minutes: 60
|
||||
name: (macOS) Run Clippy and tests
|
||||
@@ -92,6 +103,14 @@ jobs:
|
||||
- name: cargo clippy
|
||||
run: ./script/clippy
|
||||
|
||||
- name: Check unused dependencies
|
||||
uses: bnjbvr/cargo-machete@main
|
||||
|
||||
- name: Check licenses
|
||||
run: |
|
||||
script/check-licenses
|
||||
script/generate-licenses /tmp/zed_licenses_output
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
@@ -248,20 +267,20 @@ jobs:
|
||||
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
|
||||
- name: Upload app bundle (universal) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg
|
||||
path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
|
||||
- name: Upload app bundle (x86_64) to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg
|
||||
@@ -312,7 +331,7 @@ jobs:
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
|
||||
@@ -359,7 +378,7 @@ jobs:
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
We're working to clean up our issue tracker by closing older issues that might not be relevant anymore. Are you able to reproduce this issue in the latest version of Zed? If so, please let us know by commenting on this issue and we will keep it open; otherwise, we'll close it in 7 days. Feel free to open a new issue if you're seeing this message after the issue has been closed.
|
||||
|
||||
Thanks for your help!
|
||||
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, feel free to ping a Zed team member to reopen this issue or open a new one."
|
||||
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue."
|
||||
# We will increase `days-before-stale` to 365 on or after Jan 24th,
|
||||
# 2024. This date marks one year since migrating issues from
|
||||
# 'community' to 'zed' repository. The migration added activity to all
|
||||
1
.github/workflows/deploy_cloudflare.yml
vendored
1
.github/workflows/deploy_cloudflare.yml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
jobs:
|
||||
deploy-docs:
|
||||
name: Deploy Docs
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
8
.github/workflows/docs.yml
vendored
8
.github/workflows/docs.yml
vendored
@@ -11,6 +11,7 @@ on:
|
||||
jobs:
|
||||
check_formatting:
|
||||
name: "Check formatting"
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -29,5 +30,8 @@ jobs:
|
||||
false
|
||||
}
|
||||
|
||||
- name: Check spelling
|
||||
run: script/check-spelling docs/
|
||||
- name: Check for Typos with Typos-CLI
|
||||
uses: crate-ci/typos@v1.24.6
|
||||
with:
|
||||
config: ./typos.toml
|
||||
files: ./docs/
|
||||
|
||||
23
Cargo.lock
generated
23
Cargo.lock
generated
@@ -412,6 +412,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"picker",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
@@ -8472,6 +8473,7 @@ dependencies = [
|
||||
"terminal",
|
||||
"text",
|
||||
"unindent",
|
||||
"url",
|
||||
"util",
|
||||
"which 6.0.3",
|
||||
"windows 0.58.0",
|
||||
@@ -8973,6 +8975,7 @@ dependencies = [
|
||||
"futures 0.3.30",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"itertools 0.13.0",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
@@ -9116,6 +9119,7 @@ name = "remote"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"collections",
|
||||
"fs",
|
||||
"futures 0.3.30",
|
||||
@@ -9147,6 +9151,8 @@ dependencies = [
|
||||
"env_logger",
|
||||
"fs",
|
||||
"futures 0.3.30",
|
||||
"git",
|
||||
"git_hosting_providers",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"language",
|
||||
@@ -9298,6 +9304,7 @@ dependencies = [
|
||||
"system-configuration 0.6.1",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.0",
|
||||
"tokio-socks",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
"url",
|
||||
@@ -11996,6 +12003,7 @@ dependencies = [
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -14859,13 +14867,6 @@ dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_svelte"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_terraform"
|
||||
version = "0.1.1"
|
||||
@@ -14894,14 +14895,6 @@ dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_vue"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_zig"
|
||||
version = "0.3.1"
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -158,12 +158,10 @@ members = [
|
||||
"extensions/ruff",
|
||||
"extensions/slash-commands-example",
|
||||
"extensions/snippets",
|
||||
"extensions/svelte",
|
||||
"extensions/terraform",
|
||||
"extensions/test-extension",
|
||||
"extensions/toml",
|
||||
"extensions/uiua",
|
||||
"extensions/vue",
|
||||
"extensions/zig",
|
||||
|
||||
#
|
||||
@@ -391,7 +389,14 @@ pulldown-cmark = { version = "0.12.0", default-features = false }
|
||||
rand = "0.8.5"
|
||||
regex = "1.5"
|
||||
repair_json = "0.1.0"
|
||||
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f6998da16bbca97b6dddda9be7827c50e29", default-features = false, features = ["charset", "http2", "macos-system-configuration", "rustls-tls-native-roots", "stream"]}
|
||||
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f6998da16bbca97b6dddda9be7827c50e29", default-features = false, features = [
|
||||
"charset",
|
||||
"http2",
|
||||
"macos-system-configuration",
|
||||
"rustls-tls-native-roots",
|
||||
"socks",
|
||||
"stream",
|
||||
] }
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { version = "0.15", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
|
||||
1
assets/icons/diff.svg
Normal file
1
assets/icons/diff.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-diff"><path d="M12 3v14"/><path d="M5 10h14"/><path d="M5 21h14"/></svg>
|
||||
|
After Width: | Height: | Size: 275 B |
@@ -128,6 +128,7 @@
|
||||
"php": "php",
|
||||
"plist": "template",
|
||||
"png": "image",
|
||||
"postcss": "css",
|
||||
"ppt": "document",
|
||||
"pptx": "document",
|
||||
"prettierignore": "prettier",
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"cmd-]": "pane::GoForward",
|
||||
"alt-f7": "editor::FindAllReferences",
|
||||
"cmd-alt-f7": "editor::FindAllReferences",
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"cmd-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
|
||||
"cmd-alt-b": "editor::GoToDefinitionSplit",
|
||||
"cmd-shift-b": "editor::GoToTypeDefinition",
|
||||
"cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
|
||||
@@ -64,7 +64,8 @@
|
||||
"cmd-shift-o": "file_finder::Toggle",
|
||||
"cmd-shift-a": "command_palette::Toggle",
|
||||
"shift shift": "command_palette::Toggle",
|
||||
"cmd-alt-o": "project_symbols::Toggle",
|
||||
"cmd-alt-o": "project_symbols::Toggle", // JetBrains: Go to Symbol
|
||||
"cmd-o": "project_symbols::Toggle", // JetBrains: Go to Class
|
||||
"cmd-1": "workspace::ToggleLeftDock",
|
||||
"cmd-6": "diagnostics::Deploy"
|
||||
}
|
||||
|
||||
@@ -1,85 +1,33 @@
|
||||
<task_description>
|
||||
|
||||
# Code Change Workflow
|
||||
The user of a code editor wants to make a change to their codebase.
|
||||
You must describe the change using the following XML structure:
|
||||
|
||||
Your task is to guide the user through code changes using a series of steps. Each step should describe a high-level change, which can consist of multiple edits to distinct locations in the codebase.
|
||||
|
||||
## Output Example
|
||||
|
||||
Provide output as XML, with the following format:
|
||||
|
||||
<step>
|
||||
Update the Person struct to store an age
|
||||
|
||||
```rust
|
||||
struct Person {
|
||||
// existing fields...
|
||||
age: u8,
|
||||
height: f32,
|
||||
// existing fields...
|
||||
}
|
||||
|
||||
impl Person {
|
||||
fn age(&self) -> u8 {
|
||||
self.age
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<edit>
|
||||
<path>src/person.rs</path>
|
||||
<operation>insert_before</operation>
|
||||
<search>height: f32,</search>
|
||||
<description>Add the age field</description>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/person.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>impl Person {</search>
|
||||
<description>Add the age getter</description>
|
||||
</edit>
|
||||
</step>
|
||||
|
||||
## Output Format
|
||||
|
||||
First, each `<step>` must contain a written description of the change that should be made. The description should begin with a high-level overview, and can contain markdown code blocks as well. The description should be self-contained and actionable.
|
||||
|
||||
After the description, each `<step>` must contain one or more `<edit>` tags, each of which refer to a specific range in a source file. Each `<edit>` tag must contain the following child tags:
|
||||
|
||||
### `<path>` (required)
|
||||
|
||||
This tag contains the path to the file that will be changed. It can be an existing path, or a path that should be created.
|
||||
|
||||
### `<search>` (optional)
|
||||
|
||||
This tag contains a search string to locate in the source file, e.g. `pub fn baz() {`. If not provided, the new content will be inserted at the top of the file. Make sure to produce a string that exists in the source file and that isn't ambiguous. When there's ambiguity, add more lines to the search to eliminate it.
|
||||
|
||||
### `<description>` (required)
|
||||
|
||||
This tag contains a single-line description of the edit that should be made at the given location.
|
||||
|
||||
### `<operation>` (required)
|
||||
|
||||
This tag indicates what type of change should be made, relative to the given location. It can be one of the following:
|
||||
- `update`: Rewrites the specified string entirely based on the given description.
|
||||
- `create`: Creates a new file with the given path based on the provided description.
|
||||
- `insert_before`: Inserts new text based on the given description before the specified search string.
|
||||
- `insert_after`: Inserts new text based on the given description after the specified search string.
|
||||
- `delete`: Deletes the specified string from the containing file.
|
||||
- <patch> - A group of related code changes.
|
||||
Child tags:
|
||||
- <title> (required) - A high-level description of the changes. This should be as short
|
||||
as possible, possibly using common abbreviations.
|
||||
- <edit> (1 or more) - An edit to make at a particular range within a file.
|
||||
Includes the following child tags:
|
||||
- <path> (required) - The path to the file that will be changed.
|
||||
- <description> (optional) - An arbitrarily-long comment that describes the purpose
|
||||
of this edit.
|
||||
- <old_text> (optional) - An excerpt from the file's current contents that uniquely
|
||||
identifies a range within the file where the edit should occur. If this tag is not
|
||||
specified, then the entire file will be used as the range.
|
||||
- <new_text> (required) - The new text to insert into the file.
|
||||
- <operation> (required) - The type of change that should occur at the given range
|
||||
of the file. Must be one of the following values:
|
||||
- `update`: Replaces the entire range with the new text.
|
||||
- `insert_before`: Inserts the new text before the range.
|
||||
- `insert_after`: Inserts new text after the range.
|
||||
- `create`: Creates a new file with the given path and the new text.
|
||||
- `delete`: Deletes the specified range from the file.
|
||||
|
||||
<guidelines>
|
||||
- There's no need to describe *what* to do, just *where* to do it.
|
||||
- Only reference locations that actually exist (unless you're creating a file).
|
||||
- If creating a file, assume any subsequent updates are included at the time of creation.
|
||||
- Don't create and then update a file. Always create new files in one hot.
|
||||
- Prefer multiple edits to smaller regions, as opposed to one big edit to a larger region.
|
||||
- Don't produce edits that intersect each other. In that case, merge them into a bigger edit.
|
||||
- Never nest an edit with another edit. Never include CDATA. All edits are leaf nodes.
|
||||
- Descriptions are required for all edits except delete.
|
||||
- When generating multiple edits, ensure the descriptions are specific to each individual operation.
|
||||
- Avoid referring to the search string in the description. Focus on the change to be made, not the location where it's made. That's implicit with the `search` string you provide.
|
||||
- Don't generate multiple edits at the same location. Instead, combine them together in a single edit with a succinct combined description.
|
||||
- Never provide multiple edits whose ranges intersect each other. Instead, merge them into one edit.
|
||||
- Prefer multiple edits to smaller, disjoint ranges, rather than one edit to a larger range.
|
||||
- There's no need to escape angle brackets within XML tags.
|
||||
- Always ensure imports are added if you're referencing symbols that are not in scope.
|
||||
</guidelines>
|
||||
|
||||
@@ -124,189 +72,137 @@ Update all shapes to store their origin as an (x, y) tuple and implement Display
|
||||
<message role="assistant">
|
||||
We'll need to update both the rectangle and circle modules.
|
||||
|
||||
<step>
|
||||
Add origin fields to both shape types.
|
||||
|
||||
```rust
|
||||
struct Rectangle {
|
||||
// existing fields ...
|
||||
origin: (f64, f64),
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
struct Circle {
|
||||
// existing fields ...
|
||||
origin: (f64, f64),
|
||||
}
|
||||
```
|
||||
<patch>
|
||||
<title>Add origins and display impls to shapes</title>
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<description>Add the origin field to Rectangle struct</description>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>
|
||||
pub struct Rectangle {
|
||||
</old_text>
|
||||
<new_text>
|
||||
origin: (f64, f64),
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<operation>insert_before</operation>
|
||||
<search>
|
||||
width: f64,
|
||||
height: f64,
|
||||
</search>
|
||||
<description>Add the origin field to Rectangle</description>
|
||||
<description>Update the Rectangle's new function to take an origin parameter</description>
|
||||
<operation>update</operation>
|
||||
<old_text>
|
||||
fn new(width: f64, height: f64) -> Self {
|
||||
Rectangle { width, height }
|
||||
}
|
||||
</old_text>
|
||||
<new_text>
|
||||
fn new(origin: (f64, f64), width: f64, height: f64) -> Self {
|
||||
Rectangle { origin, width, height }
|
||||
}
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<operation>insert_before</operation>
|
||||
<search>
|
||||
<description>Add the origin field to Circle struct</description>
|
||||
<operation>insert_after</operation>
|
||||
<old_text>
|
||||
pub struct Circle {
|
||||
radius: f64,
|
||||
</search>
|
||||
<description>Add the origin field to Circle</description>
|
||||
</edit>
|
||||
|
||||
<step>
|
||||
Update both shape's constructors to take an origin.
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<operation>update</operation>
|
||||
<search>
|
||||
fn new(width: f64, height: f64) -> Self {
|
||||
Rectangle { width, height }
|
||||
}
|
||||
</search>
|
||||
<description>Update the Rectangle new function to take an origin</description>
|
||||
</old_text>
|
||||
<new_text>
|
||||
origin: (f64, f64),
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<description>Update the Circle's new function to take an origin parameter</description>
|
||||
<operation>update</operation>
|
||||
<search>
|
||||
fn new(radius: f64) -> Self {
|
||||
Circle { radius }
|
||||
}
|
||||
</search>
|
||||
<description>Update the Circle new function to take an origin</description>
|
||||
<old_text>
|
||||
fn new(radius: f64) -> Self {
|
||||
Circle { radius }
|
||||
}
|
||||
</old_text>
|
||||
<new_text>
|
||||
fn new(origin: (f64, f64), radius: f64) -> Self {
|
||||
Circle { origin, radius }
|
||||
}
|
||||
</new_text>
|
||||
</edit>
|
||||
</step>
|
||||
|
||||
<step>
|
||||
Implement Display for both shapes
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<description>Add an import for the std::fmt module</description>
|
||||
<operation>insert_before</operation>
|
||||
<search>
|
||||
<old_text>
|
||||
struct Rectangle {
|
||||
</search>
|
||||
<description>Add an import for the `std::fmt` module</description>
|
||||
</old_text>
|
||||
<new_text>
|
||||
use std::fmt;
|
||||
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/rectangle.rs</path>
|
||||
<description>Add a Display implementation for Rectangle</description>
|
||||
<operation>insert_after</operation>
|
||||
<search>
|
||||
<old_text>
|
||||
Rectangle { width, height }
|
||||
}
|
||||
}
|
||||
</search>
|
||||
<description>Add a Display implementation for Rectangle</description>
|
||||
</old_text>
|
||||
<new_text>
|
||||
impl fmt::Display for Rectangle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.format_struct(f, "Rectangle")
|
||||
.field("origin", &self.origin)
|
||||
.field("width", &self.width)
|
||||
.field("height", &self.height)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<operation>insert_before</operation>
|
||||
<search>
|
||||
struct Circle {
|
||||
</search>
|
||||
<description>Add an import for the `std::fmt` module</description>
|
||||
<operation>insert_before</operation>
|
||||
<old_text>
|
||||
struct Circle {
|
||||
</old_text>
|
||||
<new_text>
|
||||
use std::fmt;
|
||||
</new_text>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/shapes/circle.rs</path>
|
||||
<description>Add a Display implementation for Circle</description>
|
||||
<operation>insert_after</operation>
|
||||
<search>
|
||||
<old_text>
|
||||
Circle { radius }
|
||||
}
|
||||
}
|
||||
</search>
|
||||
<description>Add a Display implementation for Circle</description>
|
||||
</old_text>
|
||||
<new_text>
|
||||
impl fmt::Display for Rectangle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.format_struct(f, "Rectangle")
|
||||
.field("origin", &self.origin)
|
||||
.field("width", &self.width)
|
||||
.field("height", &self.height)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
</new_text>
|
||||
</edit>
|
||||
</step>
|
||||
</patch>
|
||||
|
||||
</message>
|
||||
</example>
|
||||
|
||||
<example number=2>
|
||||
<message role="user">
|
||||
|
||||
```rs src/user.rs
|
||||
struct User {
|
||||
pub name: String,
|
||||
age: u32,
|
||||
email: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
fn new(name: String, age: u32, email: String) -> Self {
|
||||
User { name, age, email }
|
||||
}
|
||||
|
||||
pub fn print_info(&self) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Let's print all the user information and delete the email field.
|
||||
</message>
|
||||
|
||||
<message role="assistant">
|
||||
<step>
|
||||
Update the 'print_info' method to print user information
|
||||
|
||||
```rust
|
||||
impl User {
|
||||
// ... other methods ...
|
||||
|
||||
pub fn print_info(&self) {
|
||||
println!("Name: {name}, Age: {age}", name = self.name, age = self.age);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<edit>
|
||||
<path>src/user.rs</path>
|
||||
<operation>update</operation>
|
||||
<search>
|
||||
pub fn print_info(&self) {
|
||||
todo!()
|
||||
}
|
||||
</search>
|
||||
<description>Print all the user information</description>
|
||||
</edit>
|
||||
</step>
|
||||
|
||||
<step>
|
||||
Remove the 'email' field from the User struct
|
||||
|
||||
<edit>
|
||||
<path>src/user.rs</path>
|
||||
<operation>delete</operation>
|
||||
<search>
|
||||
email: String,
|
||||
</search>
|
||||
</edit>
|
||||
|
||||
<edit>
|
||||
<path>src/user.rs</path>
|
||||
<operation>update</operation>
|
||||
<symbol>
|
||||
fn new(name: String, age: u32, email: String) -> Self {
|
||||
User { name, age, email }
|
||||
}
|
||||
</symbol>
|
||||
<description>Remove email parameter from new method</description>
|
||||
</edit>
|
||||
</step>
|
||||
</message>
|
||||
</example>
|
||||
|
||||
You should think step by step. When possible, produce smaller, coherent logical steps as opposed to one big step that combines lots of heterogeneous edits.
|
||||
|
||||
</task_description>
|
||||
|
||||
@@ -1,496 +0,0 @@
|
||||
<overview>
|
||||
Your task is to map a step from a workflow to locations in source code where code needs to be changed to fulfill that step.
|
||||
Given a workflow containing background context plus a series of <step> tags, you will resolve *one* of these step tags to resolve to one or more locations in the code.
|
||||
With each location, you will produce a brief, one-line description of the changes to be made.
|
||||
|
||||
<guidelines>
|
||||
- There's no need to describe *what* to do, just *where* to do it.
|
||||
- Only reference locations that actually exist (unless you're creating a file).
|
||||
- If creating a file, assume any subsequent updates are included at the time of creation.
|
||||
- Don't create and then update a file. Always create new files in shot.
|
||||
- Prefer updating symbols lower in the syntax tree if possible.
|
||||
- Never include suggestions on a parent symbol and one of its children in the same suggestions block.
|
||||
- Never nest an operation with another operation or include CDATA or other content. All suggestions are leaf nodes.
|
||||
- Descriptions are required for all suggestions except delete.
|
||||
- When generating multiple suggestions, ensure the descriptions are specific to each individual operation.
|
||||
- Avoid referring to the location in the description. Focus on the change to be made, not the location where it's made. That's implicit with the symbol you provide.
|
||||
- Don't generate multiple suggestions at the same location. Instead, combine them together in a single operation with a succinct combined description.
|
||||
- To add imports respond with a suggestion where the `"symbol"` key is set to `"#imports"`
|
||||
</guidelines>
|
||||
</overview>
|
||||
|
||||
<examples>
|
||||
<example>
|
||||
<workflow_context>
|
||||
<message role="user">
|
||||
```rs src/rectangle.rs
|
||||
struct Rectangle {
|
||||
width: f64,
|
||||
height: f64,
|
||||
}
|
||||
|
||||
impl Rectangle {
|
||||
fn new(width: f64, height: f64) -> Self {
|
||||
Rectangle { width, height }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We need to add methods to calculate the area and perimeter of the rectangle. Can you help with that?
|
||||
</message>
|
||||
<message role="assistant">
|
||||
Sure, I can help with that!
|
||||
|
||||
<step>Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct</step>
|
||||
<step>Implement the 'Display' trait for the Rectangle struct</step>
|
||||
</message>
|
||||
</workflow_context>
|
||||
|
||||
<step_to_resolve>
|
||||
Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct
|
||||
</step_to_resolve>
|
||||
|
||||
<incorrect_output reason="NEVER append multiple children at the same location.">
|
||||
{
|
||||
"title": "Add Rectangle methods",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "AppendChild",
|
||||
"path": "src/shapes.rs",
|
||||
"symbol": "impl Rectangle",
|
||||
"description": "Add calculate_area method"
|
||||
},
|
||||
{
|
||||
"kind": "AppendChild",
|
||||
"path": "src/shapes.rs",
|
||||
"symbol": "impl Rectangle",
|
||||
"description": "Add calculate_perimeter method"
|
||||
}
|
||||
]
|
||||
}
|
||||
</incorrect_output>
|
||||
|
||||
<correct_output>
|
||||
{
|
||||
"title": "Add Rectangle methods",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "AppendChild",
|
||||
"path": "src/shapes.rs",
|
||||
"symbol": "impl Rectangle",
|
||||
"description": "Add calculate area and perimeter methods"
|
||||
}
|
||||
]
|
||||
}
|
||||
</correct_output>
|
||||
|
||||
<step_to_resolve>
|
||||
Implement the 'Display' trait for the Rectangle struct
|
||||
</step_to_resolve>
|
||||
|
||||
<output>
|
||||
{
|
||||
"title": "Implement Display for Rectangle",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "InsertSiblingAfter",
|
||||
"path": "src/shapes.rs",
|
||||
"symbol": "impl Rectangle",
|
||||
"description": "Implement Display trait for Rectangle"
|
||||
}
|
||||
]
|
||||
}
|
||||
</output>
|
||||
|
||||
<example>
|
||||
<workflow_context>
|
||||
<message role="user">
|
||||
```rs src/user.rs
|
||||
struct User {
|
||||
pub name: String,
|
||||
age: u32,
|
||||
email: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
fn new(name: String, age: u32, email: String) -> Self {
|
||||
User { name, age, email }
|
||||
}
|
||||
|
||||
pub fn print_info(&self) {
|
||||
println!("Name: {}, Age: {}, Email: {}", self.name, self.age, self.email);
|
||||
}
|
||||
}
|
||||
```
|
||||
</message>
|
||||
<message role="assistant">
|
||||
Certainly!
|
||||
<step>Update the 'print_info' method to use formatted output</step>
|
||||
<step>Remove the 'email' field from the User struct</step>
|
||||
</message>
|
||||
</workflow_context>
|
||||
|
||||
<step_to_resolve>
|
||||
Update the 'print_info' method to use formatted output
|
||||
</step_to_resolve>
|
||||
|
||||
<output>
|
||||
{
|
||||
"title": "Use formatted output",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "Update",
|
||||
"path": "src/user.rs",
|
||||
"symbol": "impl User pub fn print_info",
|
||||
"description": "Use formatted output"
|
||||
}
|
||||
]
|
||||
}
|
||||
</output>
|
||||
|
||||
<step_to_resolve>
|
||||
Remove the 'email' field from the User struct
|
||||
</step_to_resolve>
|
||||
|
||||
<output>
|
||||
{
|
||||
"title": "Remove email field",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "Delete",
|
||||
"path": "src/user.rs",
|
||||
"symbol": "struct User email"
|
||||
}
|
||||
]
|
||||
}
|
||||
</output>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
<workflow_context>
|
||||
<message role="user">
|
||||
```rs src/vehicle.rs
|
||||
struct Vehicle {
|
||||
make: String,
|
||||
model: String,
|
||||
year: u32,
|
||||
}
|
||||
|
||||
impl Vehicle {
|
||||
fn new(make: String, model: String, year: u32) -> Self {
|
||||
Vehicle { make, model, year }
|
||||
}
|
||||
|
||||
fn print_year(&self) {
|
||||
println!("Year: {}", self.year);
|
||||
}
|
||||
}
|
||||
```
|
||||
</message>
|
||||
<message role="assistant">
|
||||
<step>Add a 'use std::fmt;' statement at the beginning of the file</step>
|
||||
<step>Add a new method 'start_engine' in the Vehicle impl block</step>
|
||||
</message>
|
||||
</workflow_context>
|
||||
|
||||
<step_to_resolve>
|
||||
Add a 'use std::fmt;' statement at the beginning of the file
|
||||
</step_to_resolve>
|
||||
|
||||
<output>
|
||||
{
|
||||
"title": "Add use std::fmt statement",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "PrependChild",
|
||||
"path": "src/vehicle.rs",
|
||||
"symbol": "#imports",
|
||||
"description": "Add 'use std::fmt' statement"
|
||||
}
|
||||
]
|
||||
}
|
||||
</output>
|
||||
|
||||
<step_to_resolve>
|
||||
Add a new method 'start_engine' in the Vehicle impl block
|
||||
</step_to_resolve>
|
||||
|
||||
<output>
|
||||
{
|
||||
"title": "Add start_engine method",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "InsertSiblingAfter",
|
||||
"path": "src/vehicle.rs",
|
||||
"symbol": "impl Vehicle fn new",
|
||||
"description": "Add start_engine method"
|
||||
}
|
||||
]
|
||||
}
|
||||
</output>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
<workflow_context>
|
||||
<message role="user">
|
||||
```rs src/employee.rs
|
||||
struct Employee {
|
||||
name: String,
|
||||
position: String,
|
||||
salary: u32,
|
||||
department: String,
|
||||
}
|
||||
|
||||
impl Employee {
|
||||
fn new(name: String, position: String, salary: u32, department: String) -> Self {
|
||||
Employee { name, position, salary, department }
|
||||
}
|
||||
|
||||
fn print_details(&self) {
|
||||
println!("Name: {}, Position: {}, Salary: {}, Department: {}",
|
||||
self.name, self.position, self.salary, self.department);
|
||||
}
|
||||
|
||||
fn give_raise(&mut self, amount: u32) {
|
||||
self.salary += amount;
|
||||
}
|
||||
}
|
||||
```
|
||||
</message>
|
||||
<message role="assistant">
|
||||
<step>Make salary an f32</step>
|
||||
<step>Remove the 'department' field and update the 'print_details' method</step>
|
||||
</message>
|
||||
</workflow_context>
|
||||
|
||||
<step_to_resolve>
|
||||
Make salary an f32
|
||||
</step_to_resolve>
|
||||
|
||||
<incorrect_output reason="NEVER include suggestions on a parent symbol and one of its children in the same suggestions block.">
|
||||
{
|
||||
"title": "Change salary to f32",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "Update",
|
||||
"path": "src/employee.rs",
|
||||
"symbol": "struct Employee",
|
||||
"description": "Change the type of salary to an f32"
|
||||
},
|
||||
{
|
||||
"kind": "Update",
|
||||
"path": "src/employee.rs",
|
||||
"symbol": "struct Employee salary",
|
||||
"description": "Change the type to an f32"
|
||||
}
|
||||
]
|
||||
}
|
||||
</incorrect_output>
|
||||
|
||||
<correct_output>
|
||||
{
|
||||
"title": "Change salary to f32",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "Update",
|
||||
"path": "src/employee.rs",
|
||||
"symbol": "struct Employee salary",
|
||||
"description": "Change the type to an f32"
|
||||
}
|
||||
]
|
||||
}
|
||||
</correct_output>
|
||||
|
||||
<step_to_resolve>
|
||||
Remove the 'department' field and update the 'print_details' method
|
||||
</step_to_resolve>
|
||||
|
||||
<output>
|
||||
{
|
||||
"title": "Remove department",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "Delete",
|
||||
"path": "src/employee.rs",
|
||||
"symbol": "struct Employee department"
|
||||
},
|
||||
{
|
||||
"kind": "Update",
|
||||
"path": "src/employee.rs",
|
||||
"symbol": "impl Employee fn print_details",
|
||||
"description": "Don't print the 'department' field"
|
||||
}
|
||||
]
|
||||
}
|
||||
</output>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
<workflow_context>
|
||||
<message role="user">
|
||||
```rs src/game.rs
|
||||
struct Player {
|
||||
name: String,
|
||||
health: i32,
|
||||
pub score: u32,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn new(name: String) -> Self {
|
||||
Player { name, health: 100, score: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
struct Game {
|
||||
players: Vec<Player>,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
fn new() -> Self {
|
||||
Game { players: Vec::new() }
|
||||
}
|
||||
}
|
||||
```
|
||||
</message>
|
||||
<message role="assistant">
|
||||
<step>Add a 'level' field to Player and update the 'new' method</step>
|
||||
</message>
|
||||
</workflow_context>
|
||||
|
||||
<step_to_resolve>
|
||||
Add a 'level' field to Player and update the 'new' method
|
||||
</step_to_resolve>
|
||||
|
||||
<output>
|
||||
{
|
||||
"title": "Add level field to Player",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "InsertSiblingAfter",
|
||||
"path": "src/game.rs",
|
||||
"symbol": "struct Player pub score",
|
||||
"description": "Add level field to Player"
|
||||
},
|
||||
{
|
||||
"kind": "Update",
|
||||
"path": "src/game.rs",
|
||||
"symbol": "impl Player pub fn new",
|
||||
"description": "Initialize level in new method"
|
||||
}
|
||||
]
|
||||
}
|
||||
</output>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
<workflow_context>
|
||||
<message role="user">
|
||||
```rs src/config.rs
|
||||
use std::collections::HashMap;
|
||||
|
||||
struct Config {
|
||||
settings: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn new() -> Self {
|
||||
Config { settings: HashMap::new() }
|
||||
}
|
||||
}
|
||||
```
|
||||
</message>
|
||||
<message role="assistant">
|
||||
<step>Add a 'load_from_file' method to Config and import necessary modules</step>
|
||||
</message>
|
||||
</workflow_context>
|
||||
|
||||
<step_to_resolve>
|
||||
Add a 'load_from_file' method to Config and import necessary modules
|
||||
</step_to_resolve>
|
||||
|
||||
<output>
|
||||
{
|
||||
"title": "Add load_from_file method",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "PrependChild",
|
||||
"path": "src/config.rs",
|
||||
"symbol": "#imports",
|
||||
"description": "Import std::fs and std::io modules"
|
||||
},
|
||||
{
|
||||
"kind": "AppendChild",
|
||||
"path": "src/config.rs",
|
||||
"symbol": "impl Config",
|
||||
"description": "Add load_from_file method"
|
||||
}
|
||||
]
|
||||
}
|
||||
</output>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
<workflow_context>
|
||||
<message role="user">
|
||||
```rs src/database.rs
|
||||
pub(crate) struct Database {
|
||||
connection: Connection,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
fn new(url: &str) -> Result<Self, Error> {
|
||||
let connection = Connection::connect(url)?;
|
||||
Ok(Database { connection })
|
||||
}
|
||||
|
||||
async fn query(&self, sql: &str) -> Result<Vec<Row>, Error> {
|
||||
self.connection.query(sql, &[])
|
||||
}
|
||||
}
|
||||
```
|
||||
</message>
|
||||
<message role="assistant">
|
||||
<step>Add error handling to the 'query' method and create a custom error type</step>
|
||||
</message>
|
||||
</workflow_context>
|
||||
|
||||
<step_to_resolve>
|
||||
Add error handling to the 'query' method and create a custom error type
|
||||
</step_to_resolve>
|
||||
|
||||
<output>
|
||||
{
|
||||
"title": "Add error handling to query",
|
||||
"suggestions": [
|
||||
{
|
||||
"kind": "PrependChild",
|
||||
"path": "src/database.rs",
|
||||
"description": "Import necessary error handling modules"
|
||||
},
|
||||
{
|
||||
"kind": "InsertSiblingBefore",
|
||||
"path": "src/database.rs",
|
||||
"symbol": "pub(crate) struct Database",
|
||||
"description": "Define custom DatabaseError enum"
|
||||
},
|
||||
{
|
||||
"kind": "Update",
|
||||
"path": "src/database.rs",
|
||||
"symbol": "impl Database async fn query",
|
||||
"description": "Implement error handling in query method"
|
||||
}
|
||||
]
|
||||
}
|
||||
</output>
|
||||
</example>
|
||||
</examples>
|
||||
|
||||
Now generate the suggestions for the following step:
|
||||
|
||||
<workflow_context>
|
||||
{{{workflow_context}}}
|
||||
</workflow_context>
|
||||
|
||||
<step_to_resolve>
|
||||
{{{step_to_resolve}}}
|
||||
</step_to_resolve>
|
||||
@@ -712,10 +712,10 @@
|
||||
// May take 2 values:
|
||||
// 1. Rely on default platform handling of option key, on macOS
|
||||
// this means generating certain unicode characters
|
||||
// "option_to_meta": false,
|
||||
// "option_as_meta": false,
|
||||
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
|
||||
// "option_to_meta": true,
|
||||
"option_as_meta": true,
|
||||
// "option_as_meta": true,
|
||||
"option_as_meta": false,
|
||||
// Whether or not selecting text in the terminal will automatically
|
||||
// copy to the system clipboard.
|
||||
"copy_on_select": false,
|
||||
|
||||
@@ -97,6 +97,7 @@ language = { workspace = true, features = ["test-support"] }
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
languages = { workspace = true, features = ["test-support"] }
|
||||
log.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
|
||||
@@ -6,6 +6,7 @@ mod context;
|
||||
pub mod context_store;
|
||||
mod inline_assistant;
|
||||
mod model_selector;
|
||||
mod patch;
|
||||
mod prompt_library;
|
||||
mod prompts;
|
||||
mod slash_command;
|
||||
@@ -14,7 +15,6 @@ pub mod slash_command_settings;
|
||||
mod streaming_diff;
|
||||
mod terminal_inline_assistant;
|
||||
mod tools;
|
||||
mod workflow;
|
||||
|
||||
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
|
||||
use assistant_settings::AssistantSettings;
|
||||
@@ -35,11 +35,13 @@ use language_model::{
|
||||
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
|
||||
};
|
||||
pub(crate) use model_selector::*;
|
||||
pub use patch::*;
|
||||
pub use prompts::PromptBuilder;
|
||||
use prompts::PromptLoadingParams;
|
||||
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use slash_command::workflow_command::WorkflowSlashCommand;
|
||||
use slash_command::{
|
||||
auto_command, cargo_workspace_command, context_server_command, default_command, delta_command,
|
||||
diagnostics_command, docs_command, fetch_command, file_command, now_command, project_command,
|
||||
@@ -50,7 +52,6 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
pub(crate) use streaming_diff::*;
|
||||
use util::ResultExt;
|
||||
pub use workflow::*;
|
||||
|
||||
use crate::slash_command_settings::SlashCommandSettings;
|
||||
|
||||
@@ -393,12 +394,25 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
|
||||
slash_command_registry.register_command(now_command::NowSlashCommand, false);
|
||||
slash_command_registry.register_command(diagnostics_command::DiagnosticsSlashCommand, true);
|
||||
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
||||
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
||||
|
||||
if let Some(prompt_builder) = prompt_builder {
|
||||
slash_command_registry.register_command(
|
||||
workflow_command::WorkflowSlashCommand::new(prompt_builder.clone()),
|
||||
true,
|
||||
);
|
||||
cx.observe_global::<SettingsStore>({
|
||||
let slash_command_registry = slash_command_registry.clone();
|
||||
let prompt_builder = prompt_builder.clone();
|
||||
move |cx| {
|
||||
if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) {
|
||||
slash_command_registry.register_command(
|
||||
workflow_command::WorkflowSlashCommand::new(prompt_builder.clone()),
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
slash_command_registry.unregister_command_by_name(WorkflowSlashCommand::NAME);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe_flag::<project_command::ProjectSlashCommandFeatureFlag, _>({
|
||||
let slash_command_registry = slash_command_registry.clone();
|
||||
move |is_enabled, _cx| {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use ::open_ai::Model as OpenAiModel;
|
||||
use anthropic::Model as AnthropicModel;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, Pixels};
|
||||
use language_model::provider::open_ai;
|
||||
@@ -61,6 +62,13 @@ pub struct AssistantSettings {
|
||||
pub default_model: LanguageModelSelection,
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub using_outdated_settings_version: bool,
|
||||
pub enable_experimental_live_diffs: bool,
|
||||
}
|
||||
|
||||
impl AssistantSettings {
|
||||
pub fn are_live_diffs_enabled(&self, cx: &AppContext) -> bool {
|
||||
cx.is_staff() || self.enable_experimental_live_diffs
|
||||
}
|
||||
}
|
||||
|
||||
/// Assistant panel settings
|
||||
@@ -238,6 +246,7 @@ impl AssistantSettingsContent {
|
||||
}
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
|
||||
},
|
||||
@@ -257,6 +266,7 @@ impl AssistantSettingsContent {
|
||||
.to_string(),
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -373,6 +383,7 @@ impl Default for VersionedAssistantSettingsContent {
|
||||
default_height: None,
|
||||
default_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -403,6 +414,10 @@ pub struct AssistantSettingsContentV2 {
|
||||
default_model: Option<LanguageModelSelection>,
|
||||
/// Additional models with which to generate alternatives when performing inline assists.
|
||||
inline_alternatives: Option<Vec<LanguageModelSelection>>,
|
||||
/// Enable experimental live diffs in the assistant panel.
|
||||
///
|
||||
/// Default: false
|
||||
enable_experimental_live_diffs: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
@@ -525,7 +540,10 @@ impl Settings for AssistantSettings {
|
||||
);
|
||||
merge(&mut settings.default_model, value.default_model);
|
||||
merge(&mut settings.inline_alternatives, value.inline_alternatives);
|
||||
// merge(&mut settings.infer_context, value.infer_context); TODO re-enable this once we ship context inference
|
||||
merge(
|
||||
&mut settings.enable_experimental_live_diffs,
|
||||
value.enable_experimental_live_diffs,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
@@ -584,6 +602,7 @@ mod tests {
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
}),
|
||||
)
|
||||
},
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
mod context_tests;
|
||||
|
||||
use crate::{
|
||||
prompts::PromptBuilder, slash_command::SlashCommandLine, MessageId, MessageStatus,
|
||||
WorkflowStep, WorkflowStepEdit, WorkflowStepResolution, WorkflowSuggestionGroup,
|
||||
prompts::PromptBuilder, slash_command::SlashCommandLine, AssistantEdit, AssistantPatch,
|
||||
AssistantPatchStatus, MessageId, MessageStatus,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_slash_command::{
|
||||
@@ -15,13 +15,10 @@ use clock::ReplicaId;
|
||||
use collections::{HashMap, HashSet};
|
||||
use feature_flags::{FeatureFlag, FeatureFlagAppExt};
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::{
|
||||
future::{self, Shared},
|
||||
FutureExt, StreamExt,
|
||||
};
|
||||
use futures::{future::Shared, FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, RenderImage,
|
||||
SharedString, Subscription, Task,
|
||||
AppContext, Context as _, EventEmitter, Model, ModelContext, RenderImage, SharedString,
|
||||
Subscription, Task,
|
||||
};
|
||||
|
||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||
@@ -38,7 +35,7 @@ use project::Project;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{self, max, Ordering},
|
||||
cmp::{max, Ordering},
|
||||
fmt::Debug,
|
||||
iter, mem,
|
||||
ops::Range,
|
||||
@@ -300,7 +297,7 @@ pub enum ContextEvent {
|
||||
MessagesEdited,
|
||||
SummaryChanged,
|
||||
StreamedCompletion,
|
||||
WorkflowStepsUpdated {
|
||||
PatchesUpdated {
|
||||
removed: Vec<Range<language::Anchor>>,
|
||||
updated: Vec<Range<language::Anchor>>,
|
||||
},
|
||||
@@ -454,13 +451,14 @@ pub struct XmlTag {
|
||||
#[derive(Copy, Clone, Debug, strum::EnumString, PartialEq, Eq, strum::AsRefStr)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum XmlTagKind {
|
||||
Step,
|
||||
Patch,
|
||||
Title,
|
||||
Edit,
|
||||
Path,
|
||||
Search,
|
||||
Within,
|
||||
Operation,
|
||||
Description,
|
||||
OldText,
|
||||
NewText,
|
||||
Operation,
|
||||
}
|
||||
|
||||
pub struct Context {
|
||||
@@ -490,7 +488,7 @@ pub struct Context {
|
||||
_subscriptions: Vec<Subscription>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
workflow_steps: Vec<WorkflowStep>,
|
||||
patches: Vec<AssistantPatch>,
|
||||
xml_tags: Vec<XmlTag>,
|
||||
project: Option<Model<Project>>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
@@ -506,7 +504,7 @@ impl ContextAnnotation for PendingSlashCommand {
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextAnnotation for WorkflowStep {
|
||||
impl ContextAnnotation for AssistantPatch {
|
||||
fn range(&self) -> &Range<language::Anchor> {
|
||||
&self.range
|
||||
}
|
||||
@@ -591,7 +589,7 @@ impl Context {
|
||||
telemetry,
|
||||
project,
|
||||
language_registry,
|
||||
workflow_steps: Vec::new(),
|
||||
patches: Vec::new(),
|
||||
xml_tags: Vec::new(),
|
||||
prompt_builder,
|
||||
};
|
||||
@@ -929,48 +927,49 @@ impl Context {
|
||||
self.summary.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn workflow_step_containing(
|
||||
pub(crate) fn patch_containing(
|
||||
&self,
|
||||
offset: usize,
|
||||
position: Point,
|
||||
cx: &AppContext,
|
||||
) -> Option<&WorkflowStep> {
|
||||
) -> Option<&AssistantPatch> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let index = self
|
||||
.workflow_steps
|
||||
.binary_search_by(|step| {
|
||||
let step_range = step.range.to_offset(&buffer);
|
||||
if offset < step_range.start {
|
||||
Ordering::Greater
|
||||
} else if offset > step_range.end {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
Some(&self.workflow_steps[index])
|
||||
let index = self.patches.binary_search_by(|patch| {
|
||||
let patch_range = patch.range.to_point(&buffer);
|
||||
if position < patch_range.start {
|
||||
Ordering::Greater
|
||||
} else if position > patch_range.end {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
if let Ok(ix) = index {
|
||||
Some(&self.patches[ix])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn workflow_step_ranges(&self) -> impl Iterator<Item = Range<language::Anchor>> + '_ {
|
||||
self.workflow_steps.iter().map(|step| step.range.clone())
|
||||
pub fn patch_ranges(&self) -> impl Iterator<Item = Range<language::Anchor>> + '_ {
|
||||
self.patches.iter().map(|patch| patch.range.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn workflow_step_for_range(
|
||||
pub(crate) fn patch_for_range(
|
||||
&self,
|
||||
range: &Range<language::Anchor>,
|
||||
cx: &AppContext,
|
||||
) -> Option<&WorkflowStep> {
|
||||
) -> Option<&AssistantPatch> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let index = self.workflow_step_index_for_range(range, buffer).ok()?;
|
||||
Some(&self.workflow_steps[index])
|
||||
let index = self.patch_index_for_range(range, buffer).ok()?;
|
||||
Some(&self.patches[index])
|
||||
}
|
||||
|
||||
fn workflow_step_index_for_range(
|
||||
fn patch_index_for_range(
|
||||
&self,
|
||||
tagged_range: &Range<text::Anchor>,
|
||||
buffer: &text::BufferSnapshot,
|
||||
) -> Result<usize, usize> {
|
||||
self.workflow_steps
|
||||
self.patches
|
||||
.binary_search_by(|probe| probe.range.cmp(&tagged_range, buffer))
|
||||
}
|
||||
|
||||
@@ -1018,8 +1017,6 @@ impl Context {
|
||||
language::BufferEvent::Edited => {
|
||||
self.count_remaining_tokens(cx);
|
||||
self.reparse(cx);
|
||||
// Use `inclusive = true` to invalidate a step when an edit occurs
|
||||
// at the start/end of a parsed step.
|
||||
cx.emit(ContextEvent::MessagesEdited);
|
||||
}
|
||||
_ => {}
|
||||
@@ -1248,8 +1245,8 @@ impl Context {
|
||||
|
||||
let mut removed_slash_command_ranges = Vec::new();
|
||||
let mut updated_slash_commands = Vec::new();
|
||||
let mut removed_steps = Vec::new();
|
||||
let mut updated_steps = Vec::new();
|
||||
let mut removed_patches = Vec::new();
|
||||
let mut updated_patches = Vec::new();
|
||||
while let Some(mut row_range) = row_ranges.next() {
|
||||
while let Some(next_row_range) = row_ranges.peek() {
|
||||
if row_range.end >= next_row_range.start {
|
||||
@@ -1273,11 +1270,11 @@ impl Context {
|
||||
&mut removed_slash_command_ranges,
|
||||
cx,
|
||||
);
|
||||
self.reparse_workflow_steps_in_range(
|
||||
self.reparse_patches_in_range(
|
||||
start..end,
|
||||
&buffer,
|
||||
&mut updated_steps,
|
||||
&mut removed_steps,
|
||||
&mut updated_patches,
|
||||
&mut removed_patches,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -1289,10 +1286,10 @@ impl Context {
|
||||
});
|
||||
}
|
||||
|
||||
if !updated_steps.is_empty() || !removed_steps.is_empty() {
|
||||
cx.emit(ContextEvent::WorkflowStepsUpdated {
|
||||
removed: removed_steps,
|
||||
updated: updated_steps,
|
||||
if !updated_patches.is_empty() || !removed_patches.is_empty() {
|
||||
cx.emit(ContextEvent::PatchesUpdated {
|
||||
removed: removed_patches,
|
||||
updated: updated_patches,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1354,7 +1351,7 @@ impl Context {
|
||||
removed.extend(removed_commands.map(|command| command.source_range));
|
||||
}
|
||||
|
||||
fn reparse_workflow_steps_in_range(
|
||||
fn reparse_patches_in_range(
|
||||
&mut self,
|
||||
range: Range<text::Anchor>,
|
||||
buffer: &BufferSnapshot,
|
||||
@@ -1369,41 +1366,32 @@ impl Context {
|
||||
self.xml_tags
|
||||
.splice(intersecting_tags_range.clone(), new_tags);
|
||||
|
||||
// Find which steps intersect the changed range.
|
||||
let intersecting_steps_range =
|
||||
self.indices_intersecting_buffer_range(&self.workflow_steps, range.clone(), cx);
|
||||
// Find which patches intersect the changed range.
|
||||
let intersecting_patches_range =
|
||||
self.indices_intersecting_buffer_range(&self.patches, range.clone(), cx);
|
||||
|
||||
// Reparse all tags after the last unchanged step before the change.
|
||||
// Reparse all tags after the last unchanged patch before the change.
|
||||
let mut tags_start_ix = 0;
|
||||
if let Some(preceding_unchanged_step) =
|
||||
self.workflow_steps[..intersecting_steps_range.start].last()
|
||||
if let Some(preceding_unchanged_patch) =
|
||||
self.patches[..intersecting_patches_range.start].last()
|
||||
{
|
||||
tags_start_ix = match self.xml_tags.binary_search_by(|tag| {
|
||||
tag.range
|
||||
.start
|
||||
.cmp(&preceding_unchanged_step.range.end, buffer)
|
||||
.cmp(&preceding_unchanged_patch.range.end, buffer)
|
||||
.then(Ordering::Less)
|
||||
}) {
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
}
|
||||
|
||||
// Rebuild the edit suggestions in the range.
|
||||
let mut new_steps = self.parse_steps(tags_start_ix, range.end, buffer);
|
||||
|
||||
if let Some(project) = self.project() {
|
||||
for step in &mut new_steps {
|
||||
Self::resolve_workflow_step_internal(step, &project, cx);
|
||||
}
|
||||
}
|
||||
|
||||
updated.extend(new_steps.iter().map(|step| step.range.clone()));
|
||||
let removed_steps = self
|
||||
.workflow_steps
|
||||
.splice(intersecting_steps_range, new_steps);
|
||||
// Rebuild the patches in the range.
|
||||
let new_patches = self.parse_patches(tags_start_ix, range.end, buffer, cx);
|
||||
updated.extend(new_patches.iter().map(|patch| patch.range.clone()));
|
||||
let removed_patches = self.patches.splice(intersecting_patches_range, new_patches);
|
||||
removed.extend(
|
||||
removed_steps
|
||||
.map(|step| step.range)
|
||||
removed_patches
|
||||
.map(|patch| patch.range)
|
||||
.filter(|range| !updated.contains(&range)),
|
||||
);
|
||||
}
|
||||
@@ -1464,60 +1452,95 @@ impl Context {
|
||||
tags
|
||||
}
|
||||
|
||||
fn parse_steps(
|
||||
fn parse_patches(
|
||||
&mut self,
|
||||
tags_start_ix: usize,
|
||||
buffer_end: text::Anchor,
|
||||
buffer: &BufferSnapshot,
|
||||
) -> Vec<WorkflowStep> {
|
||||
let mut new_steps = Vec::new();
|
||||
let mut pending_step = None;
|
||||
let mut edit_step_depth = 0;
|
||||
cx: &AppContext,
|
||||
) -> Vec<AssistantPatch> {
|
||||
let mut new_patches = Vec::new();
|
||||
let mut pending_patch = None;
|
||||
let mut patch_tag_depth = 0;
|
||||
let mut tags = self.xml_tags[tags_start_ix..].iter().peekable();
|
||||
'tags: while let Some(tag) = tags.next() {
|
||||
if tag.range.start.cmp(&buffer_end, buffer).is_gt() && edit_step_depth == 0 {
|
||||
if tag.range.start.cmp(&buffer_end, buffer).is_gt() && patch_tag_depth == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if tag.kind == XmlTagKind::Step && tag.is_open_tag {
|
||||
edit_step_depth += 1;
|
||||
let edit_start = tag.range.start;
|
||||
let mut edits = Vec::new();
|
||||
let mut step = WorkflowStep {
|
||||
range: edit_start..edit_start,
|
||||
leading_tags_end: tag.range.end,
|
||||
trailing_tag_start: None,
|
||||
if tag.kind == XmlTagKind::Patch && tag.is_open_tag {
|
||||
patch_tag_depth += 1;
|
||||
let patch_start = tag.range.start;
|
||||
let mut edits = Vec::<Result<AssistantEdit>>::new();
|
||||
let mut patch = AssistantPatch {
|
||||
range: patch_start..patch_start,
|
||||
title: String::new().into(),
|
||||
edits: Default::default(),
|
||||
resolution: None,
|
||||
resolution_task: None,
|
||||
status: crate::AssistantPatchStatus::Pending,
|
||||
};
|
||||
|
||||
while let Some(tag) = tags.next() {
|
||||
step.trailing_tag_start.get_or_insert(tag.range.start);
|
||||
if tag.kind == XmlTagKind::Patch && !tag.is_open_tag {
|
||||
patch_tag_depth -= 1;
|
||||
if patch_tag_depth == 0 {
|
||||
patch.range.end = tag.range.end;
|
||||
|
||||
if tag.kind == XmlTagKind::Step && !tag.is_open_tag {
|
||||
// step.trailing_tag_start = Some(tag.range.start);
|
||||
edit_step_depth -= 1;
|
||||
if edit_step_depth == 0 {
|
||||
step.range.end = tag.range.end;
|
||||
step.edits = edits.into();
|
||||
new_steps.push(step);
|
||||
// Include the line immediately after this <patch> tag if it's empty.
|
||||
let patch_end_offset = patch.range.end.to_offset(buffer);
|
||||
let mut patch_end_chars = buffer.chars_at(patch_end_offset);
|
||||
if patch_end_chars.next() == Some('\n')
|
||||
&& patch_end_chars.next().map_or(true, |ch| ch == '\n')
|
||||
{
|
||||
let messages = self.messages_for_offsets(
|
||||
[patch_end_offset, patch_end_offset + 1],
|
||||
cx,
|
||||
);
|
||||
if messages.len() == 1 {
|
||||
patch.range.end = buffer.anchor_before(patch_end_offset + 1);
|
||||
}
|
||||
}
|
||||
|
||||
edits.sort_unstable_by(|a, b| {
|
||||
if let (Ok(a), Ok(b)) = (a, b) {
|
||||
a.path.cmp(&b.path)
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
patch.edits = edits.into();
|
||||
patch.status = AssistantPatchStatus::Ready;
|
||||
new_patches.push(patch);
|
||||
continue 'tags;
|
||||
}
|
||||
}
|
||||
|
||||
if tag.kind == XmlTagKind::Title && tag.is_open_tag {
|
||||
let content_start = tag.range.end;
|
||||
while let Some(tag) = tags.next() {
|
||||
if tag.kind == XmlTagKind::Title && !tag.is_open_tag {
|
||||
let content_end = tag.range.start;
|
||||
patch.title =
|
||||
trimmed_text_in_range(buffer, content_start..content_end)
|
||||
.into();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tag.kind == XmlTagKind::Edit && tag.is_open_tag {
|
||||
let mut path = None;
|
||||
let mut search = None;
|
||||
let mut old_text = None;
|
||||
let mut new_text = None;
|
||||
let mut operation = None;
|
||||
let mut description = None;
|
||||
|
||||
while let Some(tag) = tags.next() {
|
||||
if tag.kind == XmlTagKind::Edit && !tag.is_open_tag {
|
||||
edits.push(WorkflowStepEdit::new(
|
||||
edits.push(AssistantEdit::new(
|
||||
path,
|
||||
operation,
|
||||
search,
|
||||
old_text,
|
||||
new_text,
|
||||
description,
|
||||
));
|
||||
break;
|
||||
@@ -1526,7 +1549,8 @@ impl Context {
|
||||
if tag.is_open_tag
|
||||
&& [
|
||||
XmlTagKind::Path,
|
||||
XmlTagKind::Search,
|
||||
XmlTagKind::OldText,
|
||||
XmlTagKind::NewText,
|
||||
XmlTagKind::Operation,
|
||||
XmlTagKind::Description,
|
||||
]
|
||||
@@ -1538,15 +1562,18 @@ impl Context {
|
||||
if tag.kind == kind && !tag.is_open_tag {
|
||||
let tag = tags.next().unwrap();
|
||||
let content_end = tag.range.start;
|
||||
let mut content = buffer
|
||||
.text_for_range(content_start..content_end)
|
||||
.collect::<String>();
|
||||
content.truncate(content.trim_end().len());
|
||||
let content = trimmed_text_in_range(
|
||||
buffer,
|
||||
content_start..content_end,
|
||||
);
|
||||
match kind {
|
||||
XmlTagKind::Path => path = Some(content),
|
||||
XmlTagKind::Operation => operation = Some(content),
|
||||
XmlTagKind::Search => {
|
||||
search = Some(content).filter(|s| !s.is_empty())
|
||||
XmlTagKind::OldText => {
|
||||
old_text = Some(content).filter(|s| !s.is_empty())
|
||||
}
|
||||
XmlTagKind::NewText => {
|
||||
new_text = Some(content).filter(|s| !s.is_empty())
|
||||
}
|
||||
XmlTagKind::Description => {
|
||||
description =
|
||||
@@ -1561,162 +1588,28 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
pending_step = Some(step);
|
||||
patch.edits = edits.into();
|
||||
pending_patch = Some(patch);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut pending_step) = pending_step {
|
||||
pending_step.range.end = text::Anchor::MAX;
|
||||
new_steps.push(pending_step);
|
||||
}
|
||||
|
||||
new_steps
|
||||
}
|
||||
|
||||
pub fn resolve_workflow_step(
|
||||
&mut self,
|
||||
tagged_range: Range<text::Anchor>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<()> {
|
||||
let index = self
|
||||
.workflow_step_index_for_range(&tagged_range, self.buffer.read(cx))
|
||||
.ok()?;
|
||||
let step = &mut self.workflow_steps[index];
|
||||
let project = self.project.as_ref()?;
|
||||
step.resolution.take();
|
||||
Self::resolve_workflow_step_internal(step, project, cx);
|
||||
None
|
||||
}
|
||||
|
||||
fn resolve_workflow_step_internal(
|
||||
step: &mut WorkflowStep,
|
||||
project: &Model<Project>,
|
||||
cx: &mut ModelContext<'_, Context>,
|
||||
) {
|
||||
step.resolution_task = Some(cx.spawn({
|
||||
let range = step.range.clone();
|
||||
let edits = step.edits.clone();
|
||||
let project = project.clone();
|
||||
|this, mut cx| async move {
|
||||
let suggestion_groups =
|
||||
Self::compute_step_resolution(project, edits, &mut cx).await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let buffer = this.buffer.read(cx).text_snapshot();
|
||||
let ix = this.workflow_step_index_for_range(&range, &buffer).ok();
|
||||
if let Some(ix) = ix {
|
||||
let step = &mut this.workflow_steps[ix];
|
||||
|
||||
let resolution = suggestion_groups.map(|suggestion_groups| {
|
||||
let mut title = String::new();
|
||||
for mut chunk in buffer.text_for_range(
|
||||
step.leading_tags_end
|
||||
..step.trailing_tag_start.unwrap_or(step.range.end),
|
||||
) {
|
||||
if title.is_empty() {
|
||||
chunk = chunk.trim_start();
|
||||
}
|
||||
if let Some((prefix, _)) = chunk.split_once('\n') {
|
||||
title.push_str(prefix);
|
||||
break;
|
||||
} else {
|
||||
title.push_str(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
WorkflowStepResolution {
|
||||
title,
|
||||
suggestion_groups,
|
||||
}
|
||||
});
|
||||
|
||||
step.resolution = Some(Arc::new(resolution));
|
||||
cx.emit(ContextEvent::WorkflowStepsUpdated {
|
||||
removed: vec![],
|
||||
updated: vec![range],
|
||||
})
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async fn compute_step_resolution(
|
||||
project: Model<Project>,
|
||||
edits: Arc<[Result<WorkflowStepEdit>]>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>> {
|
||||
let mut suggestion_tasks = Vec::new();
|
||||
for edit in edits.iter() {
|
||||
let edit = edit.as_ref().map_err(|e| anyhow!("{e}"))?;
|
||||
suggestion_tasks.push(edit.resolve(project.clone(), cx.clone()));
|
||||
}
|
||||
|
||||
// Expand the context ranges of each suggestion and group suggestions with overlapping context ranges.
|
||||
let suggestions = future::try_join_all(suggestion_tasks).await?;
|
||||
|
||||
let mut suggestions_by_buffer = HashMap::default();
|
||||
for (buffer, suggestion) in suggestions {
|
||||
suggestions_by_buffer
|
||||
.entry(buffer)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(suggestion);
|
||||
}
|
||||
|
||||
let mut suggestion_groups_by_buffer = HashMap::default();
|
||||
for (buffer, mut suggestions) in suggestions_by_buffer {
|
||||
let mut suggestion_groups = Vec::<WorkflowSuggestionGroup>::new();
|
||||
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
|
||||
// Sort suggestions by their range so that earlier, larger ranges come first
|
||||
suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
|
||||
|
||||
// Merge overlapping suggestions
|
||||
suggestions.dedup_by(|a, b| b.try_merge(a, &snapshot));
|
||||
|
||||
// Create context ranges for each suggestion
|
||||
for suggestion in suggestions {
|
||||
let context_range = {
|
||||
let suggestion_point_range = suggestion.range().to_point(&snapshot);
|
||||
let start_row = suggestion_point_range.start.row.saturating_sub(5);
|
||||
let end_row =
|
||||
cmp::min(suggestion_point_range.end.row + 5, snapshot.max_point().row);
|
||||
let start = snapshot.anchor_before(Point::new(start_row, 0));
|
||||
let end =
|
||||
snapshot.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
|
||||
start..end
|
||||
};
|
||||
|
||||
if let Some(last_group) = suggestion_groups.last_mut() {
|
||||
if last_group
|
||||
.context_range
|
||||
.end
|
||||
.cmp(&context_range.start, &snapshot)
|
||||
.is_ge()
|
||||
{
|
||||
// Merge with the previous group if context ranges overlap
|
||||
last_group.context_range.end = context_range.end;
|
||||
last_group.suggestions.push(suggestion);
|
||||
} else {
|
||||
// Create a new group
|
||||
suggestion_groups.push(WorkflowSuggestionGroup {
|
||||
context_range,
|
||||
suggestions: vec![suggestion],
|
||||
});
|
||||
}
|
||||
if let Some(mut pending_patch) = pending_patch {
|
||||
let patch_start = pending_patch.range.start.to_offset(buffer);
|
||||
if let Some(message) = self.message_for_offset(patch_start, cx) {
|
||||
if message.anchor_range.end == text::Anchor::MAX {
|
||||
pending_patch.range.end = text::Anchor::MAX;
|
||||
} else {
|
||||
// Create the first group
|
||||
suggestion_groups.push(WorkflowSuggestionGroup {
|
||||
context_range,
|
||||
suggestions: vec![suggestion],
|
||||
});
|
||||
let message_end = buffer.anchor_after(message.offset_range.end - 1);
|
||||
pending_patch.range.end = message_end;
|
||||
}
|
||||
} else {
|
||||
pending_patch.range.end = text::Anchor::MAX;
|
||||
}
|
||||
|
||||
suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
|
||||
new_patches.push(pending_patch);
|
||||
}
|
||||
|
||||
Ok(suggestion_groups_by_buffer)
|
||||
new_patches
|
||||
}
|
||||
|
||||
pub fn pending_command_for_position(
|
||||
@@ -2315,11 +2208,11 @@ impl Context {
|
||||
let mut updated = Vec::new();
|
||||
let mut removed = Vec::new();
|
||||
for range in ranges {
|
||||
self.reparse_workflow_steps_in_range(range, &buffer, &mut updated, &mut removed, cx);
|
||||
self.reparse_patches_in_range(range, &buffer, &mut updated, &mut removed, cx);
|
||||
}
|
||||
|
||||
if !updated.is_empty() || !removed.is_empty() {
|
||||
cx.emit(ContextEvent::WorkflowStepsUpdated { removed, updated })
|
||||
cx.emit(ContextEvent::PatchesUpdated { removed, updated })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2825,6 +2718,24 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
fn trimmed_text_in_range(buffer: &BufferSnapshot, range: Range<text::Anchor>) -> String {
|
||||
let mut is_start = true;
|
||||
let mut content = buffer
|
||||
.text_for_range(range)
|
||||
.map(|mut chunk| {
|
||||
if is_start {
|
||||
chunk = chunk.trim_start_matches('\n');
|
||||
if !chunk.is_empty() {
|
||||
is_start = false;
|
||||
}
|
||||
}
|
||||
chunk
|
||||
})
|
||||
.collect::<String>();
|
||||
content.truncate(content.trim_end().len());
|
||||
content
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ContextVersion {
|
||||
context: clock::Global,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use super::{MessageCacheMetadata, WorkflowStepEdit};
|
||||
use super::{AssistantEdit, MessageCacheMetadata};
|
||||
use crate::{
|
||||
assistant_panel, prompt_library, slash_command::file_command, CacheStatus, Context,
|
||||
ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
|
||||
WorkflowStepEditKind,
|
||||
assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus,
|
||||
Context, ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{
|
||||
@@ -15,6 +14,7 @@ use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
|
||||
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
||||
use parking_lot::Mutex;
|
||||
use pretty_assertions::assert_eq;
|
||||
use project::Project;
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
@@ -478,7 +478,15 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
cx.update(prompt_library::init);
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
let mut settings_store = cx.update(SettingsStore::test);
|
||||
cx.update(|cx| {
|
||||
settings_store
|
||||
.set_user_settings(
|
||||
r#"{ "assistant": { "enable_experimental_live_diffs": true } }"#,
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
cx.set_global(settings_store);
|
||||
cx.update(language::init);
|
||||
cx.update(Project::init_settings);
|
||||
@@ -520,7 +528,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
»",
|
||||
cx,
|
||||
);
|
||||
expect_steps(
|
||||
expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
@@ -539,17 +547,17 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
one
|
||||
two
|
||||
«
|
||||
<step»",
|
||||
<patch»",
|
||||
cx,
|
||||
);
|
||||
expect_steps(
|
||||
expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
<step",
|
||||
<patch",
|
||||
&[],
|
||||
cx,
|
||||
);
|
||||
@@ -563,36 +571,24 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
one
|
||||
two
|
||||
|
||||
<step«>
|
||||
Add a second function
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
|
||||
<patch«>
|
||||
<edit>»",
|
||||
cx,
|
||||
);
|
||||
expect_steps(
|
||||
expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
«<step>
|
||||
Add a second function
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
|
||||
«<patch>
|
||||
<edit>»",
|
||||
&[&[]],
|
||||
cx,
|
||||
);
|
||||
|
||||
// The full suggestion is added
|
||||
// The full patch is added
|
||||
edit(
|
||||
&context,
|
||||
"
|
||||
@@ -600,51 +596,46 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
one
|
||||
two
|
||||
|
||||
<step>
|
||||
Add a second function
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
|
||||
<patch>
|
||||
<edit>«
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>fn one</search>
|
||||
<description>add a `two` function</description>
|
||||
<old_text>fn one</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>
|
||||
</step>
|
||||
</patch>
|
||||
|
||||
also,»",
|
||||
cx,
|
||||
);
|
||||
expect_steps(
|
||||
expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
«<step>
|
||||
Add a second function
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
|
||||
«<patch>
|
||||
<edit>
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>fn one</search>
|
||||
<description>add a `two` function</description>
|
||||
<old_text>fn one</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>
|
||||
</step>»
|
||||
|
||||
</patch>
|
||||
»
|
||||
also,",
|
||||
&[&[WorkflowStepEdit {
|
||||
&[&[AssistantEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: WorkflowStepEditKind::InsertAfter {
|
||||
search: "fn one".into(),
|
||||
kind: AssistantEditKind::InsertAfter {
|
||||
old_text: "fn one".into(),
|
||||
new_text: "fn two() {}".into(),
|
||||
description: "add a `two` function".into(),
|
||||
},
|
||||
}]],
|
||||
@@ -659,51 +650,46 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
one
|
||||
two
|
||||
|
||||
<step>
|
||||
Add a second function
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
|
||||
<patch>
|
||||
<edit>
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>«fn zero»</search>
|
||||
<description>add a `two` function</description>
|
||||
<old_text>«fn zero»</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>
|
||||
</step>
|
||||
</patch>
|
||||
|
||||
also,",
|
||||
cx,
|
||||
);
|
||||
expect_steps(
|
||||
expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
«<step>
|
||||
Add a second function
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
|
||||
«<patch>
|
||||
<edit>
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>fn zero</search>
|
||||
<description>add a `two` function</description>
|
||||
<old_text>fn zero</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>
|
||||
</step>»
|
||||
|
||||
</patch>
|
||||
»
|
||||
also,",
|
||||
&[&[WorkflowStepEdit {
|
||||
&[&[AssistantEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: WorkflowStepEditKind::InsertAfter {
|
||||
search: "fn zero".into(),
|
||||
kind: AssistantEditKind::InsertAfter {
|
||||
old_text: "fn zero".into(),
|
||||
new_text: "fn two() {}".into(),
|
||||
description: "add a `two` function".into(),
|
||||
},
|
||||
}]],
|
||||
@@ -715,27 +701,24 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||
});
|
||||
expect_steps(
|
||||
expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
<step>
|
||||
Add a second function
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
|
||||
<patch>
|
||||
<edit>
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>fn zero</search>
|
||||
<description>add a `two` function</description>
|
||||
<old_text>fn zero</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>
|
||||
</step>
|
||||
</patch>
|
||||
|
||||
also,",
|
||||
&[],
|
||||
@@ -746,33 +729,31 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
context.update(cx, |context, cx| {
|
||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||
});
|
||||
expect_steps(
|
||||
expect_patches(
|
||||
&context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
«<step>
|
||||
Add a second function
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
|
||||
«<patch>
|
||||
<edit>
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>fn zero</search>
|
||||
<description>add a `two` function</description>
|
||||
<old_text>fn zero</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>
|
||||
</step>»
|
||||
|
||||
</patch>
|
||||
»
|
||||
also,",
|
||||
&[&[WorkflowStepEdit {
|
||||
&[&[AssistantEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: WorkflowStepEditKind::InsertAfter {
|
||||
search: "fn zero".into(),
|
||||
kind: AssistantEditKind::InsertAfter {
|
||||
old_text: "fn zero".into(),
|
||||
new_text: "fn two() {}".into(),
|
||||
description: "add a `two` function".into(),
|
||||
},
|
||||
}]],
|
||||
@@ -792,33 +773,31 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
expect_steps(
|
||||
expect_patches(
|
||||
&deserialized_context,
|
||||
"
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
«<step>
|
||||
Add a second function
|
||||
|
||||
```rust
|
||||
fn two() {}
|
||||
```
|
||||
|
||||
«<patch>
|
||||
<edit>
|
||||
<description>add a `two` function</description>
|
||||
<path>src/lib.rs</path>
|
||||
<operation>insert_after</operation>
|
||||
<search>fn zero</search>
|
||||
<description>add a `two` function</description>
|
||||
<old_text>fn zero</old_text>
|
||||
<new_text>
|
||||
fn two() {}
|
||||
</new_text>
|
||||
</edit>
|
||||
</step>»
|
||||
|
||||
</patch>
|
||||
»
|
||||
also,",
|
||||
&[&[WorkflowStepEdit {
|
||||
&[&[AssistantEdit {
|
||||
path: "src/lib.rs".into(),
|
||||
kind: WorkflowStepEditKind::InsertAfter {
|
||||
search: "fn zero".into(),
|
||||
kind: AssistantEditKind::InsertAfter {
|
||||
old_text: "fn zero".into(),
|
||||
new_text: "fn two() {}".into(),
|
||||
description: "add a `two` function".into(),
|
||||
},
|
||||
}]],
|
||||
@@ -834,48 +813,58 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
cx.executor().run_until_parked();
|
||||
}
|
||||
|
||||
fn expect_steps(
|
||||
#[track_caller]
|
||||
fn expect_patches(
|
||||
context: &Model<Context>,
|
||||
expected_marked_text: &str,
|
||||
expected_suggestions: &[&[WorkflowStepEdit]],
|
||||
expected_suggestions: &[&[AssistantEdit]],
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
context.update(cx, |context, cx| {
|
||||
let expected_marked_text = expected_marked_text.unindent();
|
||||
let (expected_text, expected_ranges) = marked_text_ranges(&expected_marked_text, false);
|
||||
let expected_marked_text = expected_marked_text.unindent();
|
||||
let (expected_text, _) = marked_text_ranges(&expected_marked_text, false);
|
||||
|
||||
let (buffer_text, ranges, patches) = context.update(cx, |context, cx| {
|
||||
context.buffer.read_with(cx, |buffer, _| {
|
||||
assert_eq!(buffer.text(), expected_text);
|
||||
let ranges = context
|
||||
.workflow_steps
|
||||
.patches
|
||||
.iter()
|
||||
.map(|entry| entry.range.to_offset(buffer))
|
||||
.collect::<Vec<_>>();
|
||||
let marked = generate_marked_text(&expected_text, &ranges, false);
|
||||
assert_eq!(
|
||||
marked,
|
||||
expected_marked_text,
|
||||
"unexpected suggestion ranges. actual: {ranges:?}, expected: {expected_ranges:?}"
|
||||
);
|
||||
let suggestions = context
|
||||
.workflow_steps
|
||||
.iter()
|
||||
.map(|step| {
|
||||
step.edits
|
||||
.iter()
|
||||
.map(|edit| {
|
||||
let edit = edit.as_ref().unwrap();
|
||||
WorkflowStepEdit {
|
||||
path: edit.path.clone(),
|
||||
kind: edit.kind.clone(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(suggestions, expected_suggestions);
|
||||
});
|
||||
(
|
||||
buffer.text(),
|
||||
ranges,
|
||||
context
|
||||
.patches
|
||||
.iter()
|
||||
.map(|step| step.edits.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
assert_eq!(buffer_text, expected_text);
|
||||
|
||||
let actual_marked_text = generate_marked_text(&expected_text, &ranges, false);
|
||||
assert_eq!(actual_marked_text, expected_marked_text);
|
||||
|
||||
assert_eq!(
|
||||
patches
|
||||
.iter()
|
||||
.map(|patch| {
|
||||
patch
|
||||
.iter()
|
||||
.map(|edit| {
|
||||
let edit = edit.as_ref().unwrap();
|
||||
AssistantEdit {
|
||||
path: edit.path.clone(),
|
||||
kind: edit.kind.clone(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
expected_suggestions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,13 +82,6 @@ pub struct InlineAssistant {
|
||||
assists: HashMap<InlineAssistId, InlineAssist>,
|
||||
assists_by_editor: HashMap<WeakView<Editor>, EditorInlineAssists>,
|
||||
assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
|
||||
assist_observations: HashMap<
|
||||
InlineAssistId,
|
||||
(
|
||||
async_watch::Sender<AssistStatus>,
|
||||
async_watch::Receiver<AssistStatus>,
|
||||
),
|
||||
>,
|
||||
confirmed_assists: HashMap<InlineAssistId, Model<CodegenAlternative>>,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
@@ -96,19 +89,6 @@ pub struct InlineAssistant {
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
pub enum AssistStatus {
|
||||
Idle,
|
||||
Started,
|
||||
Stopped,
|
||||
Finished,
|
||||
}
|
||||
|
||||
impl AssistStatus {
|
||||
pub fn is_done(&self) -> bool {
|
||||
matches!(self, Self::Stopped | Self::Finished)
|
||||
}
|
||||
}
|
||||
|
||||
impl Global for InlineAssistant {}
|
||||
|
||||
impl InlineAssistant {
|
||||
@@ -123,7 +103,6 @@ impl InlineAssistant {
|
||||
assists: HashMap::default(),
|
||||
assists_by_editor: HashMap::default(),
|
||||
assist_groups: HashMap::default(),
|
||||
assist_observations: HashMap::default(),
|
||||
confirmed_assists: HashMap::default(),
|
||||
prompt_history: VecDeque::default(),
|
||||
prompt_builder,
|
||||
@@ -835,17 +814,6 @@ impl InlineAssistant {
|
||||
.insert(assist_id, confirmed_alternative);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the assist from the status updates map
|
||||
self.assist_observations.remove(&assist_id);
|
||||
}
|
||||
|
||||
pub fn undo_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
|
||||
let Some(codegen) = self.confirmed_assists.remove(&assist_id) else {
|
||||
return false;
|
||||
};
|
||||
codegen.update(cx, |this, cx| this.undo(cx));
|
||||
true
|
||||
}
|
||||
|
||||
fn dismiss_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
|
||||
@@ -1039,10 +1007,6 @@ impl InlineAssistant {
|
||||
codegen.start(user_prompt, assistant_panel_context, cx)
|
||||
})
|
||||
.log_err();
|
||||
|
||||
if let Some((tx, _)) = self.assist_observations.get(&assist_id) {
|
||||
tx.send(AssistStatus::Started).ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
|
||||
@@ -1053,25 +1017,6 @@ impl InlineAssistant {
|
||||
};
|
||||
|
||||
assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
|
||||
|
||||
if let Some((tx, _)) = self.assist_observations.get(&assist_id) {
|
||||
tx.send(AssistStatus::Stopped).ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assist_status(&self, assist_id: InlineAssistId, cx: &AppContext) -> InlineAssistStatus {
|
||||
if let Some(assist) = self.assists.get(&assist_id) {
|
||||
match assist.codegen.read(cx).status(cx) {
|
||||
CodegenStatus::Idle => InlineAssistStatus::Idle,
|
||||
CodegenStatus::Pending => InlineAssistStatus::Pending,
|
||||
CodegenStatus::Done => InlineAssistStatus::Done,
|
||||
CodegenStatus::Error(_) => InlineAssistStatus::Error,
|
||||
}
|
||||
} else if self.confirmed_assists.contains_key(&assist_id) {
|
||||
InlineAssistStatus::Confirmed
|
||||
} else {
|
||||
InlineAssistStatus::Canceled
|
||||
}
|
||||
}
|
||||
|
||||
fn update_editor_highlights(&self, editor: &View<Editor>, cx: &mut WindowContext) {
|
||||
@@ -1257,42 +1202,6 @@ impl InlineAssistant {
|
||||
.collect();
|
||||
})
|
||||
}
|
||||
|
||||
pub fn observe_assist(
|
||||
&mut self,
|
||||
assist_id: InlineAssistId,
|
||||
) -> async_watch::Receiver<AssistStatus> {
|
||||
if let Some((_, rx)) = self.assist_observations.get(&assist_id) {
|
||||
rx.clone()
|
||||
} else {
|
||||
let (tx, rx) = async_watch::channel(AssistStatus::Idle);
|
||||
self.assist_observations.insert(assist_id, (tx, rx.clone()));
|
||||
rx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum InlineAssistStatus {
|
||||
Idle,
|
||||
Pending,
|
||||
Done,
|
||||
Error,
|
||||
Confirmed,
|
||||
Canceled,
|
||||
}
|
||||
|
||||
impl InlineAssistStatus {
|
||||
pub(crate) fn is_pending(&self) -> bool {
|
||||
matches!(self, Self::Pending)
|
||||
}
|
||||
|
||||
pub(crate) fn is_confirmed(&self) -> bool {
|
||||
matches!(self, Self::Confirmed)
|
||||
}
|
||||
|
||||
pub(crate) fn is_done(&self) -> bool {
|
||||
matches!(self, Self::Done)
|
||||
}
|
||||
}
|
||||
|
||||
struct EditorInlineAssists {
|
||||
@@ -2290,8 +2199,6 @@ impl InlineAssist {
|
||||
|
||||
if assist.decorations.is_none() {
|
||||
this.finish_assist(assist_id, false, cx);
|
||||
} else if let Some(tx) = this.assist_observations.get(&assist_id) {
|
||||
tx.0.send(AssistStatus::Finished).ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
746
crates/assistant/src/patch.rs
Normal file
746
crates/assistant/src/patch.rs
Normal file
@@ -0,0 +1,746 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use editor::ProposedChangesEditor;
|
||||
use futures::{future, TryFutureExt as _};
|
||||
use gpui::{AppContext, AsyncAppContext, Model, SharedString};
|
||||
use language::{AutoindentMode, Buffer, BufferSnapshot};
|
||||
use project::{Project, ProjectPath};
|
||||
use std::{cmp, ops::Range, path::Path, sync::Arc};
|
||||
use text::{AnchorRangeExt as _, Bias, OffsetRangeExt as _, Point};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct AssistantPatch {
|
||||
pub range: Range<language::Anchor>,
|
||||
pub title: SharedString,
|
||||
pub edits: Arc<[Result<AssistantEdit>]>,
|
||||
pub status: AssistantPatchStatus,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum AssistantPatchStatus {
|
||||
Pending,
|
||||
Ready,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct AssistantEdit {
|
||||
pub path: String,
|
||||
pub kind: AssistantEditKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum AssistantEditKind {
|
||||
Update {
|
||||
old_text: String,
|
||||
new_text: String,
|
||||
description: String,
|
||||
},
|
||||
Create {
|
||||
new_text: String,
|
||||
description: String,
|
||||
},
|
||||
InsertBefore {
|
||||
old_text: String,
|
||||
new_text: String,
|
||||
description: String,
|
||||
},
|
||||
InsertAfter {
|
||||
old_text: String,
|
||||
new_text: String,
|
||||
description: String,
|
||||
},
|
||||
Delete {
|
||||
old_text: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct ResolvedPatch {
|
||||
pub edit_groups: HashMap<Model<Buffer>, Vec<ResolvedEditGroup>>,
|
||||
pub errors: Vec<AssistantPatchResolutionError>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ResolvedEditGroup {
|
||||
pub context_range: Range<language::Anchor>,
|
||||
pub edits: Vec<ResolvedEdit>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ResolvedEdit {
|
||||
range: Range<language::Anchor>,
|
||||
new_text: String,
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct AssistantPatchResolutionError {
|
||||
pub edit_ix: usize,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum SearchDirection {
|
||||
Up,
|
||||
Left,
|
||||
Diagonal,
|
||||
}
|
||||
|
||||
// A measure of the currently quality of an in-progress fuzzy search.
|
||||
//
|
||||
// Uses 60 bits to store a numeric cost, and 4 bits to store the preceding
|
||||
// operation in the search.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct SearchState {
|
||||
score: u32,
|
||||
direction: SearchDirection,
|
||||
}
|
||||
|
||||
impl SearchState {
|
||||
fn new(score: u32, direction: SearchDirection) -> Self {
|
||||
Self { score, direction }
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolvedPatch {
|
||||
pub fn apply(&self, editor: &ProposedChangesEditor, cx: &mut AppContext) {
|
||||
for (buffer, groups) in &self.edit_groups {
|
||||
let branch = editor.branch_buffer_for_base(buffer).unwrap();
|
||||
Self::apply_edit_groups(groups, &branch, cx);
|
||||
}
|
||||
editor.recalculate_all_buffer_diffs();
|
||||
}
|
||||
|
||||
fn apply_edit_groups(
|
||||
groups: &Vec<ResolvedEditGroup>,
|
||||
buffer: &Model<Buffer>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let mut edits = Vec::new();
|
||||
for group in groups {
|
||||
for suggestion in &group.edits {
|
||||
edits.push((suggestion.range.clone(), suggestion.new_text.clone()));
|
||||
}
|
||||
}
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
edits,
|
||||
Some(AutoindentMode::Block {
|
||||
original_indent_columns: Vec::new(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolvedEdit {
|
||||
pub fn try_merge(&mut self, other: &Self, buffer: &text::BufferSnapshot) -> bool {
|
||||
let range = &self.range;
|
||||
let other_range = &other.range;
|
||||
|
||||
// Don't merge if we don't contain the other suggestion.
|
||||
if range.start.cmp(&other_range.start, buffer).is_gt()
|
||||
|| range.end.cmp(&other_range.end, buffer).is_lt()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(description) = &mut self.description {
|
||||
if let Some(other_description) = &other.description {
|
||||
description.push('\n');
|
||||
description.push_str(other_description);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantEdit {
|
||||
pub fn new(
|
||||
path: Option<String>,
|
||||
operation: Option<String>,
|
||||
old_text: Option<String>,
|
||||
new_text: Option<String>,
|
||||
description: Option<String>,
|
||||
) -> Result<Self> {
|
||||
let path = path.ok_or_else(|| anyhow!("missing path"))?;
|
||||
let operation = operation.ok_or_else(|| anyhow!("missing operation"))?;
|
||||
|
||||
let kind = match operation.as_str() {
|
||||
"update" => AssistantEditKind::Update {
|
||||
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
||||
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
||||
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
||||
},
|
||||
"insert_before" => AssistantEditKind::InsertBefore {
|
||||
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
||||
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
||||
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
||||
},
|
||||
"insert_after" => AssistantEditKind::InsertAfter {
|
||||
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
||||
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
||||
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
||||
},
|
||||
"delete" => AssistantEditKind::Delete {
|
||||
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
||||
},
|
||||
"create" => AssistantEditKind::Create {
|
||||
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
||||
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
||||
},
|
||||
_ => Err(anyhow!("unknown operation {operation:?}"))?,
|
||||
};
|
||||
|
||||
Ok(Self { path, kind })
|
||||
}
|
||||
|
||||
pub async fn resolve(
|
||||
&self,
|
||||
project: Model<Project>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<(Model<Buffer>, ResolvedEdit)> {
|
||||
let path = self.path.clone();
|
||||
let kind = self.kind.clone();
|
||||
let buffer = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
let project_path = project
|
||||
.find_project_path(Path::new(&path), cx)
|
||||
.or_else(|| {
|
||||
// If we couldn't find a project path for it, put it in the active worktree
|
||||
// so that when we create the buffer, it can be saved.
|
||||
let worktree = project
|
||||
.active_entry()
|
||||
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
|
||||
.or_else(|| project.worktrees(cx).next())?;
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
Some(ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: Arc::from(Path::new(&path)),
|
||||
})
|
||||
})
|
||||
.with_context(|| format!("worktree not found for {:?}", path))?;
|
||||
anyhow::Ok(project.open_buffer(project_path, cx))
|
||||
})??
|
||||
.await?;
|
||||
|
||||
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
|
||||
let suggestion = cx
|
||||
.background_executor()
|
||||
.spawn(async move { kind.resolve(&snapshot) })
|
||||
.await;
|
||||
|
||||
Ok((buffer, suggestion))
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantEditKind {
|
||||
fn resolve(self, snapshot: &BufferSnapshot) -> ResolvedEdit {
|
||||
match self {
|
||||
Self::Update {
|
||||
old_text,
|
||||
new_text,
|
||||
description,
|
||||
} => {
|
||||
let range = Self::resolve_location(&snapshot, &old_text);
|
||||
ResolvedEdit {
|
||||
range,
|
||||
new_text,
|
||||
description: Some(description),
|
||||
}
|
||||
}
|
||||
Self::Create {
|
||||
new_text,
|
||||
description,
|
||||
} => ResolvedEdit {
|
||||
range: text::Anchor::MIN..text::Anchor::MAX,
|
||||
description: Some(description),
|
||||
new_text,
|
||||
},
|
||||
Self::InsertBefore {
|
||||
old_text,
|
||||
mut new_text,
|
||||
description,
|
||||
} => {
|
||||
let range = Self::resolve_location(&snapshot, &old_text);
|
||||
new_text.push('\n');
|
||||
ResolvedEdit {
|
||||
range: range.start..range.start,
|
||||
new_text,
|
||||
description: Some(description),
|
||||
}
|
||||
}
|
||||
Self::InsertAfter {
|
||||
old_text,
|
||||
mut new_text,
|
||||
description,
|
||||
} => {
|
||||
let range = Self::resolve_location(&snapshot, &old_text);
|
||||
new_text.insert(0, '\n');
|
||||
ResolvedEdit {
|
||||
range: range.end..range.end,
|
||||
new_text,
|
||||
description: Some(description),
|
||||
}
|
||||
}
|
||||
Self::Delete { old_text } => {
|
||||
let range = Self::resolve_location(&snapshot, &old_text);
|
||||
ResolvedEdit {
|
||||
range,
|
||||
new_text: String::new(),
|
||||
description: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_location(buffer: &text::BufferSnapshot, search_query: &str) -> Range<text::Anchor> {
|
||||
const INSERTION_COST: u32 = 3;
|
||||
const WHITESPACE_INSERTION_COST: u32 = 1;
|
||||
const DELETION_COST: u32 = 3;
|
||||
const WHITESPACE_DELETION_COST: u32 = 1;
|
||||
const EQUALITY_BONUS: u32 = 5;
|
||||
|
||||
struct Matrix {
|
||||
cols: usize,
|
||||
data: Vec<SearchState>,
|
||||
}
|
||||
|
||||
impl Matrix {
|
||||
fn new(rows: usize, cols: usize) -> Self {
|
||||
Matrix {
|
||||
cols,
|
||||
data: vec![SearchState::new(0, SearchDirection::Diagonal); rows * cols],
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, row: usize, col: usize) -> SearchState {
|
||||
self.data[row * self.cols + col]
|
||||
}
|
||||
|
||||
fn set(&mut self, row: usize, col: usize, cost: SearchState) {
|
||||
self.data[row * self.cols + col] = cost;
|
||||
}
|
||||
}
|
||||
|
||||
let buffer_len = buffer.len();
|
||||
let query_len = search_query.len();
|
||||
let mut matrix = Matrix::new(query_len + 1, buffer_len + 1);
|
||||
|
||||
for (row, query_byte) in search_query.bytes().enumerate() {
|
||||
for (col, buffer_byte) in buffer.bytes_in_range(0..buffer.len()).flatten().enumerate() {
|
||||
let deletion_cost = if query_byte.is_ascii_whitespace() {
|
||||
WHITESPACE_DELETION_COST
|
||||
} else {
|
||||
DELETION_COST
|
||||
};
|
||||
let insertion_cost = if buffer_byte.is_ascii_whitespace() {
|
||||
WHITESPACE_INSERTION_COST
|
||||
} else {
|
||||
INSERTION_COST
|
||||
};
|
||||
|
||||
let up = SearchState::new(
|
||||
matrix.get(row, col + 1).score.saturating_sub(deletion_cost),
|
||||
SearchDirection::Up,
|
||||
);
|
||||
let left = SearchState::new(
|
||||
matrix
|
||||
.get(row + 1, col)
|
||||
.score
|
||||
.saturating_sub(insertion_cost),
|
||||
SearchDirection::Left,
|
||||
);
|
||||
let diagonal = SearchState::new(
|
||||
if query_byte == *buffer_byte {
|
||||
matrix.get(row, col).score.saturating_add(EQUALITY_BONUS)
|
||||
} else {
|
||||
matrix
|
||||
.get(row, col)
|
||||
.score
|
||||
.saturating_sub(deletion_cost + insertion_cost)
|
||||
},
|
||||
SearchDirection::Diagonal,
|
||||
);
|
||||
matrix.set(row + 1, col + 1, up.max(left).max(diagonal));
|
||||
}
|
||||
}
|
||||
|
||||
// Traceback to find the best match
|
||||
let mut best_buffer_end = buffer_len;
|
||||
let mut best_score = 0;
|
||||
for col in 1..=buffer_len {
|
||||
let score = matrix.get(query_len, col).score;
|
||||
if score > best_score {
|
||||
best_score = score;
|
||||
best_buffer_end = col;
|
||||
}
|
||||
}
|
||||
|
||||
let mut query_ix = query_len;
|
||||
let mut buffer_ix = best_buffer_end;
|
||||
while query_ix > 0 && buffer_ix > 0 {
|
||||
let current = matrix.get(query_ix, buffer_ix);
|
||||
match current.direction {
|
||||
SearchDirection::Diagonal => {
|
||||
query_ix -= 1;
|
||||
buffer_ix -= 1;
|
||||
}
|
||||
SearchDirection::Up => {
|
||||
query_ix -= 1;
|
||||
}
|
||||
SearchDirection::Left => {
|
||||
buffer_ix -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut start = buffer.offset_to_point(buffer.clip_offset(buffer_ix, Bias::Left));
|
||||
start.column = 0;
|
||||
let mut end = buffer.offset_to_point(buffer.clip_offset(best_buffer_end, Bias::Right));
|
||||
if end.column > 0 {
|
||||
end.column = buffer.line_len(end.row);
|
||||
}
|
||||
|
||||
buffer.anchor_after(start)..buffer.anchor_before(end)
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantPatch {
|
||||
pub(crate) async fn resolve(
|
||||
&self,
|
||||
project: Model<Project>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> ResolvedPatch {
|
||||
let mut resolve_tasks = Vec::new();
|
||||
for (ix, edit) in self.edits.iter().enumerate() {
|
||||
if let Ok(edit) = edit.as_ref() {
|
||||
resolve_tasks.push(
|
||||
edit.resolve(project.clone(), cx.clone())
|
||||
.map_err(move |error| (ix, error)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let edits = future::join_all(resolve_tasks).await;
|
||||
let mut errors = Vec::new();
|
||||
let mut edits_by_buffer = HashMap::default();
|
||||
for entry in edits {
|
||||
match entry {
|
||||
Ok((buffer, edit)) => {
|
||||
edits_by_buffer
|
||||
.entry(buffer)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(edit);
|
||||
}
|
||||
Err((edit_ix, error)) => errors.push(AssistantPatchResolutionError {
|
||||
edit_ix,
|
||||
message: error.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// Expand the context ranges of each edit and group edits with overlapping context ranges.
|
||||
let mut edit_groups_by_buffer = HashMap::default();
|
||||
for (buffer, edits) in edits_by_buffer {
|
||||
if let Ok(snapshot) = buffer.update(cx, |buffer, _| buffer.text_snapshot()) {
|
||||
edit_groups_by_buffer.insert(buffer, Self::group_edits(edits, &snapshot));
|
||||
}
|
||||
}
|
||||
|
||||
ResolvedPatch {
|
||||
edit_groups: edit_groups_by_buffer,
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
fn group_edits(
|
||||
mut edits: Vec<ResolvedEdit>,
|
||||
snapshot: &text::BufferSnapshot,
|
||||
) -> Vec<ResolvedEditGroup> {
|
||||
let mut edit_groups = Vec::<ResolvedEditGroup>::new();
|
||||
// Sort edits by their range so that earlier, larger ranges come first
|
||||
edits.sort_by(|a, b| a.range.cmp(&b.range, &snapshot));
|
||||
|
||||
// Merge overlapping edits
|
||||
edits.dedup_by(|a, b| b.try_merge(a, &snapshot));
|
||||
|
||||
// Create context ranges for each edit
|
||||
for edit in edits {
|
||||
let context_range = {
|
||||
let edit_point_range = edit.range.to_point(&snapshot);
|
||||
let start_row = edit_point_range.start.row.saturating_sub(5);
|
||||
let end_row = cmp::min(edit_point_range.end.row + 5, snapshot.max_point().row);
|
||||
let start = snapshot.anchor_before(Point::new(start_row, 0));
|
||||
let end = snapshot.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
|
||||
start..end
|
||||
};
|
||||
|
||||
if let Some(last_group) = edit_groups.last_mut() {
|
||||
if last_group
|
||||
.context_range
|
||||
.end
|
||||
.cmp(&context_range.start, &snapshot)
|
||||
.is_ge()
|
||||
{
|
||||
// Merge with the previous group if context ranges overlap
|
||||
last_group.context_range.end = context_range.end;
|
||||
last_group.edits.push(edit);
|
||||
} else {
|
||||
// Create a new group
|
||||
edit_groups.push(ResolvedEditGroup {
|
||||
context_range,
|
||||
edits: vec![edit],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Create the first group
|
||||
edit_groups.push(ResolvedEditGroup {
|
||||
context_range,
|
||||
edits: vec![edit],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
edit_groups
|
||||
}
|
||||
|
||||
pub fn path_count(&self) -> usize {
|
||||
self.paths().count()
|
||||
}
|
||||
|
||||
pub fn paths(&self) -> impl '_ + Iterator<Item = &str> {
|
||||
let mut prev_path = None;
|
||||
self.edits.iter().filter_map(move |edit| {
|
||||
if let Ok(edit) = edit {
|
||||
let path = Some(edit.path.as_str());
|
||||
if path != prev_path {
|
||||
prev_path = path;
|
||||
return path;
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AssistantPatch {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.range == other.range
|
||||
&& self.title == other.title
|
||||
&& Arc::ptr_eq(&self.edits, &other.edits)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for AssistantPatch {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{AppContext, Context};
|
||||
use language::{
|
||||
language_settings::AllLanguageSettings, Language, LanguageConfig, LanguageMatcher,
|
||||
};
|
||||
use settings::SettingsStore;
|
||||
use text::{OffsetRangeExt, Point};
|
||||
use ui::BorrowAppContext;
|
||||
use unindent::Unindent as _;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_resolve_location(cx: &mut AppContext) {
|
||||
{
|
||||
let buffer = cx.new_model(|cx| {
|
||||
Buffer::local(
|
||||
concat!(
|
||||
" Lorem\n",
|
||||
" ipsum\n",
|
||||
" dolor sit amet\n",
|
||||
" consecteur",
|
||||
),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_eq!(
|
||||
AssistantEditKind::resolve_location(&snapshot, "ipsum\ndolor").to_point(&snapshot),
|
||||
Point::new(1, 0)..Point::new(2, 18)
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let buffer = cx.new_model(|cx| {
|
||||
Buffer::local(
|
||||
concat!(
|
||||
"fn foo1(a: usize) -> usize {\n",
|
||||
" 40\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"fn foo2(b: usize) -> usize {\n",
|
||||
" 42\n",
|
||||
"}\n",
|
||||
),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_eq!(
|
||||
AssistantEditKind::resolve_location(&snapshot, "fn foo1(b: usize) {\n40\n}")
|
||||
.to_point(&snapshot),
|
||||
Point::new(0, 0)..Point::new(2, 1)
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let buffer = cx.new_model(|cx| {
|
||||
Buffer::local(
|
||||
concat!(
|
||||
"fn main() {\n",
|
||||
" Foo\n",
|
||||
" .bar()\n",
|
||||
" .baz()\n",
|
||||
" .qux()\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"fn foo2(b: usize) -> usize {\n",
|
||||
" 42\n",
|
||||
"}\n",
|
||||
),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_eq!(
|
||||
AssistantEditKind::resolve_location(&snapshot, "Foo.bar.baz.qux()")
|
||||
.to_point(&snapshot),
|
||||
Point::new(1, 0)..Point::new(4, 14)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_resolve_edits(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings::<AllLanguageSettings>(cx, |_| {});
|
||||
});
|
||||
|
||||
assert_edits(
|
||||
"
|
||||
/// A person
|
||||
struct Person {
|
||||
name: String,
|
||||
age: usize,
|
||||
}
|
||||
|
||||
/// A dog
|
||||
struct Dog {
|
||||
weight: f32,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
vec![
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
name: String,
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
"
|
||||
.unindent(),
|
||||
description: "".into(),
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn name(&self) -> String {
|
||||
format!(\"{} {}\", self.first_name, self.last_name)
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: "".into(),
|
||||
},
|
||||
],
|
||||
"
|
||||
/// A person
|
||||
struct Person {
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
age: usize,
|
||||
}
|
||||
|
||||
/// A dog
|
||||
struct Dog {
|
||||
weight: f32,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
fn name(&self) -> String {
|
||||
format!(\"{} {}\", self.first_name, self.last_name)
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_edits(
|
||||
old_text: String,
|
||||
edits: Vec<AssistantEditKind>,
|
||||
new_text: String,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let buffer =
|
||||
cx.new_model(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let resolved_edits = edits
|
||||
.into_iter()
|
||||
.map(|kind| kind.resolve(&snapshot))
|
||||
.collect();
|
||||
let edit_groups = AssistantPatch::group_edits(resolved_edits, &snapshot);
|
||||
ResolvedPatch::apply_edit_groups(&edit_groups, &buffer, cx);
|
||||
let actual_new_text = buffer.read(cx).text();
|
||||
pretty_assertions::assert_eq!(actual_new_text, new_text);
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(language::tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
@@ -45,15 +45,6 @@ pub struct ProjectSlashCommandPromptContext {
|
||||
pub context_buffer: String,
|
||||
}
|
||||
|
||||
/// Context required to generate a workflow step resolution prompt.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct StepResolutionContext {
|
||||
/// The full context, including <step>...</step> tags
|
||||
pub workflow_context: String,
|
||||
/// The text of the specific step from the context to resolve
|
||||
pub step_to_resolve: String,
|
||||
}
|
||||
|
||||
pub struct PromptLoadingParams<'a> {
|
||||
pub fs: Arc<dyn Fs>,
|
||||
pub repo_path: Option<PathBuf>,
|
||||
|
||||
@@ -18,6 +18,8 @@ pub(crate) struct WorkflowSlashCommand {
|
||||
}
|
||||
|
||||
impl WorkflowSlashCommand {
|
||||
pub const NAME: &'static str = "workflow";
|
||||
|
||||
pub fn new(prompt_builder: Arc<PromptBuilder>) -> Self {
|
||||
Self { prompt_builder }
|
||||
}
|
||||
@@ -25,7 +27,7 @@ impl WorkflowSlashCommand {
|
||||
|
||||
impl SlashCommand for WorkflowSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"workflow".into()
|
||||
Self::NAME.into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
|
||||
@@ -1,507 +0,0 @@
|
||||
use crate::{AssistantPanel, InlineAssistId, InlineAssistant};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use gpui::AsyncAppContext;
|
||||
use gpui::{Model, Task, UpdateGlobal as _, View, WeakView, WindowContext};
|
||||
use language::{Buffer, BufferSnapshot};
|
||||
use project::{Project, ProjectPath};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{ops::Range, path::Path, sync::Arc};
|
||||
use text::Bias;
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct WorkflowStep {
|
||||
pub range: Range<language::Anchor>,
|
||||
pub leading_tags_end: text::Anchor,
|
||||
pub trailing_tag_start: Option<text::Anchor>,
|
||||
pub edits: Arc<[Result<WorkflowStepEdit>]>,
|
||||
pub resolution_task: Option<Task<()>>,
|
||||
pub resolution: Option<Arc<Result<WorkflowStepResolution>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct WorkflowStepEdit {
|
||||
pub path: String,
|
||||
pub kind: WorkflowStepEditKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct WorkflowStepResolution {
|
||||
pub title: String,
|
||||
pub suggestion_groups: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct WorkflowSuggestionGroup {
|
||||
pub context_range: Range<language::Anchor>,
|
||||
pub suggestions: Vec<WorkflowSuggestion>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum WorkflowSuggestion {
|
||||
Update {
|
||||
range: Range<language::Anchor>,
|
||||
description: String,
|
||||
},
|
||||
CreateFile {
|
||||
description: String,
|
||||
},
|
||||
InsertBefore {
|
||||
position: language::Anchor,
|
||||
description: String,
|
||||
},
|
||||
InsertAfter {
|
||||
position: language::Anchor,
|
||||
description: String,
|
||||
},
|
||||
Delete {
|
||||
range: Range<language::Anchor>,
|
||||
},
|
||||
}
|
||||
|
||||
impl WorkflowSuggestion {
|
||||
pub fn range(&self) -> Range<language::Anchor> {
|
||||
match self {
|
||||
Self::Update { range, .. } => range.clone(),
|
||||
Self::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
|
||||
Self::InsertBefore { position, .. } | Self::InsertAfter { position, .. } => {
|
||||
*position..*position
|
||||
}
|
||||
Self::Delete { range, .. } => range.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn description(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Update { description, .. }
|
||||
| Self::CreateFile { description }
|
||||
| Self::InsertBefore { description, .. }
|
||||
| Self::InsertAfter { description, .. } => Some(description),
|
||||
Self::Delete { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn description_mut(&mut self) -> Option<&mut String> {
|
||||
match self {
|
||||
Self::Update { description, .. }
|
||||
| Self::CreateFile { description }
|
||||
| Self::InsertBefore { description, .. }
|
||||
| Self::InsertAfter { description, .. } => Some(description),
|
||||
Self::Delete { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool {
|
||||
let range = self.range();
|
||||
let other_range = other.range();
|
||||
|
||||
// Don't merge if we don't contain the other suggestion.
|
||||
if range.start.cmp(&other_range.start, buffer).is_gt()
|
||||
|| range.end.cmp(&other_range.end, buffer).is_lt()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(description) = self.description_mut() {
|
||||
if let Some(other_description) = other.description() {
|
||||
description.push('\n');
|
||||
description.push_str(other_description);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn show(
|
||||
&self,
|
||||
editor: &View<Editor>,
|
||||
excerpt_id: editor::ExcerptId,
|
||||
workspace: &WeakView<Workspace>,
|
||||
assistant_panel: &View<AssistantPanel>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<InlineAssistId> {
|
||||
let mut initial_transaction_id = None;
|
||||
let initial_prompt;
|
||||
let suggestion_range;
|
||||
let buffer = editor.read(cx).buffer().clone();
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
|
||||
match self {
|
||||
Self::Update {
|
||||
range, description, ..
|
||||
} => {
|
||||
initial_prompt = description.clone();
|
||||
suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
|
||||
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
|
||||
}
|
||||
Self::CreateFile { description } => {
|
||||
initial_prompt = description.clone();
|
||||
suggestion_range = editor::Anchor::min()..editor::Anchor::min();
|
||||
}
|
||||
Self::InsertBefore {
|
||||
position,
|
||||
description,
|
||||
..
|
||||
} => {
|
||||
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
|
||||
initial_prompt = description.clone();
|
||||
suggestion_range = buffer.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction(cx);
|
||||
let line_start = buffer.insert_empty_line(position, true, true, cx);
|
||||
initial_transaction_id = buffer.end_transaction(cx);
|
||||
buffer.refresh_preview(cx);
|
||||
|
||||
let line_start = buffer.read(cx).anchor_before(line_start);
|
||||
line_start..line_start
|
||||
});
|
||||
}
|
||||
Self::InsertAfter {
|
||||
position,
|
||||
description,
|
||||
..
|
||||
} => {
|
||||
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
|
||||
initial_prompt = description.clone();
|
||||
suggestion_range = buffer.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction(cx);
|
||||
let line_start = buffer.insert_empty_line(position, true, true, cx);
|
||||
initial_transaction_id = buffer.end_transaction(cx);
|
||||
buffer.refresh_preview(cx);
|
||||
|
||||
let line_start = buffer.read(cx).anchor_before(line_start);
|
||||
line_start..line_start
|
||||
});
|
||||
}
|
||||
Self::Delete { range, .. } => {
|
||||
initial_prompt = "Delete".to_string();
|
||||
suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
|
||||
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
|
||||
}
|
||||
}
|
||||
|
||||
InlineAssistant::update_global(cx, |inline_assistant, cx| {
|
||||
Some(inline_assistant.suggest_assist(
|
||||
editor,
|
||||
suggestion_range,
|
||||
initial_prompt,
|
||||
initial_transaction_id,
|
||||
false,
|
||||
Some(workspace.clone()),
|
||||
Some(assistant_panel),
|
||||
cx,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkflowStepEdit {
|
||||
pub fn new(
|
||||
path: Option<String>,
|
||||
operation: Option<String>,
|
||||
search: Option<String>,
|
||||
description: Option<String>,
|
||||
) -> Result<Self> {
|
||||
let path = path.ok_or_else(|| anyhow!("missing path"))?;
|
||||
let operation = operation.ok_or_else(|| anyhow!("missing operation"))?;
|
||||
|
||||
let kind = match operation.as_str() {
|
||||
"update" => WorkflowStepEditKind::Update {
|
||||
search: search.ok_or_else(|| anyhow!("missing search"))?,
|
||||
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
||||
},
|
||||
"insert_before" => WorkflowStepEditKind::InsertBefore {
|
||||
search: search.ok_or_else(|| anyhow!("missing search"))?,
|
||||
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
||||
},
|
||||
"insert_after" => WorkflowStepEditKind::InsertAfter {
|
||||
search: search.ok_or_else(|| anyhow!("missing search"))?,
|
||||
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
||||
},
|
||||
"delete" => WorkflowStepEditKind::Delete {
|
||||
search: search.ok_or_else(|| anyhow!("missing search"))?,
|
||||
},
|
||||
"create" => WorkflowStepEditKind::Create {
|
||||
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
||||
},
|
||||
_ => Err(anyhow!("unknown operation {operation:?}"))?,
|
||||
};
|
||||
|
||||
Ok(Self { path, kind })
|
||||
}
|
||||
|
||||
pub async fn resolve(
|
||||
&self,
|
||||
project: Model<Project>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<(Model<Buffer>, super::WorkflowSuggestion)> {
|
||||
let path = self.path.clone();
|
||||
let kind = self.kind.clone();
|
||||
let buffer = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
let project_path = project
|
||||
.find_project_path(Path::new(&path), cx)
|
||||
.or_else(|| {
|
||||
// If we couldn't find a project path for it, put it in the active worktree
|
||||
// so that when we create the buffer, it can be saved.
|
||||
let worktree = project
|
||||
.active_entry()
|
||||
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
|
||||
.or_else(|| project.worktrees(cx).next())?;
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
Some(ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: Arc::from(Path::new(&path)),
|
||||
})
|
||||
})
|
||||
.with_context(|| format!("worktree not found for {:?}", path))?;
|
||||
anyhow::Ok(project.open_buffer(project_path, cx))
|
||||
})??
|
||||
.await?;
|
||||
|
||||
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
|
||||
let suggestion = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
match kind {
|
||||
WorkflowStepEditKind::Update {
|
||||
search,
|
||||
description,
|
||||
} => {
|
||||
let range = Self::resolve_location(&snapshot, &search);
|
||||
WorkflowSuggestion::Update { range, description }
|
||||
}
|
||||
WorkflowStepEditKind::Create { description } => {
|
||||
WorkflowSuggestion::CreateFile { description }
|
||||
}
|
||||
WorkflowStepEditKind::InsertBefore {
|
||||
search,
|
||||
description,
|
||||
} => {
|
||||
let range = Self::resolve_location(&snapshot, &search);
|
||||
WorkflowSuggestion::InsertBefore {
|
||||
position: range.start,
|
||||
description,
|
||||
}
|
||||
}
|
||||
WorkflowStepEditKind::InsertAfter {
|
||||
search,
|
||||
description,
|
||||
} => {
|
||||
let range = Self::resolve_location(&snapshot, &search);
|
||||
WorkflowSuggestion::InsertAfter {
|
||||
position: range.end,
|
||||
description,
|
||||
}
|
||||
}
|
||||
WorkflowStepEditKind::Delete { search } => {
|
||||
let range = Self::resolve_location(&snapshot, &search);
|
||||
WorkflowSuggestion::Delete { range }
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok((buffer, suggestion))
|
||||
}
|
||||
|
||||
fn resolve_location(buffer: &text::BufferSnapshot, search_query: &str) -> Range<text::Anchor> {
|
||||
const INSERTION_SCORE: f64 = -1.0;
|
||||
const DELETION_SCORE: f64 = -1.0;
|
||||
const REPLACEMENT_SCORE: f64 = -1.0;
|
||||
const EQUALITY_SCORE: f64 = 5.0;
|
||||
|
||||
struct Matrix {
|
||||
cols: usize,
|
||||
data: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Matrix {
|
||||
fn new(rows: usize, cols: usize) -> Self {
|
||||
Matrix {
|
||||
cols,
|
||||
data: vec![0.0; rows * cols],
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, row: usize, col: usize) -> f64 {
|
||||
self.data[row * self.cols + col]
|
||||
}
|
||||
|
||||
fn set(&mut self, row: usize, col: usize, value: f64) {
|
||||
self.data[row * self.cols + col] = value;
|
||||
}
|
||||
}
|
||||
|
||||
let buffer_len = buffer.len();
|
||||
let query_len = search_query.len();
|
||||
let mut matrix = Matrix::new(query_len + 1, buffer_len + 1);
|
||||
|
||||
for (i, query_byte) in search_query.bytes().enumerate() {
|
||||
for (j, buffer_byte) in buffer.bytes_in_range(0..buffer.len()).flatten().enumerate() {
|
||||
let match_score = if query_byte == *buffer_byte {
|
||||
EQUALITY_SCORE
|
||||
} else {
|
||||
REPLACEMENT_SCORE
|
||||
};
|
||||
let up = matrix.get(i + 1, j) + DELETION_SCORE;
|
||||
let left = matrix.get(i, j + 1) + INSERTION_SCORE;
|
||||
let diagonal = matrix.get(i, j) + match_score;
|
||||
let score = up.max(left.max(diagonal)).max(0.);
|
||||
matrix.set(i + 1, j + 1, score);
|
||||
}
|
||||
}
|
||||
|
||||
// Traceback to find the best match
|
||||
let mut best_buffer_end = buffer_len;
|
||||
let mut best_score = 0.0;
|
||||
for col in 1..=buffer_len {
|
||||
let score = matrix.get(query_len, col);
|
||||
if score > best_score {
|
||||
best_score = score;
|
||||
best_buffer_end = col;
|
||||
}
|
||||
}
|
||||
|
||||
let mut query_ix = query_len;
|
||||
let mut buffer_ix = best_buffer_end;
|
||||
while query_ix > 0 && buffer_ix > 0 {
|
||||
let current = matrix.get(query_ix, buffer_ix);
|
||||
let up = matrix.get(query_ix - 1, buffer_ix);
|
||||
let left = matrix.get(query_ix, buffer_ix - 1);
|
||||
if current == left + INSERTION_SCORE {
|
||||
buffer_ix -= 1;
|
||||
} else if current == up + DELETION_SCORE {
|
||||
query_ix -= 1;
|
||||
} else {
|
||||
query_ix -= 1;
|
||||
buffer_ix -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
let mut start = buffer.offset_to_point(buffer.clip_offset(buffer_ix, Bias::Left));
|
||||
start.column = 0;
|
||||
let mut end = buffer.offset_to_point(buffer.clip_offset(best_buffer_end, Bias::Right));
|
||||
end.column = buffer.line_len(end.row);
|
||||
|
||||
buffer.anchor_after(start)..buffer.anchor_before(end)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "operation")]
|
||||
pub enum WorkflowStepEditKind {
|
||||
/// Rewrites the specified text entirely based on the given description.
|
||||
/// This operation completely replaces the given text.
|
||||
Update {
|
||||
/// A string in the source text to apply the update to.
|
||||
search: String,
|
||||
/// A brief description of the transformation to apply to the symbol.
|
||||
description: String,
|
||||
},
|
||||
/// Creates a new file with the given path based on the provided description.
|
||||
/// This operation adds a new file to the codebase.
|
||||
Create {
|
||||
/// A brief description of the file to be created.
|
||||
description: String,
|
||||
},
|
||||
/// Inserts text before the specified text in the source file.
|
||||
InsertBefore {
|
||||
/// A string in the source text to insert text before.
|
||||
search: String,
|
||||
/// A brief description of how the new text should be generated.
|
||||
description: String,
|
||||
},
|
||||
/// Inserts text after the specified text in the source file.
|
||||
InsertAfter {
|
||||
/// A string in the source text to insert text after.
|
||||
search: String,
|
||||
/// A brief description of how the new text should be generated.
|
||||
description: String,
|
||||
},
|
||||
/// Deletes the specified symbol from the containing file.
|
||||
Delete {
|
||||
/// A string in the source text to delete.
|
||||
search: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{AppContext, Context};
|
||||
use text::{OffsetRangeExt, Point};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_resolve_location(cx: &mut AppContext) {
|
||||
{
|
||||
let buffer = cx.new_model(|cx| {
|
||||
Buffer::local(
|
||||
concat!(
|
||||
" Lorem\n",
|
||||
" ipsum\n",
|
||||
" dolor sit amet\n",
|
||||
" consecteur",
|
||||
),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_eq!(
|
||||
WorkflowStepEdit::resolve_location(&snapshot, "ipsum\ndolor").to_point(&snapshot),
|
||||
Point::new(1, 0)..Point::new(2, 18)
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let buffer = cx.new_model(|cx| {
|
||||
Buffer::local(
|
||||
concat!(
|
||||
"fn foo1(a: usize) -> usize {\n",
|
||||
" 42\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"fn foo2(b: usize) -> usize {\n",
|
||||
" 42\n",
|
||||
"}\n",
|
||||
),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_eq!(
|
||||
WorkflowStepEdit::resolve_location(&snapshot, "fn foo1(b: usize) {\n42\n}")
|
||||
.to_point(&snapshot),
|
||||
Point::new(0, 0)..Point::new(2, 1)
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let buffer = cx.new_model(|cx| {
|
||||
Buffer::local(
|
||||
concat!(
|
||||
"fn main() {\n",
|
||||
" Foo\n",
|
||||
" .bar()\n",
|
||||
" .baz()\n",
|
||||
" .qux()\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"fn foo2(b: usize) -> usize {\n",
|
||||
" 42\n",
|
||||
"}\n",
|
||||
),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_eq!(
|
||||
WorkflowStepEdit::resolve_location(&snapshot, "Foo.bar.baz.qux()")
|
||||
.to_point(&snapshot),
|
||||
Point::new(1, 0)..Point::new(4, 14)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pub mod test;
|
||||
mod socks;
|
||||
pub mod telemetry;
|
||||
pub mod user;
|
||||
pub mod zed_urls;
|
||||
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use async_recursion::async_recursion;
|
||||
|
||||
19
crates/client/src/zed_urls.rs
Normal file
19
crates/client/src/zed_urls.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
//! Contains helper functions for constructing URLs to various Zed-related pages.
|
||||
//!
|
||||
//! These URLs will adapt to the configured server URL in order to construct
|
||||
//! links appropriate for the environment (e.g., by linking to a local copy of
|
||||
//! zed.dev in development).
|
||||
|
||||
use gpui::AppContext;
|
||||
use settings::Settings;
|
||||
|
||||
use crate::ClientSettings;
|
||||
|
||||
fn server_url(cx: &AppContext) -> &str {
|
||||
&ClientSettings::get_global(cx).server_url
|
||||
}
|
||||
|
||||
/// Returns the URL to the account page on zed.dev.
|
||||
pub fn account_url(cx: &AppContext) -> String {
|
||||
format!("{server_url}/account", server_url = server_url(cx))
|
||||
}
|
||||
@@ -10,6 +10,8 @@
|
||||
Copy,
|
||||
derive_more::Add,
|
||||
derive_more::AddAssign,
|
||||
derive_more::Sub,
|
||||
derive_more::SubAssign,
|
||||
)]
|
||||
pub struct Cents(pub u32);
|
||||
|
||||
|
||||
@@ -198,10 +198,6 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_llm_billing_enabled(&self) -> bool {
|
||||
self.stripe_api_key.is_some()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn test() -> Self {
|
||||
Self {
|
||||
|
||||
@@ -460,27 +460,27 @@ async fn check_usage_limit(
|
||||
)
|
||||
.await?;
|
||||
|
||||
if state.config.is_llm_billing_enabled() {
|
||||
if usage.spending_this_month >= FREE_TIER_MONTHLY_SPENDING_LIMIT {
|
||||
if !claims.has_llm_subscription {
|
||||
return Err(Error::http(
|
||||
StatusCode::PAYMENT_REQUIRED,
|
||||
"Maximum spending limit reached for this month.".to_string(),
|
||||
));
|
||||
}
|
||||
if usage.spending_this_month >= FREE_TIER_MONTHLY_SPENDING_LIMIT {
|
||||
if !claims.has_llm_subscription {
|
||||
return Err(Error::http(
|
||||
StatusCode::PAYMENT_REQUIRED,
|
||||
"Maximum spending limit reached for this month.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if usage.spending_this_month >= Cents(claims.max_monthly_spend_in_cents) {
|
||||
return Err(Error::Http(
|
||||
StatusCode::FORBIDDEN,
|
||||
"Maximum spending limit reached for this month.".to_string(),
|
||||
[(
|
||||
HeaderName::from_static(MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME),
|
||||
HeaderValue::from_static("true"),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
));
|
||||
}
|
||||
if (usage.spending_this_month - FREE_TIER_MONTHLY_SPENDING_LIMIT)
|
||||
>= Cents(claims.max_monthly_spend_in_cents)
|
||||
{
|
||||
return Err(Error::Http(
|
||||
StatusCode::FORBIDDEN,
|
||||
"Maximum spending limit reached for this month.".to_string(),
|
||||
[(
|
||||
HeaderName::from_static(MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME),
|
||||
HeaderValue::from_static("true"),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,7 +625,6 @@ where
|
||||
impl<S> Drop for TokenCountingStream<S> {
|
||||
fn drop(&mut self) {
|
||||
let state = self.state.clone();
|
||||
let is_llm_billing_enabled = state.config.is_llm_billing_enabled();
|
||||
let claims = self.claims.clone();
|
||||
let provider = self.provider;
|
||||
let model = std::mem::take(&mut self.model);
|
||||
@@ -639,14 +638,7 @@ impl<S> Drop for TokenCountingStream<S> {
|
||||
provider,
|
||||
&model,
|
||||
tokens,
|
||||
// We're passing `false` here if LLM billing is not enabled
|
||||
// so that we don't write any records to the
|
||||
// `billing_events` table until we're ready to bill users.
|
||||
if is_llm_billing_enabled {
|
||||
claims.has_llm_subscription
|
||||
} else {
|
||||
false
|
||||
},
|
||||
claims.has_llm_subscription,
|
||||
Cents(claims.max_monthly_spend_in_cents),
|
||||
Utc::now(),
|
||||
)
|
||||
|
||||
@@ -412,7 +412,7 @@ impl LlmDatabase {
|
||||
if !is_staff
|
||||
&& spending_this_month > FREE_TIER_MONTHLY_SPENDING_LIMIT
|
||||
&& has_llm_subscription
|
||||
&& spending_this_month <= max_monthly_spend
|
||||
&& (spending_this_month - FREE_TIER_MONTHLY_SPENDING_LIMIT) <= max_monthly_spend
|
||||
{
|
||||
billing_event::ActiveModel {
|
||||
id: ActiveValue::not_set(),
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use crate::{
|
||||
db::UserId,
|
||||
llm::{
|
||||
db::{
|
||||
queries::{providers::ModelParams, usages::Usage},
|
||||
LlmDatabase, TokenUsage,
|
||||
},
|
||||
db::{queries::providers::ModelParams, LlmDatabase, TokenUsage},
|
||||
FREE_TIER_MONTHLY_SPENDING_LIMIT,
|
||||
},
|
||||
test_llm_db, Cents,
|
||||
@@ -76,29 +73,9 @@ async fn test_billing_limit_exceeded(db: &mut LlmDatabase) {
|
||||
|
||||
// Verify the recorded usage and spending
|
||||
let recorded_usage = db.get_usage(user_id, provider, model, now).await.unwrap();
|
||||
|
||||
// Verify that we exceeded the free tier usage
|
||||
assert!(
|
||||
recorded_usage.spending_this_month > FREE_TIER_MONTHLY_SPENDING_LIMIT,
|
||||
"Expected spending to exceed free tier limit"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
recorded_usage,
|
||||
Usage {
|
||||
requests_this_minute: 1,
|
||||
tokens_this_minute: tokens_to_use,
|
||||
tokens_this_day: tokens_to_use,
|
||||
tokens_this_month: TokenUsage {
|
||||
input: tokens_to_use,
|
||||
input_cache_creation: 0,
|
||||
input_cache_read: 0,
|
||||
output: 0,
|
||||
},
|
||||
spending_this_month: Cents::new(1050),
|
||||
lifetime_spending: Cents::new(1050),
|
||||
}
|
||||
);
|
||||
assert_eq!(recorded_usage.spending_this_month, Cents::new(1050));
|
||||
assert!(recorded_usage.spending_this_month > FREE_TIER_MONTHLY_SPENDING_LIMIT);
|
||||
|
||||
// Verify that there is one `billing_event` record
|
||||
let billing_events = db.get_billing_events().await.unwrap();
|
||||
@@ -111,7 +88,35 @@ async fn test_billing_limit_exceeded(db: &mut LlmDatabase) {
|
||||
assert_eq!(billing_event.input_cache_read_tokens, 0);
|
||||
assert_eq!(billing_event.output_tokens, 0);
|
||||
|
||||
let tokens_to_exceed = 20_000_000; // This will cost $1.00 more, pushing us from $10.50 to $11.50, which is over the $11 monthly maximum limit
|
||||
// Record usage that puts us at $20.50
|
||||
let usage_2 = TokenUsage {
|
||||
input: 200_000_000, // This will cost $10 more, pushing us from $10.50 to $20.50,
|
||||
input_cache_creation: 0,
|
||||
input_cache_read: 0,
|
||||
output: 0,
|
||||
};
|
||||
db.record_usage(
|
||||
user_id,
|
||||
false,
|
||||
provider,
|
||||
model,
|
||||
usage_2,
|
||||
true,
|
||||
max_monthly_spend,
|
||||
now,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify the updated usage and spending
|
||||
let updated_usage = db.get_usage(user_id, provider, model, now).await.unwrap();
|
||||
assert_eq!(updated_usage.spending_this_month, Cents::new(2050));
|
||||
|
||||
// Verify that there are now two billing events
|
||||
let billing_events = db.get_billing_events().await.unwrap();
|
||||
assert_eq!(billing_events.len(), 2);
|
||||
|
||||
let tokens_to_exceed = 20_000_000; // This will cost $1.00 more, pushing us from $20.50 to $21.50, which is over the $11 monthly maximum limit
|
||||
let usage_exceeding = TokenUsage {
|
||||
input: tokens_to_exceed,
|
||||
input_cache_creation: 0,
|
||||
@@ -132,27 +137,12 @@ async fn test_billing_limit_exceeded(db: &mut LlmDatabase) {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify that there is still one billing record
|
||||
let billing_events = db.get_billing_events().await.unwrap();
|
||||
assert_eq!(billing_events.len(), 1);
|
||||
|
||||
// Verify the updated usage and spending
|
||||
let updated_usage = db.get_usage(user_id, provider, model, now).await.unwrap();
|
||||
assert_eq!(
|
||||
updated_usage,
|
||||
Usage {
|
||||
requests_this_minute: 2,
|
||||
tokens_this_minute: tokens_to_use + tokens_to_exceed,
|
||||
tokens_this_day: tokens_to_use + tokens_to_exceed,
|
||||
tokens_this_month: TokenUsage {
|
||||
input: tokens_to_use + tokens_to_exceed,
|
||||
input_cache_creation: 0,
|
||||
input_cache_read: 0,
|
||||
output: 0,
|
||||
},
|
||||
spending_this_month: Cents::new(1150),
|
||||
lifetime_spending: Cents::new(1150),
|
||||
}
|
||||
);
|
||||
assert_eq!(updated_usage.spending_this_month, Cents::new(2150));
|
||||
|
||||
// Verify that we never exceed the user max spending for the user
|
||||
// and avoid charging them.
|
||||
let billing_events = db.get_billing_events().await.unwrap();
|
||||
assert_eq!(billing_events.len(), 2);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use crate::{llm, Cents, Result};
|
||||
use anyhow::Context;
|
||||
use chrono::Utc;
|
||||
use chrono::{Datelike, Utc};
|
||||
use collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::RwLock;
|
||||
@@ -349,10 +349,20 @@ impl StripeBilling {
|
||||
model: &StripeModel,
|
||||
success_url: &str,
|
||||
) -> Result<String> {
|
||||
let first_of_next_month = Utc::now()
|
||||
.checked_add_months(chrono::Months::new(1))
|
||||
.unwrap()
|
||||
.with_day(1)
|
||||
.unwrap();
|
||||
|
||||
let mut params = stripe::CreateCheckoutSession::new();
|
||||
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
|
||||
params.customer = Some(customer_id);
|
||||
params.client_reference_id = Some(github_login);
|
||||
params.subscription_data = Some(stripe::CreateCheckoutSessionSubscriptionData {
|
||||
billing_cycle_anchor: Some(first_of_next_month.timestamp()),
|
||||
..Default::default()
|
||||
});
|
||||
params.line_items = Some(
|
||||
[
|
||||
&model.input_tokens_price.id,
|
||||
|
||||
@@ -26,7 +26,7 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
.await;
|
||||
|
||||
// Set up project on remote FS
|
||||
let (client_ssh, server_ssh) = SshRemoteClient::fake(cx_a, server_cx);
|
||||
let (forwarder, server_ssh) = SshRemoteClient::fake_server(server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
@@ -67,6 +67,7 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = SshRemoteClient::fake_client(forwarder, cx_a).await;
|
||||
let (project_a, worktree_id) = client_a
|
||||
.build_ssh_project("/code/project1", client_ssh, cx_a)
|
||||
.await;
|
||||
|
||||
@@ -48,7 +48,6 @@ mod signature_help;
|
||||
pub mod test;
|
||||
|
||||
use ::git::diff::DiffHunkStatus;
|
||||
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
|
||||
pub(crate) use actions::*;
|
||||
use aho_corasick::AhoCorasick;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
@@ -101,7 +100,7 @@ use language::{
|
||||
};
|
||||
use linked_editing_ranges::refresh_linked_ranges;
|
||||
pub use proposed_changes_editor::{
|
||||
ProposedChangesBuffer, ProposedChangesEditor, ProposedChangesEditorToolbar,
|
||||
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
|
||||
};
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
||||
@@ -11488,11 +11487,8 @@ impl Editor {
|
||||
snapshot.line_len(buffer_row) == 0
|
||||
}
|
||||
|
||||
fn get_permalink_to_line(&mut self, cx: &mut ViewContext<Self>) -> Result<url::Url> {
|
||||
let (path, selection, repo) = maybe!({
|
||||
let project_handle = self.project.as_ref()?.clone();
|
||||
let project = project_handle.read(cx);
|
||||
|
||||
fn get_permalink_to_line(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<url::Url>> {
|
||||
let buffer_and_selection = maybe!({
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
let selection_range = selection.range();
|
||||
|
||||
@@ -11516,64 +11512,58 @@ impl Editor {
|
||||
(buffer.clone(), selection)
|
||||
};
|
||||
|
||||
let path = buffer
|
||||
.read(cx)
|
||||
.file()?
|
||||
.as_local()?
|
||||
.path()
|
||||
.to_str()?
|
||||
.to_string();
|
||||
let repo = project.get_repo(&buffer.read(cx).project_path(cx)?, cx)?;
|
||||
Some((path, selection, repo))
|
||||
Some((buffer, selection))
|
||||
});
|
||||
|
||||
let Some((buffer, selection)) = buffer_and_selection else {
|
||||
return Task::ready(Err(anyhow!("failed to determine buffer and selection")));
|
||||
};
|
||||
|
||||
let Some(project) = self.project.as_ref() else {
|
||||
return Task::ready(Err(anyhow!("editor does not have project")));
|
||||
};
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
project.get_permalink_to_line(&buffer, selection, cx)
|
||||
})
|
||||
.ok_or_else(|| anyhow!("unable to open git repository"))?;
|
||||
|
||||
const REMOTE_NAME: &str = "origin";
|
||||
let origin_url = repo
|
||||
.remote_url(REMOTE_NAME)
|
||||
.ok_or_else(|| anyhow!("remote \"{REMOTE_NAME}\" not found"))?;
|
||||
let sha = repo
|
||||
.head_sha()
|
||||
.ok_or_else(|| anyhow!("failed to read HEAD SHA"))?;
|
||||
|
||||
let (provider, remote) =
|
||||
parse_git_remote_url(GitHostingProviderRegistry::default_global(cx), &origin_url)
|
||||
.ok_or_else(|| anyhow!("failed to parse Git remote URL"))?;
|
||||
|
||||
Ok(provider.build_permalink(
|
||||
remote,
|
||||
BuildPermalinkParams {
|
||||
sha: &sha,
|
||||
path: &path,
|
||||
selection: Some(selection),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn copy_permalink_to_line(&mut self, _: &CopyPermalinkToLine, cx: &mut ViewContext<Self>) {
|
||||
let permalink = self.get_permalink_to_line(cx);
|
||||
let permalink_task = self.get_permalink_to_line(cx);
|
||||
let workspace = self.workspace();
|
||||
|
||||
match permalink {
|
||||
Ok(permalink) => {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string()));
|
||||
}
|
||||
Err(err) => {
|
||||
let message = format!("Failed to copy permalink: {err}");
|
||||
|
||||
Err::<(), anyhow::Error>(err).log_err();
|
||||
|
||||
if let Some(workspace) = self.workspace() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
struct CopyPermalinkToLine;
|
||||
|
||||
workspace.show_toast(
|
||||
Toast::new(NotificationId::unique::<CopyPermalinkToLine>(), message),
|
||||
cx,
|
||||
)
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
match permalink_task.await {
|
||||
Ok(permalink) => {
|
||||
cx.update(|cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string()));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(err) => {
|
||||
let message = format!("Failed to copy permalink: {err}");
|
||||
|
||||
Err::<(), anyhow::Error>(err).log_err();
|
||||
|
||||
if let Some(workspace) = workspace {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
struct CopyPermalinkToLine;
|
||||
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopyPermalinkToLine>(),
|
||||
message,
|
||||
),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn copy_file_location(&mut self, _: &CopyFileLocation, cx: &mut ViewContext<Self>) {
|
||||
@@ -11586,29 +11576,41 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn open_permalink_to_line(&mut self, _: &OpenPermalinkToLine, cx: &mut ViewContext<Self>) {
|
||||
let permalink = self.get_permalink_to_line(cx);
|
||||
let permalink_task = self.get_permalink_to_line(cx);
|
||||
let workspace = self.workspace();
|
||||
|
||||
match permalink {
|
||||
Ok(permalink) => {
|
||||
cx.open_url(permalink.as_ref());
|
||||
}
|
||||
Err(err) => {
|
||||
let message = format!("Failed to open permalink: {err}");
|
||||
|
||||
Err::<(), anyhow::Error>(err).log_err();
|
||||
|
||||
if let Some(workspace) = self.workspace() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
struct OpenPermalinkToLine;
|
||||
|
||||
workspace.show_toast(
|
||||
Toast::new(NotificationId::unique::<OpenPermalinkToLine>(), message),
|
||||
cx,
|
||||
)
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
match permalink_task.await {
|
||||
Ok(permalink) => {
|
||||
cx.update(|cx| {
|
||||
cx.open_url(permalink.as_ref());
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(err) => {
|
||||
let message = format!("Failed to open permalink: {err}");
|
||||
|
||||
Err::<(), anyhow::Error>(err).log_err();
|
||||
|
||||
if let Some(workspace) = workspace {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
struct OpenPermalinkToLine;
|
||||
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<OpenPermalinkToLine>(),
|
||||
message,
|
||||
),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Adds a row highlight for the given range. If a row has multiple highlights, the
|
||||
@@ -12361,10 +12363,15 @@ impl Editor {
|
||||
|
||||
let proposed_changes_buffers = new_selections_by_buffer
|
||||
.into_iter()
|
||||
.map(|(buffer, ranges)| ProposedChangesBuffer { buffer, ranges })
|
||||
.map(|(buffer, ranges)| ProposedChangeLocation { buffer, ranges })
|
||||
.collect::<Vec<_>>();
|
||||
let proposed_changes_editor = cx.new_view(|cx| {
|
||||
ProposedChangesEditor::new(proposed_changes_buffers, self.project.clone(), cx)
|
||||
ProposedChangesEditor::new(
|
||||
"Proposed changes",
|
||||
proposed_changes_buffers,
|
||||
self.project.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.window_context().defer(move |cx| {
|
||||
|
||||
@@ -16,16 +16,24 @@ use workspace::{
|
||||
|
||||
pub struct ProposedChangesEditor {
|
||||
editor: View<Editor>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
multibuffer: Model<MultiBuffer>,
|
||||
title: SharedString,
|
||||
buffer_entries: Vec<BufferEntry>,
|
||||
_recalculate_diffs_task: Task<Option<()>>,
|
||||
recalculate_diffs_tx: mpsc::UnboundedSender<RecalculateDiff>,
|
||||
}
|
||||
|
||||
pub struct ProposedChangesBuffer<T> {
|
||||
pub struct ProposedChangeLocation<T> {
|
||||
pub buffer: Model<Buffer>,
|
||||
pub ranges: Vec<Range<T>>,
|
||||
}
|
||||
|
||||
struct BufferEntry {
|
||||
base: Model<Buffer>,
|
||||
branch: Model<Buffer>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
pub struct ProposedChangesEditorToolbar {
|
||||
current_editor: Option<View<ProposedChangesEditor>>,
|
||||
}
|
||||
@@ -43,32 +51,14 @@ struct BranchBufferSemanticsProvider(Rc<dyn SemanticsProvider>);
|
||||
|
||||
impl ProposedChangesEditor {
|
||||
pub fn new<T: ToOffset>(
|
||||
buffers: Vec<ProposedChangesBuffer<T>>,
|
||||
title: impl Into<SharedString>,
|
||||
locations: Vec<ProposedChangeLocation<T>>,
|
||||
project: Option<Model<Project>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let mut subscriptions = Vec::new();
|
||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite));
|
||||
|
||||
for buffer in buffers {
|
||||
let branch_buffer = buffer.buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
||||
subscriptions.push(cx.subscribe(&branch_buffer, Self::on_buffer_event));
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.push_excerpts(
|
||||
branch_buffer,
|
||||
buffer.ranges.into_iter().map(|range| ExcerptRange {
|
||||
context: range,
|
||||
primary: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let (recalculate_diffs_tx, mut recalculate_diffs_rx) = mpsc::unbounded();
|
||||
|
||||
Self {
|
||||
let mut this = Self {
|
||||
editor: cx.new_view(|cx| {
|
||||
let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, true, cx);
|
||||
editor.set_expand_all_diff_hunks();
|
||||
@@ -81,6 +71,9 @@ impl ProposedChangesEditor {
|
||||
);
|
||||
editor
|
||||
}),
|
||||
multibuffer,
|
||||
title: title.into(),
|
||||
buffer_entries: Vec::new(),
|
||||
recalculate_diffs_tx,
|
||||
_recalculate_diffs_task: cx.spawn(|_, mut cx| async move {
|
||||
let mut buffers_to_diff = HashSet::default();
|
||||
@@ -112,7 +105,100 @@ impl ProposedChangesEditor {
|
||||
}
|
||||
None
|
||||
}),
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
this.reset_locations(locations, cx);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn branch_buffer_for_base(&self, base_buffer: &Model<Buffer>) -> Option<Model<Buffer>> {
|
||||
self.buffer_entries.iter().find_map(|entry| {
|
||||
if &entry.base == base_buffer {
|
||||
Some(entry.branch.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_title(&mut self, title: SharedString, cx: &mut ViewContext<Self>) {
|
||||
self.title = title;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn reset_locations<T: ToOffset>(
|
||||
&mut self,
|
||||
locations: Vec<ProposedChangeLocation<T>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
// Undo all branch changes
|
||||
for entry in &self.buffer_entries {
|
||||
let base_version = entry.base.read(cx).version();
|
||||
entry.branch.update(cx, |buffer, cx| {
|
||||
let undo_counts = buffer
|
||||
.operations()
|
||||
.iter()
|
||||
.filter_map(|(timestamp, _)| {
|
||||
if !base_version.observed(*timestamp) {
|
||||
Some((*timestamp, u32::MAX))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
buffer.undo_operations(undo_counts, cx);
|
||||
});
|
||||
}
|
||||
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.clear(cx);
|
||||
});
|
||||
|
||||
let mut buffer_entries = Vec::new();
|
||||
for location in locations {
|
||||
let branch_buffer;
|
||||
if let Some(ix) = self
|
||||
.buffer_entries
|
||||
.iter()
|
||||
.position(|entry| entry.base == location.buffer)
|
||||
{
|
||||
let entry = self.buffer_entries.remove(ix);
|
||||
branch_buffer = entry.branch.clone();
|
||||
buffer_entries.push(entry);
|
||||
} else {
|
||||
branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
||||
buffer_entries.push(BufferEntry {
|
||||
branch: branch_buffer.clone(),
|
||||
base: location.buffer.clone(),
|
||||
_subscription: cx.subscribe(&branch_buffer, Self::on_buffer_event),
|
||||
});
|
||||
}
|
||||
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.push_excerpts(
|
||||
branch_buffer,
|
||||
location.ranges.into_iter().map(|range| ExcerptRange {
|
||||
context: range,
|
||||
primary: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
self.buffer_entries = buffer_entries;
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |selections| selections.refresh())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn recalculate_all_buffer_diffs(&self) {
|
||||
for (ix, entry) in self.buffer_entries.iter().enumerate().rev() {
|
||||
self.recalculate_diffs_tx
|
||||
.unbounded_send(RecalculateDiff {
|
||||
buffer: entry.branch.clone(),
|
||||
debounce: ix > 0,
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,11 +248,11 @@ impl Item for ProposedChangesEditor {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn tab_icon(&self, _cx: &ui::WindowContext) -> Option<Icon> {
|
||||
Some(Icon::new(IconName::Pencil))
|
||||
Some(Icon::new(IconName::Diff))
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
|
||||
Some("Proposed changes".into())
|
||||
Some(self.title.clone())
|
||||
}
|
||||
|
||||
fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
|
||||
@@ -434,12 +434,10 @@ impl<T> Clone for Model<T> {
|
||||
|
||||
impl<T> std::fmt::Debug for Model<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Model {{ entity_id: {:?}, entity_type: {:?} }}",
|
||||
self.any_model.entity_id,
|
||||
type_name::<T>()
|
||||
)
|
||||
f.debug_struct("Model")
|
||||
.field("entity_id", &self.any_model.entity_id)
|
||||
.field("entity_type", &type_name::<T>())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,7 +567,10 @@ pub struct WeakModel<T> {
|
||||
|
||||
impl<T> std::fmt::Debug for WeakModel<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct(type_name::<WeakModel<T>>()).finish()
|
||||
f.debug_struct(&type_name::<Self>())
|
||||
.field("entity_id", &self.any_model.entity_id)
|
||||
.field("entity_type", &type_name::<T>())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@ impl project::Item for ImageItem {
|
||||
.path
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.map(str::to_lowercase)
|
||||
.unwrap_or_default();
|
||||
let ext = ext.as_str();
|
||||
|
||||
// Only open the item if it's a binary image (no SVGs, etc.)
|
||||
// Since we do not have a way to toggle to an editor
|
||||
|
||||
@@ -20,6 +20,7 @@ use anyhow::{anyhow, Context, Result};
|
||||
use async_watch as watch;
|
||||
use clock::Lamport;
|
||||
pub use clock::ReplicaId;
|
||||
use collections::HashMap;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
AnyElement, AppContext, Context as _, EventEmitter, HighlightStyle, Model, ModelContext,
|
||||
@@ -910,10 +911,8 @@ impl Buffer {
|
||||
self.apply_ops([operation.clone()], cx);
|
||||
|
||||
if let Some(timestamp) = operation_to_undo {
|
||||
let operation = self
|
||||
.text
|
||||
.undo_operations([(timestamp, u32::MAX)].into_iter().collect());
|
||||
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||
let counts = [(timestamp, u32::MAX)].into_iter().collect();
|
||||
self.undo_operations(counts, cx);
|
||||
}
|
||||
|
||||
self.diff_base_version += 1;
|
||||
@@ -2331,6 +2330,18 @@ impl Buffer {
|
||||
undone
|
||||
}
|
||||
|
||||
pub fn undo_operations(
|
||||
&mut self,
|
||||
counts: HashMap<Lamport, u32>,
|
||||
cx: &mut ModelContext<Buffer>,
|
||||
) {
|
||||
let was_dirty = self.is_dirty();
|
||||
let operation = self.text.undo_operations(counts);
|
||||
let old_version = self.version.clone();
|
||||
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||
self.did_edit(&old_version, was_dirty, cx);
|
||||
}
|
||||
|
||||
/// Manually redoes a specific transaction in the buffer's redo history.
|
||||
pub fn redo(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
|
||||
let was_dirty = self.is_dirty();
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
use anthropic::AnthropicError;
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{
|
||||
Client, PerformCompletionParams, UserStore, EXPIRED_LLM_TOKEN_HEADER_NAME,
|
||||
zed_urls, Client, PerformCompletionParams, UserStore, EXPIRED_LLM_TOKEN_HEADER_NAME,
|
||||
MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME,
|
||||
};
|
||||
use collections::BTreeMap;
|
||||
@@ -905,7 +905,6 @@ impl ConfigurationView {
|
||||
impl Render for ConfigurationView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
const ZED_AI_URL: &str = "https://zed.dev/ai";
|
||||
const ACCOUNT_SETTINGS_URL: &str = "https://zed.dev/account";
|
||||
|
||||
let is_connected = !self.state.read(cx).is_signed_out();
|
||||
let plan = self.state.read(cx).user_store.read(cx).current_plan();
|
||||
@@ -922,7 +921,7 @@ impl Render for ConfigurationView {
|
||||
h_flex().child(
|
||||
Button::new("manage_settings", "Manage Subscription")
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.on_click(cx.listener(|_, _, cx| cx.open_url(ACCOUNT_SETTINGS_URL))),
|
||||
.on_click(cx.listener(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))),
|
||||
),
|
||||
)
|
||||
} else if cx.has_flag::<ZedPro>() {
|
||||
@@ -938,7 +937,9 @@ impl Render for ConfigurationView {
|
||||
Button::new("upgrade", "Upgrade")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.color(Color::Accent)
|
||||
.on_click(cx.listener(|_, _, cx| cx.open_url(ACCOUNT_SETTINGS_URL))),
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -9,7 +9,9 @@ license = "GPL-3.0-or-later"
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
test-support = [
|
||||
"tree-sitter"
|
||||
]
|
||||
load-grammars = [
|
||||
"tree-sitter-bash",
|
||||
"tree-sitter-c",
|
||||
@@ -75,6 +77,7 @@ tree-sitter-yaml = { workspace = true, optional = true }
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tree-sitter.workspace = true
|
||||
text.workspace = true
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name = "CSS"
|
||||
grammar = "css"
|
||||
path_suffixes = ["css"]
|
||||
path_suffixes = ["css", "postcss"]
|
||||
autoclose_before = ";:.,=}])>"
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
|
||||
@@ -101,10 +101,10 @@ pub fn logs_dir() -> &'static PathBuf {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the path to the zed server directory on this ssh host.
|
||||
/// Returns the path to the Zed server directory on this SSH host.
|
||||
pub fn remote_server_state_dir() -> &'static PathBuf {
|
||||
static REMOTE_SERVER_STATE: OnceLock<PathBuf> = OnceLock::new();
|
||||
REMOTE_SERVER_STATE.get_or_init(|| return support_dir().join("server_state"))
|
||||
REMOTE_SERVER_STATE.get_or_init(|| support_dir().join("server_state"))
|
||||
}
|
||||
|
||||
/// Returns the path to the `Zed.log` file.
|
||||
|
||||
@@ -69,6 +69,7 @@ snippet_provider.workspace = true
|
||||
terminal.workspace = true
|
||||
text.workspace = true
|
||||
util.workspace = true
|
||||
url.workspace = true
|
||||
which.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::{
|
||||
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
||||
Item, NoRepositoryError, ProjectPath,
|
||||
};
|
||||
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use client::Client;
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
@@ -23,7 +24,7 @@ use language::{
|
||||
};
|
||||
use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope};
|
||||
use smol::channel::Receiver;
|
||||
use std::{io, path::Path, str::FromStr as _, sync::Arc, time::Instant};
|
||||
use std::{io, ops::Range, path::Path, str::FromStr as _, sync::Arc, time::Instant};
|
||||
use text::BufferId;
|
||||
use util::{debug_panic, maybe, ResultExt as _, TryFutureExt};
|
||||
use worktree::{File, PathChange, ProjectEntryId, UpdatedGitRepositoriesSet, Worktree, WorktreeId};
|
||||
@@ -971,6 +972,7 @@ impl BufferStore {
|
||||
client.add_model_request_handler(Self::handle_save_buffer);
|
||||
client.add_model_request_handler(Self::handle_blame_buffer);
|
||||
client.add_model_request_handler(Self::handle_reload_buffers);
|
||||
client.add_model_request_handler(Self::handle_get_permalink_to_line);
|
||||
}
|
||||
|
||||
/// Creates a buffer store, optionally retaining its buffers.
|
||||
@@ -1170,6 +1172,78 @@ impl BufferStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_permalink_to_line(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
selection: Range<u32>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<url::Url>> {
|
||||
let buffer = buffer.read(cx);
|
||||
let Some(file) = File::from_dyn(buffer.file()) else {
|
||||
return Task::ready(Err(anyhow!("buffer has no file")));
|
||||
};
|
||||
|
||||
match file.worktree.clone().read(cx) {
|
||||
Worktree::Local(worktree) => {
|
||||
let Some(repo) = worktree.local_git_repo(file.path()) else {
|
||||
return Task::ready(Err(anyhow!("no repository for buffer found")));
|
||||
};
|
||||
|
||||
let path = file.path().clone();
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
const REMOTE_NAME: &str = "origin";
|
||||
let origin_url = repo
|
||||
.remote_url(REMOTE_NAME)
|
||||
.ok_or_else(|| anyhow!("remote \"{REMOTE_NAME}\" not found"))?;
|
||||
|
||||
let sha = repo
|
||||
.head_sha()
|
||||
.ok_or_else(|| anyhow!("failed to read HEAD SHA"))?;
|
||||
|
||||
let provider_registry =
|
||||
cx.update(GitHostingProviderRegistry::default_global)?;
|
||||
|
||||
let (provider, remote) =
|
||||
parse_git_remote_url(provider_registry, &origin_url)
|
||||
.ok_or_else(|| anyhow!("failed to parse Git remote URL"))?;
|
||||
|
||||
let path = path
|
||||
.to_str()
|
||||
.context("failed to convert buffer path to string")?;
|
||||
|
||||
Ok(provider.build_permalink(
|
||||
remote,
|
||||
BuildPermalinkParams {
|
||||
sha: &sha,
|
||||
path,
|
||||
selection: Some(selection),
|
||||
},
|
||||
))
|
||||
})
|
||||
}
|
||||
Worktree::Remote(worktree) => {
|
||||
let buffer_id = buffer.remote_id();
|
||||
let project_id = worktree.project_id();
|
||||
let client = worktree.client();
|
||||
cx.spawn(|_| async move {
|
||||
let response = client
|
||||
.request(proto::GetPermalinkToLine {
|
||||
project_id,
|
||||
buffer_id: buffer_id.into(),
|
||||
selection: Some(proto::Range {
|
||||
start: selection.start as u64,
|
||||
end: selection.end as u64,
|
||||
}),
|
||||
})
|
||||
.await?;
|
||||
|
||||
url::Url::parse(&response.permalink).context("failed to parse permalink")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_buffer(&mut self, buffer: Model<Buffer>, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||
let remote_id = buffer.read(cx).remote_id();
|
||||
let is_remote = buffer.read(cx).replica_id() != 0;
|
||||
@@ -1775,6 +1849,31 @@ impl BufferStore {
|
||||
Ok(serialize_blame_buffer_response(blame))
|
||||
}
|
||||
|
||||
pub async fn handle_get_permalink_to_line(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::GetPermalinkToLine>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::GetPermalinkToLineResponse> {
|
||||
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
|
||||
// let version = deserialize_version(&envelope.payload.version);
|
||||
let selection = {
|
||||
let proto_selection = envelope
|
||||
.payload
|
||||
.selection
|
||||
.context("no selection to get permalink for defined")?;
|
||||
proto_selection.start as u32..proto_selection.end as u32
|
||||
};
|
||||
let buffer = this.read_with(&cx, |this, _| this.get_existing(buffer_id))??;
|
||||
let permalink = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.get_permalink_to_line(&buffer, selection, cx)
|
||||
})?
|
||||
.await?;
|
||||
Ok(proto::GetPermalinkToLineResponse {
|
||||
permalink: permalink.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn wait_for_loading_buffer(
|
||||
mut receiver: postage::watch::Receiver<Option<Result<Model<Buffer>, Arc<anyhow::Error>>>>,
|
||||
) -> Result<Model<Buffer>, Arc<anyhow::Error>> {
|
||||
|
||||
@@ -1243,6 +1243,10 @@ impl Project {
|
||||
self.client.clone()
|
||||
}
|
||||
|
||||
pub fn ssh_client(&self) -> Option<Model<SshRemoteClient>> {
|
||||
self.ssh_client.clone()
|
||||
}
|
||||
|
||||
pub fn user_store(&self) -> Model<UserStore> {
|
||||
self.user_store.clone()
|
||||
}
|
||||
@@ -3463,6 +3467,17 @@ impl Project {
|
||||
self.buffer_store.read(cx).blame_buffer(buffer, version, cx)
|
||||
}
|
||||
|
||||
pub fn get_permalink_to_line(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
selection: Range<u32>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<url::Url>> {
|
||||
self.buffer_store
|
||||
.read(cx)
|
||||
.get_permalink_to_line(buffer, selection, cx)
|
||||
}
|
||||
|
||||
// RPC message handlers
|
||||
|
||||
async fn handle_unshare_project(
|
||||
|
||||
@@ -2701,7 +2701,6 @@ impl ProjectPanel {
|
||||
.cursor_default()
|
||||
.when(self.width.is_some(), |this| {
|
||||
this.children(Scrollbar::horizontal(
|
||||
//percentage as f32..end_offset as f32,
|
||||
self.horizontal_scrollbar_state.clone(),
|
||||
))
|
||||
}),
|
||||
@@ -2918,7 +2917,9 @@ impl Render for ProjectPanel {
|
||||
.track_scroll(self.scroll_handle.clone()),
|
||||
)
|
||||
.children(self.render_vertical_scrollbar(cx))
|
||||
.children(self.render_horizontal_scrollbar(cx))
|
||||
.when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| {
|
||||
this.pb_4().child(scrollbar)
|
||||
})
|
||||
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
|
||||
deferred(
|
||||
anchored()
|
||||
|
||||
@@ -12,6 +12,7 @@ message Envelope {
|
||||
uint32 id = 1;
|
||||
optional uint32 responding_to = 2;
|
||||
optional PeerId original_sender_id = 3;
|
||||
optional uint32 ack_id = 266;
|
||||
|
||||
oneof payload {
|
||||
Hello hello = 4;
|
||||
@@ -292,7 +293,12 @@ message Envelope {
|
||||
Toast toast = 261;
|
||||
HideToast hide_toast = 262;
|
||||
|
||||
OpenServerSettings open_server_settings = 263; // current max
|
||||
OpenServerSettings open_server_settings = 263;
|
||||
|
||||
GetPermalinkToLine get_permalink_to_line = 264;
|
||||
GetPermalinkToLineResponse get_permalink_to_line_response = 265;
|
||||
|
||||
FlushBufferedMessages flush_buffered_messages = 267;
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
@@ -2508,3 +2514,16 @@ message HideToast {
|
||||
message OpenServerSettings {
|
||||
uint64 project_id = 1;
|
||||
}
|
||||
|
||||
message GetPermalinkToLine {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
Range selection = 3;
|
||||
}
|
||||
|
||||
message GetPermalinkToLineResponse {
|
||||
string permalink = 1;
|
||||
}
|
||||
|
||||
message FlushBufferedMessages {}
|
||||
message FlushBufferedMessagesResponse {}
|
||||
|
||||
@@ -32,6 +32,7 @@ macro_rules! messages {
|
||||
responding_to,
|
||||
original_sender_id,
|
||||
payload: Some(envelope::Payload::$name(self)),
|
||||
ack_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -370,6 +370,9 @@ messages!(
|
||||
(Toast, Background),
|
||||
(HideToast, Background),
|
||||
(OpenServerSettings, Foreground),
|
||||
(GetPermalinkToLine, Foreground),
|
||||
(GetPermalinkToLineResponse, Foreground),
|
||||
(FlushBufferedMessages, Foreground),
|
||||
);
|
||||
|
||||
request_messages!(
|
||||
@@ -494,7 +497,9 @@ request_messages!(
|
||||
(CheckFileExists, CheckFileExistsResponse),
|
||||
(ShutdownRemoteServer, Ack),
|
||||
(RemoveWorktree, Ack),
|
||||
(OpenServerSettings, OpenBufferResponse)
|
||||
(OpenServerSettings, OpenBufferResponse),
|
||||
(GetPermalinkToLine, GetPermalinkToLineResponse),
|
||||
(FlushBufferedMessages, Ack),
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
@@ -571,7 +576,7 @@ entity_messages!(
|
||||
Toast,
|
||||
HideToast,
|
||||
OpenServerSettings,
|
||||
|
||||
GetPermalinkToLine,
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
|
||||
@@ -22,6 +22,7 @@ file_finder.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
ordered-float.workspace = true
|
||||
|
||||
@@ -13,6 +13,7 @@ use gpui::{
|
||||
Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
||||
Subscription, Task, View, ViewContext, WeakView,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{
|
||||
highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText},
|
||||
@@ -247,8 +248,9 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
SerializedWorkspaceLocation::Local(paths, order) => order
|
||||
.order()
|
||||
.iter()
|
||||
.filter_map(|i| paths.paths().get(*i))
|
||||
.map(|path| path.compact().to_string_lossy().into_owned())
|
||||
.zip(paths.paths().iter())
|
||||
.sorted_by_key(|(i, _)| *i)
|
||||
.map(|(_, path)| path.compact().to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>()
|
||||
.join(""),
|
||||
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
|
||||
@@ -447,8 +449,9 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
order
|
||||
.order()
|
||||
.iter()
|
||||
.filter_map(|i| paths.paths().get(*i).cloned())
|
||||
.map(|path| path.compact())
|
||||
.zip(paths.paths().iter())
|
||||
.sorted_by_key(|(i, _)| **i)
|
||||
.map(|(_, path)| path.compact())
|
||||
.collect(),
|
||||
),
|
||||
SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()),
|
||||
|
||||
@@ -175,7 +175,7 @@ impl Render for SshPrompt {
|
||||
.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.flex_wrap()
|
||||
.flex()
|
||||
.child(if self.error_message.is_some() {
|
||||
Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Medium)
|
||||
@@ -195,6 +195,7 @@ impl Render for SshPrompt {
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.ml_1()
|
||||
.text_ellipsis()
|
||||
.overflow_x_hidden()
|
||||
.when_some(self.error_message.as_ref(), |el, error| {
|
||||
@@ -205,7 +206,7 @@ impl Render for SshPrompt {
|
||||
|el| {
|
||||
el.child(
|
||||
Label::new(format!(
|
||||
"-{}…",
|
||||
"{}…",
|
||||
self.status_message.clone().unwrap()
|
||||
))
|
||||
.size(LabelSize::Small),
|
||||
|
||||
@@ -19,6 +19,7 @@ test-support = ["fs/test-support"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::{
|
||||
proxy::ProxyLaunchError,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
channel::{
|
||||
@@ -31,6 +32,7 @@ use smol::{
|
||||
};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::VecDeque,
|
||||
ffi::OsStr,
|
||||
fmt,
|
||||
ops::ControlFlow,
|
||||
@@ -276,7 +278,7 @@ async fn run_cmd(command: &mut process::Command) -> Result<String> {
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelForwarder {
|
||||
pub struct ChannelForwarder {
|
||||
quit_tx: UnboundedSender<()>,
|
||||
forwarding_task: Task<(UnboundedSender<Envelope>, UnboundedReceiver<Envelope>)>,
|
||||
}
|
||||
@@ -347,7 +349,7 @@ const MAX_RECONNECT_ATTEMPTS: usize = 3;
|
||||
enum State {
|
||||
Connecting,
|
||||
Connected {
|
||||
ssh_connection: SshRemoteConnection,
|
||||
ssh_connection: Box<dyn SshRemoteProcess>,
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
forwarder: ChannelForwarder,
|
||||
|
||||
@@ -357,7 +359,7 @@ enum State {
|
||||
HeartbeatMissed {
|
||||
missed_heartbeats: usize,
|
||||
|
||||
ssh_connection: SshRemoteConnection,
|
||||
ssh_connection: Box<dyn SshRemoteProcess>,
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
forwarder: ChannelForwarder,
|
||||
|
||||
@@ -366,7 +368,7 @@ enum State {
|
||||
},
|
||||
Reconnecting,
|
||||
ReconnectFailed {
|
||||
ssh_connection: SshRemoteConnection,
|
||||
ssh_connection: Box<dyn SshRemoteProcess>,
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
forwarder: ChannelForwarder,
|
||||
|
||||
@@ -392,11 +394,11 @@ impl fmt::Display for State {
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn ssh_connection(&self) -> Option<&SshRemoteConnection> {
|
||||
fn ssh_connection(&self) -> Option<&dyn SshRemoteProcess> {
|
||||
match self {
|
||||
Self::Connected { ssh_connection, .. } => Some(ssh_connection),
|
||||
Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection),
|
||||
Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection),
|
||||
Self::Connected { ssh_connection, .. } => Some(ssh_connection.as_ref()),
|
||||
Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection.as_ref()),
|
||||
Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection.as_ref()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -530,7 +532,8 @@ impl SshRemoteClient {
|
||||
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
|
||||
let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
|
||||
|
||||
let client = cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx))?;
|
||||
let client =
|
||||
cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?;
|
||||
let this = cx.new_model(|_| Self {
|
||||
client: client.clone(),
|
||||
unique_identifier: unique_identifier.clone(),
|
||||
@@ -541,23 +544,19 @@ impl SshRemoteClient {
|
||||
let (proxy, proxy_incoming_tx, proxy_outgoing_rx) =
|
||||
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
|
||||
|
||||
let (ssh_connection, ssh_proxy_process) = Self::establish_connection(
|
||||
let (ssh_connection, io_task) = Self::establish_connection(
|
||||
unique_identifier,
|
||||
false,
|
||||
connection_options,
|
||||
proxy_incoming_tx,
|
||||
proxy_outgoing_rx,
|
||||
connection_activity_tx,
|
||||
delegate.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let multiplex_task = Self::multiplex(
|
||||
this.downgrade(),
|
||||
ssh_proxy_process,
|
||||
proxy_incoming_tx,
|
||||
proxy_outgoing_rx,
|
||||
connection_activity_tx,
|
||||
&mut cx,
|
||||
);
|
||||
let multiplex_task = Self::monitor(this.downgrade(), io_task, &cx);
|
||||
|
||||
if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await {
|
||||
log::error!("failed to establish connection: {}", error);
|
||||
@@ -703,30 +702,24 @@ impl SshRemoteClient {
|
||||
};
|
||||
}
|
||||
|
||||
if let Err(error) = ssh_connection.master_process.kill() {
|
||||
if let Err(error) = ssh_connection.kill().await.context("Failed to kill ssh process") {
|
||||
failed!(error, attempts, ssh_connection, delegate, forwarder);
|
||||
};
|
||||
|
||||
if let Err(error) = ssh_connection
|
||||
.master_process
|
||||
.status()
|
||||
.await
|
||||
.context("Failed to kill ssh process")
|
||||
{
|
||||
failed!(error, attempts, ssh_connection, delegate, forwarder);
|
||||
}
|
||||
|
||||
let connection_options = ssh_connection.socket.connection_options.clone();
|
||||
let connection_options = ssh_connection.connection_options();
|
||||
|
||||
let (incoming_tx, outgoing_rx) = forwarder.into_channels().await;
|
||||
let (forwarder, proxy_incoming_tx, proxy_outgoing_rx) =
|
||||
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
|
||||
let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
|
||||
|
||||
let (ssh_connection, ssh_process) = match Self::establish_connection(
|
||||
let (ssh_connection, io_task) = match Self::establish_connection(
|
||||
identifier,
|
||||
true,
|
||||
connection_options,
|
||||
proxy_incoming_tx,
|
||||
proxy_outgoing_rx,
|
||||
connection_activity_tx,
|
||||
delegate.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
@@ -738,16 +731,9 @@ impl SshRemoteClient {
|
||||
}
|
||||
};
|
||||
|
||||
let multiplex_task = Self::multiplex(
|
||||
this.clone(),
|
||||
ssh_process,
|
||||
proxy_incoming_tx,
|
||||
proxy_outgoing_rx,
|
||||
connection_activity_tx,
|
||||
&mut cx,
|
||||
);
|
||||
let multiplex_task = Self::monitor(this.clone(), io_task, &cx);
|
||||
|
||||
if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await {
|
||||
if let Err(error) = client.resync(HEARTBEAT_TIMEOUT).await {
|
||||
failed!(error, attempts, ssh_connection, delegate, forwarder);
|
||||
};
|
||||
|
||||
@@ -798,7 +784,7 @@ impl SshRemoteClient {
|
||||
cx.emit(SshRemoteEvent::Disconnected);
|
||||
Ok(())
|
||||
} else {
|
||||
log::debug!("State has transition from Reconnecting into new state while attempting reconnect. Ignoring new state.");
|
||||
log::debug!("State has transition from Reconnecting into new state while attempting reconnect.");
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
@@ -832,7 +818,15 @@ impl SshRemoteClient {
|
||||
log::warn!("ssh heartbeat: connection activity channel has been dropped. stopping.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
keepalive_timer.set(cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse());
|
||||
|
||||
if missed_heartbeats != 0 {
|
||||
missed_heartbeats = 0;
|
||||
this.update(&mut cx, |this, mut cx| {
|
||||
this.handle_heartbeat_result(missed_heartbeats, &mut cx)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
_ = keepalive_timer => {
|
||||
log::debug!("Sending heartbeat to server...");
|
||||
@@ -845,6 +839,7 @@ impl SshRemoteClient {
|
||||
ping_result
|
||||
}
|
||||
};
|
||||
|
||||
if result.is_err() {
|
||||
missed_heartbeats += 1;
|
||||
log::warn!(
|
||||
@@ -902,18 +897,17 @@ impl SshRemoteClient {
|
||||
}
|
||||
|
||||
fn multiplex(
|
||||
this: WeakModel<Self>,
|
||||
mut ssh_proxy_process: Child,
|
||||
incoming_tx: UnboundedSender<Envelope>,
|
||||
mut outgoing_rx: UnboundedReceiver<Envelope>,
|
||||
mut connection_activity_tx: Sender<()>,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Task<Result<()>> {
|
||||
) -> Task<Result<Option<i32>>> {
|
||||
let mut child_stderr = ssh_proxy_process.stderr.take().unwrap();
|
||||
let mut child_stdout = ssh_proxy_process.stdout.take().unwrap();
|
||||
let mut child_stdin = ssh_proxy_process.stdin.take().unwrap();
|
||||
|
||||
let io_task = cx.background_executor().spawn(async move {
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut stdin_buffer = Vec::new();
|
||||
let mut stdout_buffer = Vec::new();
|
||||
let mut stderr_buffer = Vec::new();
|
||||
@@ -992,8 +986,14 @@ impl SshRemoteClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn monitor(
|
||||
this: WeakModel<Self>,
|
||||
io_task: Task<Result<Option<i32>>>,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let result = io_task.await;
|
||||
|
||||
@@ -1052,21 +1052,40 @@ impl SshRemoteClient {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn establish_connection(
|
||||
unique_identifier: String,
|
||||
reconnect: bool,
|
||||
connection_options: SshConnectionOptions,
|
||||
proxy_incoming_tx: UnboundedSender<Envelope>,
|
||||
proxy_outgoing_rx: UnboundedReceiver<Envelope>,
|
||||
connection_activity_tx: Sender<()>,
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<(SshRemoteConnection, Child)> {
|
||||
) -> Result<(Box<dyn SshRemoteProcess>, Task<Result<Option<i32>>>)> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if let Some(fake) = fake::SshRemoteConnection::new(&connection_options) {
|
||||
let io_task = fake::SshRemoteConnection::multiplex(
|
||||
fake.connection_options(),
|
||||
proxy_incoming_tx,
|
||||
proxy_outgoing_rx,
|
||||
connection_activity_tx,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
return Ok((fake, io_task));
|
||||
}
|
||||
|
||||
let ssh_connection =
|
||||
SshRemoteConnection::new(connection_options, delegate.clone(), cx).await?;
|
||||
|
||||
let platform = ssh_connection.query_platform().await?;
|
||||
let remote_binary_path = delegate.remote_server_binary_path(platform, cx)?;
|
||||
ssh_connection
|
||||
.ensure_server_binary(&delegate, &remote_binary_path, platform, cx)
|
||||
.await?;
|
||||
if !reconnect {
|
||||
ssh_connection
|
||||
.ensure_server_binary(&delegate, &remote_binary_path, platform, cx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let socket = ssh_connection.socket.clone();
|
||||
run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?;
|
||||
@@ -1091,7 +1110,15 @@ impl SshRemoteClient {
|
||||
.spawn()
|
||||
.context("failed to spawn remote server")?;
|
||||
|
||||
Ok((ssh_connection, ssh_proxy_process))
|
||||
let io_task = Self::multiplex(
|
||||
ssh_proxy_process,
|
||||
proxy_incoming_tx,
|
||||
proxy_outgoing_rx,
|
||||
connection_activity_tx,
|
||||
&cx,
|
||||
);
|
||||
|
||||
Ok((Box::new(ssh_connection), io_task))
|
||||
}
|
||||
|
||||
pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) {
|
||||
@@ -1103,7 +1130,7 @@ impl SshRemoteClient {
|
||||
.lock()
|
||||
.as_ref()
|
||||
.and_then(|state| state.ssh_connection())
|
||||
.map(|ssh_connection| ssh_connection.socket.ssh_args())
|
||||
.map(|ssh_connection| ssh_connection.ssh_args())
|
||||
}
|
||||
|
||||
pub fn proto_client(&self) -> AnyProtoClient {
|
||||
@@ -1118,7 +1145,6 @@ impl SshRemoteClient {
|
||||
self.connection_options.clone()
|
||||
}
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
pub fn connection_state(&self) -> ConnectionState {
|
||||
self.state
|
||||
.lock()
|
||||
@@ -1127,37 +1153,69 @@ impl SshRemoteClient {
|
||||
.unwrap_or(ConnectionState::Disconnected)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn connection_state(&self) -> ConnectionState {
|
||||
ConnectionState::Connected
|
||||
}
|
||||
|
||||
pub fn is_disconnected(&self) -> bool {
|
||||
self.connection_state() == ConnectionState::Disconnected
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn fake(
|
||||
client_cx: &mut gpui::TestAppContext,
|
||||
pub fn simulate_disconnect(&self, cx: &mut AppContext) -> Task<()> {
|
||||
use gpui::BorrowAppContext;
|
||||
|
||||
let port = self.connection_options().port.unwrap();
|
||||
|
||||
let disconnect =
|
||||
cx.update_global(|c: &mut fake::GlobalConnections, _cx| c.take(port).into_channels());
|
||||
cx.spawn(|mut cx| async move {
|
||||
let (input_rx, output_tx) = disconnect.await;
|
||||
let (forwarder, _, _) = ChannelForwarder::new(input_rx, output_tx, &mut cx);
|
||||
cx.update_global(|c: &mut fake::GlobalConnections, _cx| c.replace(port, forwarder))
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn fake_server(
|
||||
server_cx: &mut gpui::TestAppContext,
|
||||
) -> (Model<Self>, Arc<ChannelClient>) {
|
||||
use gpui::Context;
|
||||
) -> (ChannelForwarder, Arc<ChannelClient>) {
|
||||
server_cx.update(|cx| {
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
|
||||
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
|
||||
|
||||
let (server_to_client_tx, server_to_client_rx) = mpsc::unbounded();
|
||||
let (client_to_server_tx, client_to_server_rx) = mpsc::unbounded();
|
||||
// We use the forwarder on the server side (in production we only use one on the client side)
|
||||
// the idea is that we can simulate a disconnect/reconnect by just messing with the forwarder.
|
||||
let (forwarder, _, _) =
|
||||
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx.to_async());
|
||||
|
||||
(
|
||||
client_cx.update(|cx| {
|
||||
let client = ChannelClient::new(server_to_client_rx, client_to_server_tx, cx);
|
||||
cx.new_model(|_| Self {
|
||||
client,
|
||||
unique_identifier: "fake".to_string(),
|
||||
connection_options: SshConnectionOptions::default(),
|
||||
state: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
}),
|
||||
server_cx.update(|cx| ChannelClient::new(client_to_server_rx, server_to_client_tx, cx)),
|
||||
)
|
||||
let client = ChannelClient::new(incoming_rx, outgoing_tx, cx, "fake-server");
|
||||
(forwarder, client)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn fake_client(
|
||||
forwarder: ChannelForwarder,
|
||||
client_cx: &mut gpui::TestAppContext,
|
||||
) -> Model<Self> {
|
||||
use gpui::BorrowAppContext;
|
||||
client_cx
|
||||
.update(|cx| {
|
||||
let port = cx.update_default_global(|c: &mut fake::GlobalConnections, _cx| {
|
||||
c.push(forwarder)
|
||||
});
|
||||
|
||||
Self::new(
|
||||
"fake".to_string(),
|
||||
SshConnectionOptions {
|
||||
host: "<fake>".to_string(),
|
||||
port: Some(port),
|
||||
..Default::default()
|
||||
},
|
||||
Arc::new(fake::Delegate),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1167,6 +1225,13 @@ impl From<SshRemoteClient> for AnyProtoClient {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
trait SshRemoteProcess: Send + Sync {
|
||||
async fn kill(&mut self) -> Result<()>;
|
||||
fn ssh_args(&self) -> Vec<String>;
|
||||
fn connection_options(&self) -> SshConnectionOptions;
|
||||
}
|
||||
|
||||
struct SshRemoteConnection {
|
||||
socket: SshSocket,
|
||||
master_process: process::Child,
|
||||
@@ -1181,6 +1246,25 @@ impl Drop for SshRemoteConnection {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SshRemoteProcess for SshRemoteConnection {
|
||||
async fn kill(&mut self) -> Result<()> {
|
||||
self.master_process.kill()?;
|
||||
|
||||
self.master_process.status().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ssh_args(&self) -> Vec<String> {
|
||||
self.socket.ssh_args()
|
||||
}
|
||||
|
||||
fn connection_options(&self) -> SshConnectionOptions {
|
||||
self.socket.connection_options.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl SshRemoteConnection {
|
||||
#[cfg(not(unix))]
|
||||
async fn new(
|
||||
@@ -1312,7 +1396,10 @@ impl SshRemoteConnection {
|
||||
let mut stderr = master_process.stderr.take().unwrap();
|
||||
stderr.read_to_end(&mut output).await?;
|
||||
|
||||
let error_message = format!("failed to connect: {}", String::from_utf8_lossy(&output));
|
||||
let error_message = format!(
|
||||
"failed to connect: {}",
|
||||
String::from_utf8_lossy(&output).trim()
|
||||
);
|
||||
delegate.set_error(error_message.clone(), cx);
|
||||
Err(anyhow!(error_message))?;
|
||||
}
|
||||
@@ -1373,14 +1460,14 @@ impl SshRemoteConnection {
|
||||
let server_mode = 0o755;
|
||||
|
||||
let t0 = Instant::now();
|
||||
delegate.set_status(Some("uploading remote development server"), cx);
|
||||
delegate.set_status(Some("Uploading remote development server"), cx);
|
||||
log::info!("uploading remote development server ({}kb)", size / 1024);
|
||||
self.upload_file(&src_path, &dst_path_gz)
|
||||
.await
|
||||
.context("failed to upload server binary")?;
|
||||
log::info!("uploaded remote development server in {:?}", t0.elapsed());
|
||||
|
||||
delegate.set_status(Some("extracting remote development server"), cx);
|
||||
delegate.set_status(Some("Extracting remote development server"), cx);
|
||||
run_cmd(
|
||||
self.socket
|
||||
.ssh_command("gunzip")
|
||||
@@ -1389,7 +1476,7 @@ impl SshRemoteConnection {
|
||||
)
|
||||
.await?;
|
||||
|
||||
delegate.set_status(Some("unzipping remote development server"), cx);
|
||||
delegate.set_status(Some("Marking remote development server executable"), cx);
|
||||
run_cmd(
|
||||
self.socket
|
||||
.ssh_command("chmod")
|
||||
@@ -1460,8 +1547,10 @@ type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, ones
|
||||
pub struct ChannelClient {
|
||||
next_message_id: AtomicU32,
|
||||
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
||||
response_channels: ResponseChannels, // Lock
|
||||
message_handlers: Mutex<ProtoMessageHandlerSet>, // Lock
|
||||
buffer: Mutex<VecDeque<Envelope>>,
|
||||
response_channels: ResponseChannels,
|
||||
message_handlers: Mutex<ProtoMessageHandlerSet>,
|
||||
max_received: AtomicU32,
|
||||
}
|
||||
|
||||
impl ChannelClient {
|
||||
@@ -1469,15 +1558,18 @@ impl ChannelClient {
|
||||
incoming_rx: mpsc::UnboundedReceiver<Envelope>,
|
||||
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
||||
cx: &AppContext,
|
||||
name: &'static str,
|
||||
) -> Arc<Self> {
|
||||
let this = Arc::new(Self {
|
||||
outgoing_tx,
|
||||
next_message_id: AtomicU32::new(0),
|
||||
max_received: AtomicU32::new(0),
|
||||
response_channels: ResponseChannels::default(),
|
||||
message_handlers: Default::default(),
|
||||
buffer: Mutex::new(VecDeque::new()),
|
||||
});
|
||||
|
||||
Self::start_handling_messages(this.clone(), incoming_rx, cx);
|
||||
Self::start_handling_messages(this.clone(), incoming_rx, cx, name);
|
||||
|
||||
this
|
||||
}
|
||||
@@ -1486,6 +1578,7 @@ impl ChannelClient {
|
||||
this: Arc<Self>,
|
||||
mut incoming_rx: mpsc::UnboundedReceiver<Envelope>,
|
||||
cx: &AppContext,
|
||||
name: &'static str,
|
||||
) {
|
||||
cx.spawn(|cx| {
|
||||
let this = Arc::downgrade(&this);
|
||||
@@ -1495,6 +1588,26 @@ impl ChannelClient {
|
||||
let Some(this) = this.upgrade() else {
|
||||
return anyhow::Ok(());
|
||||
};
|
||||
if let Some(ack_id) = incoming.ack_id {
|
||||
let mut buffer = this.buffer.lock();
|
||||
while buffer.front().is_some_and(|msg| msg.id <= ack_id) {
|
||||
buffer.pop_front();
|
||||
}
|
||||
}
|
||||
if let Some(proto::envelope::Payload::FlushBufferedMessages(_)) = &incoming.payload {
|
||||
{
|
||||
let buffer = this.buffer.lock();
|
||||
for envelope in buffer.iter() {
|
||||
this.outgoing_tx.unbounded_send(envelope.clone()).ok();
|
||||
}
|
||||
}
|
||||
let mut envelope = proto::Ack{}.into_envelope(0, Some(incoming.id), None);
|
||||
envelope.id = this.next_message_id.fetch_add(1, SeqCst);
|
||||
this.outgoing_tx.unbounded_send(envelope)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
this.max_received.store(incoming.id, SeqCst);
|
||||
|
||||
if let Some(request_id) = incoming.responding_to {
|
||||
let request_id = MessageId(request_id);
|
||||
@@ -1516,22 +1629,22 @@ impl ChannelClient {
|
||||
this.clone().into(),
|
||||
cx.clone(),
|
||||
) {
|
||||
log::debug!("ssh message received. name:{type_name}");
|
||||
log::debug!("{name}:ssh message received. name:{type_name}");
|
||||
cx.foreground_executor().spawn(async move {
|
||||
match future.await {
|
||||
Ok(_) => {
|
||||
log::debug!("ssh message handled. name:{type_name}");
|
||||
log::debug!("{name}:ssh message handled. name:{type_name}");
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
"error handling message. type:{type_name}, error:{error}",
|
||||
"{name}:error handling message. type:{type_name}, error:{error}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
|
||||
} else {
|
||||
log::error!("unhandled ssh message name:{type_name}");
|
||||
log::error!("{name}:unhandled ssh message name:{type_name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1574,6 +1687,23 @@ impl ChannelClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resync(&self, timeout: Duration) -> Result<()> {
|
||||
smol::future::or(
|
||||
async {
|
||||
self.request(proto::FlushBufferedMessages {}).await?;
|
||||
for envelope in self.buffer.lock().iter() {
|
||||
self.outgoing_tx.unbounded_send(envelope.clone()).ok();
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
async {
|
||||
smol::Timer::after(timeout).await;
|
||||
Err(anyhow!("Timeout detected"))
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn ping(&self, timeout: Duration) -> Result<()> {
|
||||
smol::future::or(
|
||||
async {
|
||||
@@ -1603,7 +1733,8 @@ impl ChannelClient {
|
||||
let mut response_channels_lock = self.response_channels.lock();
|
||||
response_channels_lock.insert(MessageId(envelope.id), tx);
|
||||
drop(response_channels_lock);
|
||||
let result = self.outgoing_tx.unbounded_send(envelope);
|
||||
|
||||
let result = self.send_buffered(envelope);
|
||||
async move {
|
||||
if let Err(error) = &result {
|
||||
log::error!("failed to send message: {}", error);
|
||||
@@ -1620,6 +1751,12 @@ impl ChannelClient {
|
||||
|
||||
pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> {
|
||||
envelope.id = self.next_message_id.fetch_add(1, SeqCst);
|
||||
self.send_buffered(envelope)
|
||||
}
|
||||
|
||||
pub fn send_buffered(&self, mut envelope: proto::Envelope) -> Result<()> {
|
||||
envelope.ack_id = Some(self.max_received.load(SeqCst));
|
||||
self.buffer.lock().push_back(envelope.clone());
|
||||
self.outgoing_tx.unbounded_send(envelope)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1650,3 +1787,165 @@ impl ProtoClient for ChannelClient {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
mod fake {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use futures::{
|
||||
channel::{
|
||||
mpsc::{self, Sender},
|
||||
oneshot,
|
||||
},
|
||||
select_biased, FutureExt, SinkExt, StreamExt,
|
||||
};
|
||||
use gpui::{AsyncAppContext, BorrowAppContext, Global, SemanticVersion, Task};
|
||||
use rpc::proto::Envelope;
|
||||
|
||||
use super::{
|
||||
ChannelForwarder, SshClientDelegate, SshConnectionOptions, SshPlatform, SshRemoteProcess,
|
||||
};
|
||||
|
||||
pub(super) struct SshRemoteConnection {
|
||||
connection_options: SshConnectionOptions,
|
||||
}
|
||||
|
||||
impl SshRemoteConnection {
|
||||
pub(super) fn new(
|
||||
connection_options: &SshConnectionOptions,
|
||||
) -> Option<Box<dyn SshRemoteProcess>> {
|
||||
if connection_options.host == "<fake>" {
|
||||
return Some(Box::new(Self {
|
||||
connection_options: connection_options.clone(),
|
||||
}));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
pub(super) async fn multiplex(
|
||||
connection_options: SshConnectionOptions,
|
||||
mut client_tx: mpsc::UnboundedSender<Envelope>,
|
||||
mut client_rx: mpsc::UnboundedReceiver<Envelope>,
|
||||
mut connection_activity_tx: Sender<()>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Task<Result<Option<i32>>> {
|
||||
let (server_tx, server_rx) = cx
|
||||
.update(|cx| {
|
||||
cx.update_global(|conns: &mut GlobalConnections, _| {
|
||||
conns.take(connection_options.port.unwrap())
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.into_channels()
|
||||
.await;
|
||||
|
||||
let (forwarder, mut proxy_tx, mut proxy_rx) =
|
||||
ChannelForwarder::new(server_tx, server_rx, cx);
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global(|conns: &mut GlobalConnections, _| {
|
||||
conns.replace(connection_options.port.unwrap(), forwarder)
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
loop {
|
||||
select_biased! {
|
||||
server_to_client = proxy_rx.next().fuse() => {
|
||||
let Some(server_to_client) = server_to_client else {
|
||||
return Ok(Some(1))
|
||||
};
|
||||
connection_activity_tx.try_send(()).ok();
|
||||
client_tx.send(server_to_client).await.ok();
|
||||
}
|
||||
client_to_server = client_rx.next().fuse() => {
|
||||
let Some(client_to_server) = client_to_server else {
|
||||
return Ok(None)
|
||||
};
|
||||
proxy_tx.send(client_to_server).await.ok();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SshRemoteProcess for SshRemoteConnection {
|
||||
async fn kill(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ssh_args(&self) -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn connection_options(&self) -> SshConnectionOptions {
|
||||
self.connection_options.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct GlobalConnections(Vec<Option<ChannelForwarder>>);
|
||||
impl Global for GlobalConnections {}
|
||||
|
||||
impl GlobalConnections {
|
||||
pub(super) fn push(&mut self, forwarder: ChannelForwarder) -> u16 {
|
||||
self.0.push(Some(forwarder));
|
||||
self.0.len() as u16 - 1
|
||||
}
|
||||
|
||||
pub(super) fn take(&mut self, port: u16) -> ChannelForwarder {
|
||||
self.0
|
||||
.get_mut(port as usize)
|
||||
.expect("no fake server for port")
|
||||
.take()
|
||||
.expect("fake server is already borrowed")
|
||||
}
|
||||
pub(super) fn replace(&mut self, port: u16, forwarder: ChannelForwarder) {
|
||||
let ret = self
|
||||
.0
|
||||
.get_mut(port as usize)
|
||||
.expect("no fake server for port")
|
||||
.replace(forwarder);
|
||||
if ret.is_some() {
|
||||
panic!("fake server is already replaced");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct Delegate;
|
||||
|
||||
impl SshClientDelegate for Delegate {
|
||||
fn ask_password(
|
||||
&self,
|
||||
_: String,
|
||||
_: &mut AsyncAppContext,
|
||||
) -> oneshot::Receiver<Result<String>> {
|
||||
unreachable!()
|
||||
}
|
||||
fn remote_server_binary_path(
|
||||
&self,
|
||||
_: SshPlatform,
|
||||
_: &mut AsyncAppContext,
|
||||
) -> Result<PathBuf> {
|
||||
unreachable!()
|
||||
}
|
||||
fn get_server_binary(
|
||||
&self,
|
||||
_: SshPlatform,
|
||||
_: &mut AsyncAppContext,
|
||||
) -> oneshot::Receiver<Result<(PathBuf, SemanticVersion)>> {
|
||||
unreachable!()
|
||||
}
|
||||
fn set_status(&self, _: Option<&str>, _: &mut AsyncAppContext) {
|
||||
unreachable!()
|
||||
}
|
||||
fn set_error(&self, _: String, _: &mut AsyncAppContext) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ client.workspace = true
|
||||
env_logger.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
git.workspace = true
|
||||
git_hosting_providers.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
|
||||
@@ -154,6 +154,7 @@ impl HeadlessProject {
|
||||
client.add_request_handler(cx.weak_model(), Self::handle_remove_worktree);
|
||||
|
||||
client.add_model_request_handler(Self::handle_open_buffer_by_path);
|
||||
client.add_model_request_handler(Self::handle_open_new_buffer);
|
||||
client.add_model_request_handler(Self::handle_find_search_candidates);
|
||||
client.add_model_request_handler(Self::handle_open_server_settings);
|
||||
|
||||
@@ -363,6 +364,32 @@ impl HeadlessProject {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle_open_new_buffer(
|
||||
this: Model<Self>,
|
||||
_message: TypedEnvelope<proto::OpenNewBuffer>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::OpenBufferResponse> {
|
||||
let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
|
||||
let buffer_store = this.buffer_store.clone();
|
||||
let buffer = this
|
||||
.buffer_store
|
||||
.update(cx, |buffer_store, cx| buffer_store.create_buffer(cx));
|
||||
anyhow::Ok((buffer_store, buffer))
|
||||
})??;
|
||||
|
||||
let buffer = buffer.await?;
|
||||
let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
|
||||
buffer_store.update(&mut cx, |buffer_store, cx| {
|
||||
buffer_store
|
||||
.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})?;
|
||||
|
||||
Ok(proto::OpenBufferResponse {
|
||||
buffer_id: buffer_id.to_proto(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle_open_server_settings(
|
||||
this: Model<Self>,
|
||||
_: TypedEnvelope<proto::OpenServerSettings>,
|
||||
|
||||
@@ -641,6 +641,47 @@ async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut Test
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 20)]
|
||||
async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||
let (project, _headless, fs) = init_test(cx, server_cx).await;
|
||||
|
||||
let (worktree, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree("/code/project1", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
|
||||
let ix = buffer.text().find('1').unwrap();
|
||||
buffer.edit([(ix..ix + 1, "100")], None, cx);
|
||||
});
|
||||
|
||||
let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
|
||||
client
|
||||
.update(cx, |client, cx| client.simulate_disconnect(cx))
|
||||
.detach();
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
|
||||
"fn one() -> usize { 100 }"
|
||||
);
|
||||
}
|
||||
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::try_init().ok();
|
||||
@@ -651,9 +692,9 @@ async fn init_test(
|
||||
cx: &mut TestAppContext,
|
||||
server_cx: &mut TestAppContext,
|
||||
) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
|
||||
let (ssh_remote_client, ssh_server_client) = SshRemoteClient::fake(cx, server_cx);
|
||||
init_logger();
|
||||
|
||||
let (forwarder, ssh_server_client) = SshRemoteClient::fake_server(server_cx);
|
||||
let fs = FakeFs::new(server_cx.executor());
|
||||
fs.insert_tree(
|
||||
"/code",
|
||||
@@ -694,8 +735,9 @@ async fn init_test(
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let project = build_project(ssh_remote_client, cx);
|
||||
|
||||
let ssh = SshRemoteClient::fake_client(forwarder, cx).await;
|
||||
let project = build_project(ssh, cx);
|
||||
project
|
||||
.update(cx, {
|
||||
let headless = headless.clone();
|
||||
|
||||
@@ -5,6 +5,7 @@ use client::ProxySettings;
|
||||
use fs::{Fs, RealFs};
|
||||
use futures::channel::mpsc;
|
||||
use futures::{select, select_biased, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt};
|
||||
use git::GitHostingProviderRegistry;
|
||||
use gpui::{AppContext, Context as _, ModelContext, UpdateGlobal as _};
|
||||
use http_client::{read_proxy_from_env, Uri};
|
||||
use language::LanguageRegistry;
|
||||
@@ -269,7 +270,7 @@ fn start_server(
|
||||
})
|
||||
.detach();
|
||||
|
||||
ChannelClient::new(incoming_rx, outgoing_tx, cx)
|
||||
ChannelClient::new(incoming_rx, outgoing_tx, cx, "server")
|
||||
}
|
||||
|
||||
fn init_paths() -> anyhow::Result<()> {
|
||||
@@ -313,6 +314,8 @@ pub fn execute_run(
|
||||
let listeners = ServerListeners::new(stdin_socket, stdout_socket, stderr_socket)?;
|
||||
|
||||
log::info!("starting headless gpui app");
|
||||
|
||||
let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new());
|
||||
gpui::App::headless().run(move |cx| {
|
||||
settings::init(cx);
|
||||
HeadlessProject::init(cx);
|
||||
@@ -322,6 +325,9 @@ pub fn execute_run(
|
||||
|
||||
client::init_settings(cx);
|
||||
|
||||
GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx);
|
||||
git_hosting_providers::init(cx);
|
||||
|
||||
let project = cx.new_model(|cx| {
|
||||
let fs = Arc::new(RealFs::new(Default::default(), None));
|
||||
let node_settings_rx = initialize_settings(session.clone(), fs.clone(), cx);
|
||||
|
||||
@@ -44,8 +44,12 @@ impl ReqwestClient {
|
||||
let mut client = reqwest::Client::builder()
|
||||
.use_rustls_tls()
|
||||
.default_headers(map);
|
||||
if let Some(proxy) = proxy.clone() {
|
||||
client = client.proxy(reqwest::Proxy::all(proxy.to_string())?);
|
||||
if let Some(proxy) = proxy.clone().and_then(|proxy_uri| {
|
||||
reqwest::Proxy::all(proxy_uri.to_string())
|
||||
.inspect_err(|e| log::error!("Failed to parse proxy URI {}: {}", proxy_uri, e))
|
||||
.ok()
|
||||
}) {
|
||||
client = client.proxy(proxy);
|
||||
}
|
||||
let client = client.build()?;
|
||||
let mut client: ReqwestClient = client.into();
|
||||
@@ -232,3 +236,47 @@ impl http_client::HttpClient for ReqwestClient {
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use http_client::{http, HttpClient};
|
||||
|
||||
use crate::ReqwestClient;
|
||||
|
||||
#[test]
|
||||
fn test_proxy_uri() {
|
||||
let client = ReqwestClient::new();
|
||||
assert_eq!(client.proxy(), None);
|
||||
|
||||
let proxy = http::Uri::from_static("http://localhost:10809");
|
||||
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
|
||||
assert_eq!(client.proxy(), Some(&proxy));
|
||||
|
||||
let proxy = http::Uri::from_static("https://localhost:10809");
|
||||
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
|
||||
assert_eq!(client.proxy(), Some(&proxy));
|
||||
|
||||
let proxy = http::Uri::from_static("socks4://localhost:10808");
|
||||
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
|
||||
assert_eq!(client.proxy(), Some(&proxy));
|
||||
|
||||
let proxy = http::Uri::from_static("socks4a://localhost:10808");
|
||||
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
|
||||
assert_eq!(client.proxy(), Some(&proxy));
|
||||
|
||||
let proxy = http::Uri::from_static("socks5://localhost:10808");
|
||||
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
|
||||
assert_eq!(client.proxy(), Some(&proxy));
|
||||
|
||||
let proxy = http::Uri::from_static("socks5h://localhost:10808");
|
||||
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
|
||||
assert_eq!(client.proxy(), Some(&proxy));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_invalid_proxy_uri() {
|
||||
let proxy = http::Uri::from_static("file:///etc/hosts");
|
||||
ReqwestClient::proxy_and_user_agent(Some(proxy), "test").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ pub struct TerminalSettingsContent {
|
||||
pub alternate_scroll: Option<AlternateScroll>,
|
||||
/// Sets whether the option key behaves as the meta key.
|
||||
///
|
||||
/// Default: true
|
||||
/// Default: false
|
||||
pub option_as_meta: Option<bool>,
|
||||
/// Whether or not selecting text in the terminal will automatically
|
||||
/// copy to the system clipboard.
|
||||
|
||||
@@ -1427,7 +1427,7 @@ impl Buffer {
|
||||
fn undo_or_redo(&mut self, transaction: Transaction) -> Operation {
|
||||
let mut counts = HashMap::default();
|
||||
for edit_id in transaction.edit_ids {
|
||||
counts.insert(edit_id, self.undo_map.undo_count(edit_id) + 1);
|
||||
counts.insert(edit_id, self.undo_map.undo_count(edit_id).saturating_add(1));
|
||||
}
|
||||
|
||||
let operation = self.undo_operations(counts);
|
||||
|
||||
@@ -170,6 +170,7 @@ pub enum IconName {
|
||||
Dash,
|
||||
DatabaseZap,
|
||||
Delete,
|
||||
Diff,
|
||||
Disconnected,
|
||||
Download,
|
||||
Ellipsis,
|
||||
|
||||
@@ -98,32 +98,40 @@ impl Vim {
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_decimal_string(mut num: &str, mut delta: i64) -> String {
|
||||
let mut negative = false;
|
||||
if num.chars().next() == Some('-') {
|
||||
negative = true;
|
||||
delta = 0 - delta;
|
||||
num = &num[1..];
|
||||
}
|
||||
let result = if let Ok(value) = u64::from_str_radix(num, 10) {
|
||||
let wrapped = value.wrapping_add_signed(delta);
|
||||
if delta < 0 && wrapped > value {
|
||||
negative = !negative;
|
||||
(u64::MAX - wrapped).wrapping_add(1)
|
||||
} else if delta > 0 && wrapped < value {
|
||||
negative = !negative;
|
||||
u64::MAX - wrapped
|
||||
} else {
|
||||
wrapped
|
||||
fn increment_decimal_string(num: &str, delta: i64) -> String {
|
||||
let (negative, delta, num_str) = match num.strip_prefix('-') {
|
||||
Some(n) => (true, -delta, n),
|
||||
None => (false, delta, num),
|
||||
};
|
||||
let num_length = num_str.len();
|
||||
let leading_zero = num_str.starts_with('0');
|
||||
|
||||
let (result, new_negative) = match u64::from_str_radix(num_str, 10) {
|
||||
Ok(value) => {
|
||||
let wrapped = value.wrapping_add_signed(delta);
|
||||
if delta < 0 && wrapped > value {
|
||||
((u64::MAX - wrapped).wrapping_add(1), !negative)
|
||||
} else if delta > 0 && wrapped < value {
|
||||
(u64::MAX - wrapped, !negative)
|
||||
} else {
|
||||
(wrapped, negative)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
u64::MAX
|
||||
Err(_) => (u64::MAX, negative),
|
||||
};
|
||||
|
||||
if result == 0 || !negative {
|
||||
format!("{}", result)
|
||||
let formatted = format!("{}", result);
|
||||
let new_significant_digits = formatted.len();
|
||||
let padding = if leading_zero {
|
||||
num_length.saturating_sub(new_significant_digits)
|
||||
} else {
|
||||
format!("-{}", result)
|
||||
0
|
||||
};
|
||||
|
||||
if new_negative && result != 0 {
|
||||
format!("-{}{}", "0".repeat(padding), formatted)
|
||||
} else {
|
||||
format!("{}{}", "0".repeat(padding), formatted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +294,63 @@ mod test {
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_with_leading_zeros(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
000ˇ9
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
001ˇ0
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("2 ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
000ˇ8
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_with_leading_zeros_and_zero(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
01ˇ1
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
01ˇ2
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("1 2 ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
00ˇ0
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_with_changing_leading_zeros(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
099ˇ9
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
100ˇ0
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("2 ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
99ˇ8
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_with_two_dots(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
@@ -322,6 +387,27 @@ mod test {
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_sign_change_with_leading_zeros(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {"
|
||||
00ˇ1
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
00ˇ0
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
-00ˇ1
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("2 ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
00ˇ1
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_bin_wrapping_and_padding(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{"Put":{"state":"00ˇ1\n"}}
|
||||
{"Key":"ctrl-x"}
|
||||
{"Get":{"state":"00ˇ0\n","mode":"Normal"}}
|
||||
{"Key":"ctrl-x"}
|
||||
{"Get":{"state":"-00ˇ1\n","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"ctrl-a"}
|
||||
{"Get":{"state":"00ˇ1\n","mode":"Normal"}}
|
||||
@@ -0,0 +1,6 @@
|
||||
{"Put":{"state":"099ˇ9\n"}}
|
||||
{"Key":"ctrl-a"}
|
||||
{"Get":{"state":"100ˇ0\n","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"ctrl-x"}
|
||||
{"Get":{"state":"99ˇ8\n","mode":"Normal"}}
|
||||
@@ -0,0 +1,6 @@
|
||||
{"Put":{"state":"000ˇ9\n"}}
|
||||
{"Key":"ctrl-a"}
|
||||
{"Get":{"state":"001ˇ0\n","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"ctrl-x"}
|
||||
{"Get":{"state":"000ˇ8\n","mode":"Normal"}}
|
||||
@@ -0,0 +1,7 @@
|
||||
{"Put":{"state":"01ˇ1\n"}}
|
||||
{"Key":"ctrl-a"}
|
||||
{"Get":{"state":"01ˇ2\n","mode":"Normal"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"2"}
|
||||
{"Key":"ctrl-x"}
|
||||
{"Get":{"state":"00ˇ0\n","mode":"Normal"}}
|
||||
@@ -0,0 +1,4 @@
|
||||
{"Put":{"state":"001ˇ0\n"}}
|
||||
{"Key":"10"}
|
||||
{"Key":"ctrl-x"}
|
||||
{"Get":{"state":"000ˇ9\n","mode":"Normal"}}
|
||||
@@ -380,6 +380,8 @@ impl WorkspaceDb {
|
||||
&self,
|
||||
worktree_roots: &[P],
|
||||
) -> Option<SerializedWorkspace> {
|
||||
// paths are sorted before db interactions to ensure that the order of the paths
|
||||
// doesn't affect the workspace selection for existing workspaces
|
||||
let local_paths = LocalPaths::new(worktree_roots);
|
||||
|
||||
// Note that we re-assign the workspace_id here in case it's empty
|
||||
@@ -833,8 +835,8 @@ impl WorkspaceDb {
|
||||
}
|
||||
|
||||
query! {
|
||||
fn session_workspaces(session_id: String) -> Result<Vec<(LocalPaths, Option<u64>, Option<u64>)>> {
|
||||
SELECT local_paths, window_id, ssh_project_id
|
||||
fn session_workspaces(session_id: String) -> Result<Vec<(LocalPaths, LocalPathsOrder, Option<u64>, Option<u64>)>> {
|
||||
SELECT local_paths, local_paths_order, window_id, ssh_project_id
|
||||
FROM workspaces
|
||||
WHERE session_id = ?1 AND dev_server_project_id IS NULL
|
||||
ORDER BY timestamp DESC
|
||||
@@ -971,7 +973,7 @@ impl WorkspaceDb {
|
||||
) -> Result<Vec<SerializedWorkspaceLocation>> {
|
||||
let mut workspaces = Vec::new();
|
||||
|
||||
for (location, window_id, ssh_project_id) in
|
||||
for (location, order, window_id, ssh_project_id) in
|
||||
self.session_workspaces(last_session_id.to_owned())?
|
||||
{
|
||||
if let Some(ssh_project_id) = ssh_project_id {
|
||||
@@ -980,8 +982,7 @@ impl WorkspaceDb {
|
||||
} else if location.paths().iter().all(|path| path.exists())
|
||||
&& location.paths().iter().any(|path| path.is_dir())
|
||||
{
|
||||
let location =
|
||||
SerializedWorkspaceLocation::from_local_paths(location.paths().iter());
|
||||
let location = SerializedWorkspaceLocation::Local(location, order);
|
||||
workspaces.push((location, window_id.map(WindowId::from)));
|
||||
}
|
||||
}
|
||||
@@ -1603,27 +1604,56 @@ mod tests {
|
||||
window_id: Some(50),
|
||||
};
|
||||
|
||||
let workspace_6 = SerializedWorkspace {
|
||||
id: WorkspaceId(6),
|
||||
location: SerializedWorkspaceLocation::Local(
|
||||
LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
|
||||
LocalPathsOrder::new([2, 1, 0]),
|
||||
),
|
||||
center_group: Default::default(),
|
||||
window_bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
session_id: Some("session-id-3".to_owned()),
|
||||
window_id: Some(60),
|
||||
};
|
||||
|
||||
db.save_workspace(workspace_1.clone()).await;
|
||||
db.save_workspace(workspace_2.clone()).await;
|
||||
db.save_workspace(workspace_3.clone()).await;
|
||||
db.save_workspace(workspace_4.clone()).await;
|
||||
db.save_workspace(workspace_5.clone()).await;
|
||||
db.save_workspace(workspace_6.clone()).await;
|
||||
|
||||
let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
|
||||
assert_eq!(locations.len(), 2);
|
||||
assert_eq!(locations[0].0, LocalPaths::new(["/tmp1"]));
|
||||
assert_eq!(locations[0].1, Some(10));
|
||||
assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
|
||||
assert_eq!(locations[0].2, Some(10));
|
||||
assert_eq!(locations[1].0, LocalPaths::new(["/tmp2"]));
|
||||
assert_eq!(locations[1].1, Some(20));
|
||||
assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
|
||||
assert_eq!(locations[1].2, Some(20));
|
||||
|
||||
let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
|
||||
assert_eq!(locations.len(), 2);
|
||||
assert_eq!(locations[0].0, LocalPaths::new(["/tmp3"]));
|
||||
assert_eq!(locations[0].1, Some(30));
|
||||
assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
|
||||
assert_eq!(locations[0].2, Some(30));
|
||||
let empty_paths: Vec<&str> = Vec::new();
|
||||
assert_eq!(locations[1].0, LocalPaths::new(empty_paths.iter()));
|
||||
assert_eq!(locations[1].1, Some(50));
|
||||
assert_eq!(locations[1].2, Some(ssh_project.id.0));
|
||||
assert_eq!(locations[1].1, LocalPathsOrder::new([]));
|
||||
assert_eq!(locations[1].2, Some(50));
|
||||
assert_eq!(locations[1].3, Some(ssh_project.id.0));
|
||||
|
||||
let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
|
||||
assert_eq!(locations.len(), 1);
|
||||
assert_eq!(
|
||||
locations[0].0,
|
||||
LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
|
||||
);
|
||||
assert_eq!(locations[0].1, LocalPathsOrder::new([2, 1, 0]));
|
||||
assert_eq!(locations[0].2, Some(60));
|
||||
}
|
||||
|
||||
fn default_workspace<P: AsRef<Path>>(
|
||||
@@ -1654,15 +1684,30 @@ mod tests {
|
||||
WorkspaceDb(open_test_db("test_serializing_workspaces_last_session_workspaces").await);
|
||||
|
||||
let workspaces = [
|
||||
(1, dir1.path().to_str().unwrap(), 9),
|
||||
(2, dir2.path().to_str().unwrap(), 5),
|
||||
(3, dir3.path().to_str().unwrap(), 8),
|
||||
(4, dir4.path().to_str().unwrap(), 2),
|
||||
(1, vec![dir1.path()], vec![0], 9),
|
||||
(2, vec![dir2.path()], vec![0], 5),
|
||||
(3, vec![dir3.path()], vec![0], 8),
|
||||
(4, vec![dir4.path()], vec![0], 2),
|
||||
(
|
||||
5,
|
||||
vec![dir1.path(), dir2.path(), dir3.path()],
|
||||
vec![0, 1, 2],
|
||||
3,
|
||||
),
|
||||
(
|
||||
6,
|
||||
vec![dir2.path(), dir3.path(), dir4.path()],
|
||||
vec![2, 1, 0],
|
||||
4,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(id, location, window_id)| SerializedWorkspace {
|
||||
.map(|(id, locations, order, window_id)| SerializedWorkspace {
|
||||
id: WorkspaceId(id),
|
||||
location: SerializedWorkspaceLocation::from_local_paths([location]),
|
||||
location: SerializedWorkspaceLocation::Local(
|
||||
LocalPaths::new(locations),
|
||||
LocalPathsOrder::new(order),
|
||||
),
|
||||
center_group: Default::default(),
|
||||
window_bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
@@ -1681,28 +1726,44 @@ mod tests {
|
||||
WindowId::from(2), // Top
|
||||
WindowId::from(8),
|
||||
WindowId::from(5),
|
||||
WindowId::from(9), // Bottom
|
||||
WindowId::from(9),
|
||||
WindowId::from(3),
|
||||
WindowId::from(4), // Bottom
|
||||
]));
|
||||
|
||||
let have = db
|
||||
.last_session_workspace_locations("one-session", stack)
|
||||
.unwrap();
|
||||
assert_eq!(have.len(), 4);
|
||||
assert_eq!(have.len(), 6);
|
||||
assert_eq!(
|
||||
have[0],
|
||||
SerializedWorkspaceLocation::from_local_paths(&[dir4.path().to_str().unwrap()])
|
||||
SerializedWorkspaceLocation::from_local_paths(&[dir4.path()])
|
||||
);
|
||||
assert_eq!(
|
||||
have[1],
|
||||
SerializedWorkspaceLocation::from_local_paths([dir3.path().to_str().unwrap()])
|
||||
SerializedWorkspaceLocation::from_local_paths([dir3.path()])
|
||||
);
|
||||
assert_eq!(
|
||||
have[2],
|
||||
SerializedWorkspaceLocation::from_local_paths([dir2.path().to_str().unwrap()])
|
||||
SerializedWorkspaceLocation::from_local_paths([dir2.path()])
|
||||
);
|
||||
assert_eq!(
|
||||
have[3],
|
||||
SerializedWorkspaceLocation::from_local_paths([dir1.path().to_str().unwrap()])
|
||||
SerializedWorkspaceLocation::from_local_paths([dir1.path()])
|
||||
);
|
||||
assert_eq!(
|
||||
have[4],
|
||||
SerializedWorkspaceLocation::Local(
|
||||
LocalPaths::new([dir1.path(), dir2.path(), dir3.path()]),
|
||||
LocalPathsOrder::new([0, 1, 2]),
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
have[5],
|
||||
SerializedWorkspaceLocation::Local(
|
||||
LocalPaths::new([dir2.path(), dir3.path(), dir4.path()]),
|
||||
LocalPathsOrder::new([2, 1, 0]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1103,20 +1103,28 @@ impl Workspace {
|
||||
|
||||
let mut paths_to_open = abs_paths;
|
||||
|
||||
let paths_order = serialized_workspace
|
||||
let workspace_location = serialized_workspace
|
||||
.as_ref()
|
||||
.map(|ws| &ws.location)
|
||||
.and_then(|loc| match loc {
|
||||
SerializedWorkspaceLocation::Local(_, order) => Some(order.order()),
|
||||
SerializedWorkspaceLocation::Local(paths, order) => {
|
||||
Some((paths.paths(), order.order()))
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
|
||||
if let Some(paths_order) = paths_order {
|
||||
paths_to_open = paths_order
|
||||
if let Some((paths, order)) = workspace_location {
|
||||
// todo: should probably move this logic to a method on the SerializedWorkspaceLocation
|
||||
// it's only valid for Local and would be more clear there and be able to be tested
|
||||
// and reused elsewhere
|
||||
paths_to_open = order
|
||||
.iter()
|
||||
.filter_map(|i| paths_to_open.get(*i).cloned())
|
||||
.collect::<Vec<_>>();
|
||||
if paths_order.iter().enumerate().any(|(i, &j)| i != j) {
|
||||
.zip(paths.iter())
|
||||
.sorted_by_key(|(i, _)| *i)
|
||||
.map(|(_, path)| path.clone())
|
||||
.collect();
|
||||
|
||||
if order.iter().enumerate().any(|(i, &j)| i != j) {
|
||||
project_handle
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.set_worktrees_reordered(true, cx);
|
||||
@@ -1889,11 +1897,7 @@ impl Workspace {
|
||||
directories: true,
|
||||
multiple: true,
|
||||
},
|
||||
if self.project.read(cx).is_via_ssh() {
|
||||
DirectoryLister::Project(self.project.clone())
|
||||
} else {
|
||||
DirectoryLister::Local(self.app_state.fs.clone())
|
||||
},
|
||||
DirectoryLister::Local(self.app_state.fs.clone()),
|
||||
cx,
|
||||
);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ pub(crate) mod windows_only_instance;
|
||||
pub use app_menus::*;
|
||||
use assistant::PromptBuilder;
|
||||
use breadcrumbs::Breadcrumbs;
|
||||
use client::ZED_URL_SCHEME;
|
||||
use client::{zed_urls, ZED_URL_SCHEME};
|
||||
use collections::VecDeque;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use editor::ProposedChangesEditorToolbar;
|
||||
@@ -419,8 +419,7 @@ pub fn initialize_workspace(
|
||||
)
|
||||
.register_action(
|
||||
|_: &mut Workspace, _: &OpenAccountSettings, cx: &mut ViewContext<Workspace>| {
|
||||
let server_url = &client::ClientSettings::get_global(cx).server_url;
|
||||
cx.open_url(&format!("{server_url}/account"));
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
},
|
||||
)
|
||||
.register_action(
|
||||
|
||||
@@ -1498,13 +1498,13 @@ List of `integer` column numbers
|
||||
"directories": [".env", "env", ".venv", "venv"],
|
||||
"activate_script": "default"
|
||||
}
|
||||
}
|
||||
},
|
||||
"env": {},
|
||||
"font_family": null,
|
||||
"font_features": null,
|
||||
"font_size": null,
|
||||
"line_height": "comfortable",
|
||||
"option_as_meta": true,
|
||||
"option_as_meta": false,
|
||||
"button": false,
|
||||
"shell": {},
|
||||
"toolbar": {
|
||||
@@ -1732,7 +1732,7 @@ See Buffer Font Features
|
||||
|
||||
- Description: Re-interprets the option keys to act like a 'meta' key, like in Emacs.
|
||||
- Setting: `option_as_meta`
|
||||
- Default: `true`
|
||||
- Default: `false`
|
||||
|
||||
**Options**
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ Zed supports hundreds of programming languages and text formats. Some work out-o
|
||||
- [JavaScript](./languages/javascript.md)
|
||||
- [Julia](./languages/julia.md)
|
||||
- [JSON](./languages/json.md)
|
||||
- [Jsonnet](./languages/jsonnet.md)
|
||||
- [Kotlin](./languages/kotlin.md)
|
||||
- [Lua](./languages/lua.md)
|
||||
- [Luau](./languages/luau.md)
|
||||
@@ -104,7 +105,6 @@ Zed supports hundreds of programming languages and text formats. Some work out-o
|
||||
- [Groq](https://github.com/juice49/zed-groq)
|
||||
- [INI](https://github.com/bajrangCoder/zed-ini)
|
||||
- [Java](https://github.com/zed-extensions/java)
|
||||
- [Jsonnet](https://github.com/narqo/zed-jsonnet)
|
||||
- [Justfiles](https://github.com/jackTabsCode/zed-just)
|
||||
- [LaTeX](https://github.com/rzukic/zed-latex)
|
||||
- [Ledger](https://github.com/mrkstwrt/zed-ledger)
|
||||
|
||||
@@ -10,11 +10,9 @@ Both use:
|
||||
- Tree Sitter: [tree-sitter/tree-sitter-java](https://github.com/tree-sitter/tree-sitter-java)
|
||||
- Language Server: [eclipse-jdtls/eclipse.jdt.ls](https://github.com/eclipse-jdtls/eclipse.jdt.ls)
|
||||
|
||||
## Pre-requisites
|
||||
## Install OpenJDK
|
||||
|
||||
You will need to install both a Java runtime (OpenJDK) and Eclipse JDT Language Server (`eclipse.jdt.ls`).
|
||||
|
||||
### Install OpenJDK
|
||||
You will need to install a Java runtime (OpenJDK).
|
||||
|
||||
- MacOS: `brew install openjdk`
|
||||
- Ubuntu: `sudo add-apt-repository ppa:openjdk-23 && sudo apt-get install openjdk-23`
|
||||
@@ -23,26 +21,19 @@ You will need to install both a Java runtime (OpenJDK) and Eclipse JDT Language
|
||||
|
||||
Or manually download and install [OpenJDK 23](https://jdk.java.net/23/).
|
||||
|
||||
### Install JDTLS
|
||||
|
||||
- MacOS: `brew install jdtls`
|
||||
- Arch: [`jdtls` from AUR](https://aur.archlinux.org/packages/jdtls)
|
||||
|
||||
Or manually download install:
|
||||
|
||||
- [JDTLS Milestone Builds](http://download.eclipse.org/jdtls/milestones/) (updated every two weeks)
|
||||
- [JDTLS Snapshot Builds](https://download.eclipse.org/jdtls/snapshots/) (frequent updates)
|
||||
|
||||
## Extension Install
|
||||
|
||||
You can install either by opening {#action zed::Extensions}({#kb zed::Extensions}) and searching for `java`.
|
||||
|
||||
We recommend you install one or the other and not both.
|
||||
|
||||
## Settings / Initialization Options
|
||||
|
||||
See [JDTLS Language Server Settings & Capabilities](https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Language-Server-Settings-&-Capabilities) for a complete list of options.
|
||||
Both extensions will automatically download the language server, see: [Manual JDTLS Install](#manual-jdts-install) below if you'd prefer to manage that yourself.
|
||||
|
||||
Add the following to your Zed Settings by launching {#action zed::OpenSettings}({#kb zed::OpenSettings}).
|
||||
For available `initialization_options` please see the [Initialize Request section of the Eclipse.jdt.ls Wiki](https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request).
|
||||
|
||||
You can add these customizations to your Zed Settings by launching {#action zed::OpenSettings}({#kb zed::OpenSettings}) or by using a `.zed/setting.json` inside your project.
|
||||
|
||||
### Zed Java Settings
|
||||
|
||||
@@ -50,9 +41,11 @@ Add the following to your Zed Settings by launching {#action zed::OpenSettings}(
|
||||
{
|
||||
"lsp": {
|
||||
"jdtls": {
|
||||
"settings": {},
|
||||
"settings": {
|
||||
"version": "1.40.0", // jdtls version to download and use
|
||||
"classpath": "/path/to/classes.jar:/path/to/more/classes/"
|
||||
},
|
||||
"initialization_options": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,37 +64,9 @@ Add the following to your Zed Settings by launching {#action zed::OpenSettings}(
|
||||
}
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [Zed Java Readme](https://github.com/zed-extensions/java)
|
||||
- [Java with Eclipse JDTLS Readme](https://github.com/ABckh/zed-java-eclipse-jdtls)
|
||||
|
||||
### Support
|
||||
|
||||
If you have issues with either of these plugins, please open issues on their respective repositories:
|
||||
|
||||
- [Zed Java Issues](https://github.com/zed-extensions/java/issues)
|
||||
- [Java with Eclipse JDTLS Issues](https://github.com/ABckh/zed-java-eclipse-jdtls/issues)
|
||||
|
||||
## Example Configs
|
||||
|
||||
### Zed Java Classpath
|
||||
|
||||
You can optionally configure the class path that JDTLS uses with:
|
||||
|
||||
```json
|
||||
{
|
||||
"lsp": {
|
||||
"jdtls": {
|
||||
"settings": {
|
||||
"classpath": "/path/to/classes.jar:/path/to/more/classes/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Zed Java Initialization Options
|
||||
### Zed Java Initialization Options
|
||||
|
||||
There are also many more options you can pass directly to the language server, for example:
|
||||
|
||||
@@ -184,7 +149,7 @@ There are also many more options you can pass directly to the language server, f
|
||||
}
|
||||
```
|
||||
|
||||
## Java with Eclipse JTDLS Configuration {#zed-java-eclipse-configuration}
|
||||
### Java with Eclipse JTDLS Configuration {#zed-java-eclipse-configuration}
|
||||
|
||||
Configuration options match those provided in the [redhat-developer/vscode-java extension](https://github.com/redhat-developer/vscode-java#supported-vs-code-settings).
|
||||
|
||||
@@ -201,3 +166,27 @@ For example, to enable [Lombok Support](https://github.com/redhat-developer/vsco
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Manual JDTLS Install
|
||||
|
||||
If you prefer, you can install JDTLS yourself and both extensions can be configured to use that instead.
|
||||
|
||||
- MacOS: `brew install jdtls`
|
||||
- Arch: [`jdtls` from AUR](https://aur.archlinux.org/packages/jdtls)
|
||||
|
||||
Or manually download install:
|
||||
|
||||
- [JDTLS Milestone Builds](http://download.eclipse.org/jdtls/milestones/) (updated every two weeks)
|
||||
- [JDTLS Snapshot Builds](https://download.eclipse.org/jdtls/snapshots/) (frequent updates)
|
||||
|
||||
## See also
|
||||
|
||||
- [Zed Java Readme](https://github.com/zed-extensions/java)
|
||||
- [Java with Eclipse JDTLS Readme](https://github.com/ABckh/zed-java-eclipse-jdtls)
|
||||
|
||||
## Support
|
||||
|
||||
If you have issues with either of these plugins, please open issues on their respective repositories:
|
||||
|
||||
- [Zed Java Issues](https://github.com/zed-extensions/java/issues)
|
||||
- [Java with Eclipse JDTLS Issues](https://github.com/ABckh/zed-java-eclipse-jdtls/issues)
|
||||
|
||||
24
docs/src/languages/jsonnet.md
Normal file
24
docs/src/languages/jsonnet.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Jsonnet
|
||||
|
||||
Jsonnet language support in Zed is provided by the community-maintained [Jsonnet extension](https://github.com/narqo/zed-jsonnet).
|
||||
|
||||
- Tree Sitter: [sourcegraph/tree-sitter-jsonnet](https://github.com/sourcegraph/tree-sitter-jsonnet)
|
||||
- Language Server: [grafana/jsonnet-language-server](https://github.com/grafana/jsonnet-language-server)
|
||||
|
||||
## Configuration
|
||||
|
||||
Workspace configuration options can be passed to the language server via the `lsp` settings of the `settings.json`.
|
||||
|
||||
The following example enables support for resolving [tanka](https://tanka.dev) import paths in `jsonnet-language-server`:
|
||||
|
||||
```json
|
||||
{
|
||||
"lsp": {
|
||||
"jsonnet-language-server": {
|
||||
"settings": {
|
||||
"resolve_paths_with_tanka": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -3,6 +3,16 @@
|
||||
(arguments (alias) @name)
|
||||
(#match? @context "^(defmodule|defprotocol)$")) @item
|
||||
|
||||
(call
|
||||
target: (identifier) @context
|
||||
(arguments (_) @name)?
|
||||
(#match? @context "^(setup|setup_all)$")) @item
|
||||
|
||||
(call
|
||||
target: (identifier) @context
|
||||
(arguments (string) @name)
|
||||
(#match? @context "^(describe|test)$")) @item
|
||||
|
||||
(unary_operator
|
||||
operator: "@" @name
|
||||
operand: (call
|
||||
|
||||
3
extensions/svelte/.gitignore
vendored
3
extensions/svelte/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
target
|
||||
*.wasm
|
||||
grammars
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "zed_svelte"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/svelte.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
zed_extension_api = "0.1.0"
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -1,15 +0,0 @@
|
||||
id = "svelte"
|
||||
name = "Svelte"
|
||||
description = "Svelte support"
|
||||
version = "0.2.0"
|
||||
schema_version = 1
|
||||
authors = []
|
||||
repository = "https://github.com/zed-extensions/svelte"
|
||||
|
||||
[language_servers.svelte-language-server]
|
||||
name = "Svelte Language Server"
|
||||
language = "Svelte"
|
||||
|
||||
[grammars.svelte]
|
||||
repository = "https://github.com/tree-sitter-grammars/tree-sitter-svelte"
|
||||
commit = "3f06f705410683adb17d146b5eca28c62fe81ba6"
|
||||
@@ -1,7 +0,0 @@
|
||||
("<" @open ">" @close)
|
||||
("{" @open "}" @close)
|
||||
("'" @open "'" @close)
|
||||
("\"" @open "\"" @close)
|
||||
("(" @open ")" @close)
|
||||
; ("[" @open "]" @close)
|
||||
; ("`" @open "`" @close)
|
||||
@@ -1,22 +0,0 @@
|
||||
name = "Svelte"
|
||||
grammar = "svelte"
|
||||
path_suffixes = ["svelte"]
|
||||
block_comment = ["<!-- ", " -->"]
|
||||
autoclose_before = ":\"'}]>"
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
{ start = "<", end = ">", close = true, newline = true, not_in = ["string"] },
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
{ start = "(", end = ")", close = true, newline = true },
|
||||
{ start = "!--", end = " --", close = true, newline = true },
|
||||
{ start = "\"", end = "\"", close = true, newline = true, not_in = ["string"] },
|
||||
{ start = "'", end = "'", close = true, newline = true, not_in = ["string"] },
|
||||
{ start = "`", end = "`", close = true, newline = true, not_in = ["string"] },
|
||||
]
|
||||
scope_opt_in_language_servers = ["tailwindcss-language-server"]
|
||||
prettier_parser_name = "svelte"
|
||||
prettier_plugins = ["prettier-plugin-svelte"]
|
||||
|
||||
[overrides.string]
|
||||
word_characters = ["-"]
|
||||
opt_into_language_servers = ["tailwindcss-language-server"]
|
||||
@@ -1,107 +0,0 @@
|
||||
|
||||
; comments
|
||||
(comment) @comment
|
||||
|
||||
; property attribute
|
||||
(attribute_directive) @attribute.function
|
||||
(attribute_identifier) @attribute
|
||||
(attribute_modifier) @attribute.special
|
||||
|
||||
; Style component attributes as @property
|
||||
(start_tag
|
||||
(
|
||||
(tag_name) @_tag_name
|
||||
(#match? @_tag_name "^[A-Z]")
|
||||
)
|
||||
(attribute
|
||||
(attribute_name
|
||||
(attribute_identifier) @tag.property
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
(self_closing_tag
|
||||
(
|
||||
(tag_name) @_tag_name
|
||||
(#match? @_tag_name "^[A-Z]")
|
||||
)
|
||||
(attribute
|
||||
(attribute_name
|
||||
(attribute_identifier) @tag.property
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
; style elements starting with lowercase letters as tags
|
||||
(
|
||||
(tag_name) @tag
|
||||
(#match? @tag "^[a-z]")
|
||||
)
|
||||
|
||||
; style elements starting with uppercase letters as components (types)
|
||||
; Also valid might be to treat them as constructors
|
||||
(
|
||||
(tag_name) @tag @tag.component.type.constructor
|
||||
(#match? @tag "^[A-Z]")
|
||||
)
|
||||
|
||||
[
|
||||
"<"
|
||||
">"
|
||||
"</"
|
||||
"/>"
|
||||
] @tag.punctuation.bracket
|
||||
|
||||
|
||||
[
|
||||
"{"
|
||||
"}"
|
||||
] @punctuation.bracket
|
||||
|
||||
[
|
||||
"|"
|
||||
] @punctuation.delimiter
|
||||
|
||||
|
||||
[
|
||||
"@"
|
||||
"#"
|
||||
":"
|
||||
"/"
|
||||
] @tag.punctuation.special
|
||||
|
||||
"=" @operator
|
||||
|
||||
|
||||
; Treating (if, each, ...) as a keyword inside of blocks
|
||||
; like {#if ...} or {#each ...}
|
||||
(block_start_tag
|
||||
tag: _ @tag.keyword
|
||||
)
|
||||
|
||||
(block_tag
|
||||
tag: _ @tag.keyword
|
||||
)
|
||||
|
||||
(block_end_tag
|
||||
tag: _ @tag.keyword
|
||||
)
|
||||
|
||||
(expression_tag
|
||||
tag: _ @tag.keyword
|
||||
)
|
||||
|
||||
; Style quoted string attribute values
|
||||
(quoted_attribute_value) @string
|
||||
|
||||
|
||||
; Highlight the `as` keyword in each blocks
|
||||
(each_start
|
||||
("as") @tag.keyword
|
||||
)
|
||||
|
||||
|
||||
; Highlight the snippet name as a function
|
||||
; (e.g. {#snippet foo(bar)}
|
||||
(snippet_name) @function
|
||||
@@ -1,9 +0,0 @@
|
||||
[
|
||||
(element)
|
||||
(if_statement)
|
||||
(each_statement)
|
||||
(await_statement)
|
||||
(snippet_statement)
|
||||
(script_element)
|
||||
(style_element)
|
||||
] @indent
|
||||
@@ -1,86 +0,0 @@
|
||||
; ; injections.scm
|
||||
; ; --------------
|
||||
|
||||
; Match script tags with a lang attribute
|
||||
(script_element
|
||||
(start_tag
|
||||
(attribute
|
||||
(attribute_name) @_attr_name
|
||||
(#eq? @_attr_name "lang")
|
||||
(quoted_attribute_value
|
||||
(attribute_value) @language
|
||||
)
|
||||
)
|
||||
)
|
||||
(raw_text) @content
|
||||
)
|
||||
|
||||
; Match script tags without a lang attribute
|
||||
(script_element
|
||||
(start_tag
|
||||
(attribute
|
||||
(attribute_name) @_attr_name
|
||||
)*
|
||||
)
|
||||
(raw_text) @content
|
||||
(#not-any-of? @_attr_name "lang")
|
||||
(#set! language "javascript")
|
||||
)
|
||||
|
||||
; Match the contents of the script's generics="T extends string" as typescript code
|
||||
;
|
||||
; Disabled for the time-being because tree-sitter is treating the generics
|
||||
; attribute as a top-level typescript statement, where `T extends string` is
|
||||
; not a valid top-level typescript statement.
|
||||
;
|
||||
; (script_element
|
||||
; (start_tag
|
||||
; (attribute
|
||||
; (attribute_name) @_attr_name
|
||||
; (#eq? @_attr_name "generics")
|
||||
; (quoted_attribute_value
|
||||
; (attribute_value) @content
|
||||
; )
|
||||
; )
|
||||
; )
|
||||
; (#set! language "typescript")
|
||||
; )
|
||||
|
||||
|
||||
; Mark everything as typescript because it's
|
||||
; a more generic superset of javascript
|
||||
; Not sure if it's possible to somehow refer to the
|
||||
; script's language attribute here.
|
||||
((svelte_raw_text) @content
|
||||
(#set! "language" "ts")
|
||||
)
|
||||
|
||||
; Match style tags with a lang attribute
|
||||
(style_element
|
||||
(start_tag
|
||||
(attribute
|
||||
(attribute_name) @_attr_name
|
||||
(#eq? @_attr_name "lang")
|
||||
(quoted_attribute_value
|
||||
(attribute_value) @language
|
||||
)
|
||||
)
|
||||
)
|
||||
(raw_text) @content
|
||||
)
|
||||
|
||||
; Match style tags without a lang attribute
|
||||
(style_element
|
||||
(start_tag
|
||||
(attribute
|
||||
(attribute_name) @_attr_name
|
||||
)*
|
||||
)
|
||||
(raw_text) @content
|
||||
(#not-any-of? @_attr_name "lang")
|
||||
(#set! language "css")
|
||||
)
|
||||
|
||||
|
||||
; Downstream TODO: Style highlighting for `style:background="red"` and `style="background: red"` strings
|
||||
; Downstream TODO: Style component comments as markdown
|
||||
@@ -1,69 +0,0 @@
|
||||
|
||||
(script_element
|
||||
(start_tag) @name
|
||||
(raw_text) @context @item
|
||||
)
|
||||
|
||||
(script_element
|
||||
(end_tag) @name @item
|
||||
)
|
||||
|
||||
(style_element
|
||||
(start_tag) @name
|
||||
(raw_text) @context
|
||||
) @item
|
||||
|
||||
|
||||
(document) @item
|
||||
|
||||
(comment) @annotation
|
||||
|
||||
(if_statement
|
||||
(if_start) @name
|
||||
) @item
|
||||
|
||||
(else_block
|
||||
(else_start) @name
|
||||
) @item
|
||||
|
||||
(else_if_block
|
||||
(else_if_start) @name
|
||||
) @item
|
||||
|
||||
(element
|
||||
(start_tag) @name
|
||||
) @item
|
||||
|
||||
(element
|
||||
(self_closing_tag) @name
|
||||
) @item
|
||||
|
||||
|
||||
; (if_end) @name @item
|
||||
|
||||
(each_statement
|
||||
(each_start) @name
|
||||
) @item
|
||||
|
||||
|
||||
(snippet_statement
|
||||
(snippet_start) @name
|
||||
) @item
|
||||
|
||||
(snippet_end) @name @item
|
||||
|
||||
(html_tag) @name @item
|
||||
|
||||
(const_tag) @name @item
|
||||
|
||||
(await_statement
|
||||
(await_start) @name
|
||||
) @item
|
||||
|
||||
(then_block
|
||||
(then_start) @name
|
||||
) @item
|
||||
|
||||
(catch_block
|
||||
(catch_start) @name
|
||||
) @item
|
||||
@@ -1,7 +0,0 @@
|
||||
(comment) @comment
|
||||
|
||||
[
|
||||
(raw_text)
|
||||
(attribute_value)
|
||||
(quoted_attribute_value)
|
||||
] @string
|
||||
@@ -1,124 +0,0 @@
|
||||
use std::{env, fs};
|
||||
use zed_extension_api::{self as zed, serde_json, Result};
|
||||
|
||||
struct SvelteExtension {
|
||||
did_find_server: bool,
|
||||
}
|
||||
|
||||
const SERVER_PATH: &str = "node_modules/svelte-language-server/bin/server.js";
|
||||
const PACKAGE_NAME: &str = "svelte-language-server";
|
||||
|
||||
impl SvelteExtension {
|
||||
fn server_exists(&self) -> bool {
|
||||
fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file())
|
||||
}
|
||||
|
||||
fn server_script_path(&mut self, 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(
|
||||
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(
|
||||
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 SvelteExtension {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
did_find_server: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn language_server_command(
|
||||
&mut self,
|
||||
id: &zed::LanguageServerId,
|
||||
_: &zed::Worktree,
|
||||
) -> Result<zed::Command> {
|
||||
let server_path = self.server_script_path(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,
|
||||
_: &zed::LanguageServerId,
|
||||
_: &zed::Worktree,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
let config = serde_json::json!({
|
||||
"inlayHints": {
|
||||
"parameterNames": {
|
||||
"enabled": "all",
|
||||
"suppressWhenArgumentMatchesName": false
|
||||
},
|
||||
"parameterTypes": {
|
||||
"enabled": true
|
||||
},
|
||||
"variableTypes": {
|
||||
"enabled": true,
|
||||
"suppressWhenTypeMatchesName": false
|
||||
},
|
||||
"propertyDeclarationTypes": {
|
||||
"enabled": true
|
||||
},
|
||||
"functionLikeReturnTypes": {
|
||||
"enabled": true
|
||||
},
|
||||
"enumMemberValues": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Some(serde_json::json!({
|
||||
"provideFormatter": true,
|
||||
"configuration": {
|
||||
"typescript": config,
|
||||
"javascript": config
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
zed::register_extension!(SvelteExtension);
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "zed_vue"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/vue.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
zed_extension_api = "0.1.0"
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -1,19 +0,0 @@
|
||||
id = "vue"
|
||||
name = "Vue"
|
||||
description = "Vue support."
|
||||
version = "0.1.0"
|
||||
schema_version = 1
|
||||
authors = ["Piotr Osiewicz <piotr@zed.dev>"]
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
|
||||
[language_servers.vue-language-server]
|
||||
name = "Vue Language Server"
|
||||
language = "Vue.js"
|
||||
language_ids = { "Vue.js" = "vue" }
|
||||
# REFACTOR is explicitly disabled, as vue-lsp does not adhere to LSP protocol for code actions with these - it
|
||||
# sends back a CodeAction with neither `command` nor `edits` fields set, which is against the spec.
|
||||
code_action_kinds = ["", "quickfix", "refactor.rewrite"]
|
||||
|
||||
[grammars.vue]
|
||||
repository = "https://github.com/tree-sitter-grammars/tree-sitter-vue"
|
||||
commit = "7e48557b903a9db9c38cea3b7839ef7e1f36c693"
|
||||
@@ -1,2 +0,0 @@
|
||||
("<" @open ">" @close)
|
||||
("\"" @open "\"" @close)
|
||||
@@ -1,22 +0,0 @@
|
||||
name = "Vue.js"
|
||||
code_fence_block_name = "vue"
|
||||
grammar = "vue"
|
||||
path_suffixes = ["vue"]
|
||||
block_comment = ["<!-- ", " -->"]
|
||||
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 = true, not_in = ["string", "comment"] },
|
||||
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
|
||||
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
|
||||
{ start = "`", end = "`", close = true, newline = false, not_in = ["string"] },
|
||||
]
|
||||
word_characters = ["-"]
|
||||
scope_opt_in_language_servers = ["tailwindcss-language-server"]
|
||||
prettier_parser_name = "vue"
|
||||
|
||||
[overrides.string]
|
||||
word_characters = ["-"]
|
||||
opt_into_language_servers = ["tailwindcss-language-server"]
|
||||
@@ -1,15 +0,0 @@
|
||||
(attribute) @property
|
||||
(directive_attribute) @property
|
||||
(quoted_attribute_value) @string
|
||||
(interpolation) @punctuation.special
|
||||
(raw_text) @embedded
|
||||
|
||||
((tag_name) @type
|
||||
(#match? @type "^[A-Z]"))
|
||||
|
||||
(directive_name) @keyword
|
||||
(directive_argument) @constant
|
||||
|
||||
(start_tag) @tag
|
||||
(end_tag) @tag
|
||||
(self_closing_tag) @tag
|
||||
@@ -1,60 +0,0 @@
|
||||
; <script>
|
||||
((script_element
|
||||
(start_tag) @_no_lang
|
||||
(raw_text) @content)
|
||||
(#not-match? @_no_lang "lang=")
|
||||
(#set! "language" "javascript"))
|
||||
|
||||
; <script lang="js">
|
||||
((script_element
|
||||
(start_tag
|
||||
(attribute
|
||||
(attribute_name) @_lang
|
||||
(quoted_attribute_value
|
||||
(attribute_value) @_js)))
|
||||
(raw_text) @content)
|
||||
(#eq? @_lang "lang")
|
||||
(#eq? @_js "js")
|
||||
(#set! "language" "javascript"))
|
||||
|
||||
; <script lang="ts">
|
||||
((script_element
|
||||
(start_tag
|
||||
(attribute
|
||||
(attribute_name) @_lang
|
||||
(quoted_attribute_value
|
||||
(attribute_value) @_ts)))
|
||||
(raw_text) @content)
|
||||
(#eq? @_lang "lang")
|
||||
(#eq? @_ts "ts")
|
||||
(#set! "language" "typescript"))
|
||||
|
||||
; <script lang="tsx">
|
||||
; <script lang="jsx">
|
||||
; Zed built-in tsx, we mark it as tsx ^:)
|
||||
(script_element
|
||||
(start_tag
|
||||
(attribute
|
||||
(attribute_name) @_attr
|
||||
(quoted_attribute_value
|
||||
(attribute_value) @language)))
|
||||
(#eq? @_attr "lang")
|
||||
(#any-of? @language "tsx" "jsx")
|
||||
(raw_text) @content)
|
||||
|
||||
|
||||
; {{ }}
|
||||
((interpolation
|
||||
(raw_text) @content)
|
||||
(#set! "language" "typescript"))
|
||||
|
||||
; v-
|
||||
(directive_attribute
|
||||
(quoted_attribute_value
|
||||
(attribute_value) @content
|
||||
(#set! "language" "typescript")))
|
||||
|
||||
; TODO: support less/sass/scss
|
||||
(style_element
|
||||
(raw_text) @content
|
||||
(#set! "language" "css"))
|
||||
@@ -1,7 +0,0 @@
|
||||
(comment) @comment
|
||||
|
||||
[
|
||||
(raw_text)
|
||||
(attribute_value)
|
||||
(quoted_attribute_value)
|
||||
] @string
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user