Merge branch 'main' into window_context_2
This commit is contained in:
commit
c76b9794e4
45 changed files with 1760 additions and 882 deletions
|
@ -10,10 +10,11 @@ doctest = false
|
|||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smallvec = { workspace = true }
|
||||
collections = { path = "../collections" }
|
||||
editor = { path = "../editor" }
|
||||
language = { path = "../language" }
|
||||
lsp = { path = "../lsp" }
|
||||
gpui = { path = "../gpui" }
|
||||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
|
@ -27,6 +28,7 @@ unindent = "0.1"
|
|||
client = { path = "../client", features = ["test-support"] }
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
serde_json = { workspace = true }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
pub mod items;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::{BTreeMap, HashSet};
|
||||
use collections::{BTreeSet, HashSet};
|
||||
use editor::{
|
||||
diagnostic_block_renderer,
|
||||
display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
|
||||
|
@ -17,6 +17,7 @@ use language::{
|
|||
Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
|
||||
SelectionGoal,
|
||||
};
|
||||
use lsp::LanguageServerId;
|
||||
use project::{DiagnosticSummary, Project, ProjectPath};
|
||||
use serde_json::json;
|
||||
use settings::Settings;
|
||||
|
@ -55,7 +56,7 @@ struct ProjectDiagnosticsEditor {
|
|||
summary: DiagnosticSummary,
|
||||
excerpts: ModelHandle<MultiBuffer>,
|
||||
path_states: Vec<PathState>,
|
||||
paths_to_update: BTreeMap<ProjectPath, usize>,
|
||||
paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
|
||||
}
|
||||
|
||||
struct PathState {
|
||||
|
@ -71,6 +72,7 @@ struct Jump {
|
|||
}
|
||||
|
||||
struct DiagnosticGroupState {
|
||||
language_server_id: LanguageServerId,
|
||||
primary_diagnostic: DiagnosticEntry<language::Anchor>,
|
||||
primary_excerpt_ix: usize,
|
||||
excerpts: Vec<ExcerptId>,
|
||||
|
@ -115,7 +117,7 @@ impl View for ProjectDiagnosticsEditor {
|
|||
}),
|
||||
"summary": self.summary,
|
||||
"paths_to_update": self.paths_to_update.iter().map(|(path, server_id)|
|
||||
(path.path.to_string_lossy(), server_id)
|
||||
(path.path.to_string_lossy(), server_id.0)
|
||||
).collect::<Vec<_>>(),
|
||||
"paths_states": self.path_states.iter().map(|state|
|
||||
json!({
|
||||
|
@ -148,7 +150,7 @@ impl ProjectDiagnosticsEditor {
|
|||
path,
|
||||
} => {
|
||||
this.paths_to_update
|
||||
.insert(path.clone(), *language_server_id);
|
||||
.insert((path.clone(), *language_server_id));
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
|
@ -167,7 +169,7 @@ impl ProjectDiagnosticsEditor {
|
|||
let project = project_handle.read(cx);
|
||||
let paths_to_update = project
|
||||
.diagnostic_summaries(cx)
|
||||
.map(|e| (e.0, e.1.language_server_id))
|
||||
.map(|(path, server_id, _)| (path, server_id))
|
||||
.collect();
|
||||
let summary = project.diagnostic_summary(cx);
|
||||
let mut this = Self {
|
||||
|
@ -195,9 +197,13 @@ impl ProjectDiagnosticsEditor {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_excerpts(&mut self, language_server_id: Option<usize>, cx: &mut ViewContext<Self>) {
|
||||
fn update_excerpts(
|
||||
&mut self,
|
||||
language_server_id: Option<LanguageServerId>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let mut paths = Vec::new();
|
||||
self.paths_to_update.retain(|path, server_id| {
|
||||
self.paths_to_update.retain(|(path, server_id)| {
|
||||
if language_server_id
|
||||
.map_or(true, |language_server_id| language_server_id == *server_id)
|
||||
{
|
||||
|
@ -214,7 +220,9 @@ impl ProjectDiagnosticsEditor {
|
|||
let buffer = project
|
||||
.update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| this.populate_excerpts(path, buffer, cx))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.populate_excerpts(path, language_server_id, buffer, cx)
|
||||
})?;
|
||||
}
|
||||
Result::<_, anyhow::Error>::Ok(())
|
||||
}
|
||||
|
@ -226,6 +234,7 @@ impl ProjectDiagnosticsEditor {
|
|||
fn populate_excerpts(
|
||||
&mut self,
|
||||
path: ProjectPath,
|
||||
language_server_id: Option<LanguageServerId>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
|
@ -264,9 +273,9 @@ impl ProjectDiagnosticsEditor {
|
|||
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
|
||||
let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
|
||||
let mut new_groups = snapshot
|
||||
.diagnostic_groups()
|
||||
.diagnostic_groups(language_server_id)
|
||||
.into_iter()
|
||||
.filter(|group| {
|
||||
.filter(|(_, group)| {
|
||||
group.entries[group.primary_ix].diagnostic.severity
|
||||
<= DiagnosticSeverity::WARNING
|
||||
})
|
||||
|
@ -278,12 +287,27 @@ impl ProjectDiagnosticsEditor {
|
|||
match (old_groups.peek(), new_groups.peek()) {
|
||||
(None, None) => break,
|
||||
(None, Some(_)) => to_insert = new_groups.next(),
|
||||
(Some(_), None) => to_remove = old_groups.next(),
|
||||
(Some((_, old_group)), Some(new_group)) => {
|
||||
(Some((_, old_group)), None) => {
|
||||
if language_server_id.map_or(true, |id| id == old_group.language_server_id)
|
||||
{
|
||||
to_remove = old_groups.next();
|
||||
} else {
|
||||
to_keep = old_groups.next();
|
||||
}
|
||||
}
|
||||
(Some((_, old_group)), Some((_, new_group))) => {
|
||||
let old_primary = &old_group.primary_diagnostic;
|
||||
let new_primary = &new_group.entries[new_group.primary_ix];
|
||||
match compare_diagnostics(old_primary, new_primary, &snapshot) {
|
||||
Ordering::Less => to_remove = old_groups.next(),
|
||||
Ordering::Less => {
|
||||
if language_server_id
|
||||
.map_or(true, |id| id == old_group.language_server_id)
|
||||
{
|
||||
to_remove = old_groups.next();
|
||||
} else {
|
||||
to_keep = old_groups.next();
|
||||
}
|
||||
}
|
||||
Ordering::Equal => {
|
||||
to_keep = old_groups.next();
|
||||
new_groups.next();
|
||||
|
@ -293,8 +317,9 @@ impl ProjectDiagnosticsEditor {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(group) = to_insert {
|
||||
if let Some((language_server_id, group)) = to_insert {
|
||||
let mut group_state = DiagnosticGroupState {
|
||||
language_server_id,
|
||||
primary_diagnostic: group.entries[group.primary_ix].clone(),
|
||||
primary_excerpt_ix: 0,
|
||||
excerpts: Default::default(),
|
||||
|
@ -772,26 +797,24 @@ mod tests {
|
|||
};
|
||||
use gpui::{TestAppContext, WindowContext};
|
||||
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
|
||||
use project::FakeFs;
|
||||
use serde_json::json;
|
||||
use unindent::Unindent as _;
|
||||
use workspace::AppState;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
let app_state = cx.update(AppState::test);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
"consts.rs": "
|
||||
Settings::test_async(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
"consts.rs": "
|
||||
const a: i32 = 'a';
|
||||
const b: i32 = c;
|
||||
"
|
||||
.unindent(),
|
||||
.unindent(),
|
||||
|
||||
"main.rs": "
|
||||
"main.rs": "
|
||||
fn main() {
|
||||
let x = vec![];
|
||||
let y = vec![];
|
||||
|
@ -803,19 +826,20 @@ mod tests {
|
|||
d(x);
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
.unindent(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let language_server_id = LanguageServerId(0);
|
||||
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
// Create some diagnostics
|
||||
project.update(cx, |project, cx| {
|
||||
project
|
||||
.update_diagnostic_entries(
|
||||
0,
|
||||
language_server_id,
|
||||
PathBuf::from("/test/main.rs"),
|
||||
None,
|
||||
vec![
|
||||
|
@ -964,10 +988,10 @@ mod tests {
|
|||
|
||||
// Diagnostics are added for another earlier path.
|
||||
project.update(cx, |project, cx| {
|
||||
project.disk_based_diagnostics_started(0, cx);
|
||||
project.disk_based_diagnostics_started(language_server_id, cx);
|
||||
project
|
||||
.update_diagnostic_entries(
|
||||
0,
|
||||
language_server_id,
|
||||
PathBuf::from("/test/consts.rs"),
|
||||
None,
|
||||
vec![DiagnosticEntry {
|
||||
|
@ -984,7 +1008,7 @@ mod tests {
|
|||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
project.disk_based_diagnostics_finished(0, cx);
|
||||
project.disk_based_diagnostics_finished(language_server_id, cx);
|
||||
});
|
||||
|
||||
view.next_notification(cx).await;
|
||||
|
@ -1064,10 +1088,10 @@ mod tests {
|
|||
|
||||
// Diagnostics are added to the first path
|
||||
project.update(cx, |project, cx| {
|
||||
project.disk_based_diagnostics_started(0, cx);
|
||||
project.disk_based_diagnostics_started(language_server_id, cx);
|
||||
project
|
||||
.update_diagnostic_entries(
|
||||
0,
|
||||
language_server_id,
|
||||
PathBuf::from("/test/consts.rs"),
|
||||
None,
|
||||
vec![
|
||||
|
@ -1100,7 +1124,7 @@ mod tests {
|
|||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
project.disk_based_diagnostics_finished(0, cx);
|
||||
project.disk_based_diagnostics_finished(language_server_id, cx);
|
||||
});
|
||||
|
||||
view.next_notification(cx).await;
|
||||
|
@ -1180,6 +1204,272 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
Settings::test_async(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
"main.js": "
|
||||
a();
|
||||
b();
|
||||
c();
|
||||
d();
|
||||
e();
|
||||
".unindent()
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let server_id_1 = LanguageServerId(100);
|
||||
let server_id_2 = LanguageServerId(101);
|
||||
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
let view = cx.add_view(&workspace, |cx| {
|
||||
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
|
||||
});
|
||||
|
||||
// Two language servers start updating diagnostics
|
||||
project.update(cx, |project, cx| {
|
||||
project.disk_based_diagnostics_started(server_id_1, cx);
|
||||
project.disk_based_diagnostics_started(server_id_2, cx);
|
||||
project
|
||||
.update_diagnostic_entries(
|
||||
server_id_1,
|
||||
PathBuf::from("/test/main.js"),
|
||||
None,
|
||||
vec![DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
|
||||
diagnostic: Diagnostic {
|
||||
message: "error 1".to_string(),
|
||||
severity: DiagnosticSeverity::WARNING,
|
||||
is_primary: true,
|
||||
is_disk_based: true,
|
||||
group_id: 1,
|
||||
..Default::default()
|
||||
},
|
||||
}],
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
project
|
||||
.update_diagnostic_entries(
|
||||
server_id_2,
|
||||
PathBuf::from("/test/main.js"),
|
||||
None,
|
||||
vec![DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
|
||||
diagnostic: Diagnostic {
|
||||
message: "warning 1".to_string(),
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
is_primary: true,
|
||||
is_disk_based: true,
|
||||
group_id: 2,
|
||||
..Default::default()
|
||||
},
|
||||
}],
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// The first language server finishes
|
||||
project.update(cx, |project, cx| {
|
||||
project.disk_based_diagnostics_finished(server_id_1, cx);
|
||||
});
|
||||
|
||||
// Only the first language server's diagnostics are shown.
|
||||
cx.foreground().run_until_parked();
|
||||
view.update(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
editor_blocks(&view.editor, cx),
|
||||
[
|
||||
(0, "path header block".into()),
|
||||
(2, "diagnostic header".into()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
concat!(
|
||||
"\n", // filename
|
||||
"\n", // padding
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"a();\n", //
|
||||
"b();",
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// The second language server finishes
|
||||
project.update(cx, |project, cx| {
|
||||
project.disk_based_diagnostics_finished(server_id_2, cx);
|
||||
});
|
||||
|
||||
// Both language server's diagnostics are shown.
|
||||
cx.foreground().run_until_parked();
|
||||
view.update(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
editor_blocks(&view.editor, cx),
|
||||
[
|
||||
(0, "path header block".into()),
|
||||
(2, "diagnostic header".into()),
|
||||
(6, "collapsed context".into()),
|
||||
(7, "diagnostic header".into()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
concat!(
|
||||
"\n", // filename
|
||||
"\n", // padding
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"a();\n", // location
|
||||
"b();\n", //
|
||||
"\n", // collapsed context
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"a();\n", // context
|
||||
"b();\n", //
|
||||
"c();", // context
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// Both language servers start updating diagnostics, and the first server finishes.
|
||||
project.update(cx, |project, cx| {
|
||||
project.disk_based_diagnostics_started(server_id_1, cx);
|
||||
project.disk_based_diagnostics_started(server_id_2, cx);
|
||||
project
|
||||
.update_diagnostic_entries(
|
||||
server_id_1,
|
||||
PathBuf::from("/test/main.js"),
|
||||
None,
|
||||
vec![DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
|
||||
diagnostic: Diagnostic {
|
||||
message: "warning 2".to_string(),
|
||||
severity: DiagnosticSeverity::WARNING,
|
||||
is_primary: true,
|
||||
is_disk_based: true,
|
||||
group_id: 1,
|
||||
..Default::default()
|
||||
},
|
||||
}],
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
project
|
||||
.update_diagnostic_entries(
|
||||
server_id_2,
|
||||
PathBuf::from("/test/main.rs"),
|
||||
None,
|
||||
vec![],
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
project.disk_based_diagnostics_finished(server_id_1, cx);
|
||||
});
|
||||
|
||||
// Only the first language server's diagnostics are updated.
|
||||
cx.foreground().run_until_parked();
|
||||
view.update(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
editor_blocks(&view.editor, cx),
|
||||
[
|
||||
(0, "path header block".into()),
|
||||
(2, "diagnostic header".into()),
|
||||
(7, "collapsed context".into()),
|
||||
(8, "diagnostic header".into()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
concat!(
|
||||
"\n", // filename
|
||||
"\n", // padding
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"a();\n", // location
|
||||
"b();\n", //
|
||||
"c();\n", // context
|
||||
"\n", // collapsed context
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"b();\n", // context
|
||||
"c();\n", //
|
||||
"d();", // context
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// The second language server finishes.
|
||||
project.update(cx, |project, cx| {
|
||||
project
|
||||
.update_diagnostic_entries(
|
||||
server_id_2,
|
||||
PathBuf::from("/test/main.js"),
|
||||
None,
|
||||
vec![DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
|
||||
diagnostic: Diagnostic {
|
||||
message: "warning 2".to_string(),
|
||||
severity: DiagnosticSeverity::WARNING,
|
||||
is_primary: true,
|
||||
is_disk_based: true,
|
||||
group_id: 1,
|
||||
..Default::default()
|
||||
},
|
||||
}],
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
project.disk_based_diagnostics_finished(server_id_2, cx);
|
||||
});
|
||||
|
||||
// Both language servers' diagnostics are updated.
|
||||
cx.foreground().run_until_parked();
|
||||
view.update(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
editor_blocks(&view.editor, cx),
|
||||
[
|
||||
(0, "path header block".into()),
|
||||
(2, "diagnostic header".into()),
|
||||
(7, "collapsed context".into()),
|
||||
(8, "diagnostic header".into()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
concat!(
|
||||
"\n", // filename
|
||||
"\n", // padding
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"b();\n", // location
|
||||
"c();\n", //
|
||||
"d();\n", // context
|
||||
"\n", // collapsed context
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"c();\n", // context
|
||||
"d();\n", //
|
||||
"e();", // context
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
|
|
|
@ -7,6 +7,7 @@ use gpui::{
|
|||
WeakViewHandle,
|
||||
};
|
||||
use language::Diagnostic;
|
||||
use lsp::LanguageServerId;
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use workspace::{item::ItemHandle, StatusItemView};
|
||||
|
@ -15,7 +16,7 @@ pub struct DiagnosticIndicator {
|
|||
summary: project::DiagnosticSummary,
|
||||
active_editor: Option<WeakViewHandle<Editor>>,
|
||||
current_diagnostic: Option<Diagnostic>,
|
||||
in_progress_checks: HashSet<usize>,
|
||||
in_progress_checks: HashSet<LanguageServerId>,
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue