project: Workaround invalid code action edits from pyright (#28354)

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Piotr Osiewicz <piotr@zed.dev>

fixes issue where:

In a two line python file like so
```

Path()
```

If the user asks for code actions on `Path` and they select (`From
pathlib import path`)
the result they get is
```

Pathfrom pathlib import Path


Path()
```
Instead of 

```

from pathlib import Path



Path()
```

This is due to a non-lsp-spec-compliant response from pyright below

```json
{"jsonrpc":"2.0","id":40,"result":[{"title":"from pathlib import Path","edit":{"changes":{"file:///Users/neb/Zed/example-project/pyright-project/main.py":[{"range":{"start":{"line":2,"character":0},"end":{"line":2,"character":4}},"newText":"Path"},{"range":{"start":{"line":2,"character":0},"end":{"line":2,"character":0}},"newText":"from pathlib import Path\n\n\n"}]}},"kind":"quickfix"}]}
```

Release Notes:

- Fixed an issue when using auto-import code actions provided by pyright
(or basedpyright) where the import would be jumbled with the scoped
import resulting in an invalid result

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
This commit is contained in:
Ben Kunkle 2025-04-08 16:13:44 -04:00 committed by GitHub
parent 97641c3298
commit 38ec45008c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 58 additions and 1 deletions

View file

@ -2635,7 +2635,8 @@ impl LocalLspStore {
.into_iter()
.map(|edit| (range_from_lsp(edit.range), edit.new_text))
.collect::<Vec<_>>();
lsp_edits.sort_by_key(|(range, _)| range.start);
lsp_edits.sort_by_key(|(range, _)| (range.start, range.end));
let mut lsp_edits = lsp_edits.into_iter().peekable();
let mut edits = Vec::new();

View file

@ -2663,6 +2663,62 @@ async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAp
});
}
#[gpui::test]
async fn test_edits_from_lsp_with_replacement_followed_by_adjacent_insertion(
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
let text = "Path()";
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/dir"),
json!({
"a.rs": text
}),
)
.await;
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer(path!("/dir/a.rs"), cx)
})
.await
.unwrap();
// Simulate the language server sending us a pair of edits at the same location,
// with an insertion following a replacement (which violates the LSP spec).
let edits = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.as_local_mut().unwrap().edits_from_lsp(
&buffer,
[
lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
new_text: "Path".into(),
},
lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
new_text: "from path import Path\n\n\n".into(),
},
],
LanguageServerId(0),
None,
cx,
)
})
.await
.unwrap();
buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
assert_eq!(buffer.text(), "from path import Path\n\n\nPath()")
});
}
#[gpui::test]
async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) {
init_test(cx);