Allow formatting selections via LSP (#18752)
Release Notes: - Added a new `editor: format selections` action that allows formatting only the currently selected text via the primary language server. --------- Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
This commit is contained in:
parent
eb76065ad3
commit
84df3a0cad
9 changed files with 322 additions and 159 deletions
|
@ -27,6 +27,7 @@ use language::{
|
||||||
use live_kit_client::MacOSDisplay;
|
use live_kit_client::MacOSDisplay;
|
||||||
use lsp::LanguageServerId;
|
use lsp::LanguageServerId;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use project::lsp_store::FormatTarget;
|
||||||
use project::{
|
use project::{
|
||||||
lsp_store::FormatTrigger, search::SearchQuery, search::SearchResult, DiagnosticSummary,
|
lsp_store::FormatTrigger, search::SearchQuery, search::SearchResult, DiagnosticSummary,
|
||||||
HoverBlockKind, Project, ProjectPath,
|
HoverBlockKind, Project, ProjectPath,
|
||||||
|
@ -4417,6 +4418,7 @@ async fn test_formatting_buffer(
|
||||||
HashSet::from_iter([buffer_b.clone()]),
|
HashSet::from_iter([buffer_b.clone()]),
|
||||||
true,
|
true,
|
||||||
FormatTrigger::Save,
|
FormatTrigger::Save,
|
||||||
|
FormatTarget::Buffer,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -4450,6 +4452,7 @@ async fn test_formatting_buffer(
|
||||||
HashSet::from_iter([buffer_b.clone()]),
|
HashSet::from_iter([buffer_b.clone()]),
|
||||||
true,
|
true,
|
||||||
FormatTrigger::Save,
|
FormatTrigger::Save,
|
||||||
|
FormatTarget::Buffer,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -4555,6 +4558,7 @@ async fn test_prettier_formatting_buffer(
|
||||||
HashSet::from_iter([buffer_b.clone()]),
|
HashSet::from_iter([buffer_b.clone()]),
|
||||||
true,
|
true,
|
||||||
FormatTrigger::Save,
|
FormatTrigger::Save,
|
||||||
|
FormatTarget::Buffer,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -4574,6 +4578,7 @@ async fn test_prettier_formatting_buffer(
|
||||||
HashSet::from_iter([buffer_a.clone()]),
|
HashSet::from_iter([buffer_a.clone()]),
|
||||||
true,
|
true,
|
||||||
FormatTrigger::Manual,
|
FormatTrigger::Manual,
|
||||||
|
FormatTarget::Buffer,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -237,6 +237,7 @@ gpui::actions!(
|
||||||
ToggleFold,
|
ToggleFold,
|
||||||
ToggleFoldRecursive,
|
ToggleFoldRecursive,
|
||||||
Format,
|
Format,
|
||||||
|
FormatSelections,
|
||||||
GoToDeclaration,
|
GoToDeclaration,
|
||||||
GoToDeclarationSplit,
|
GoToDeclarationSplit,
|
||||||
GoToDefinition,
|
GoToDefinition,
|
||||||
|
|
|
@ -122,7 +122,7 @@ use multi_buffer::{
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
use project::{
|
use project::{
|
||||||
lsp_store::FormatTrigger,
|
lsp_store::{FormatTarget, FormatTrigger},
|
||||||
project_settings::{GitGutterSetting, ProjectSettings},
|
project_settings::{GitGutterSetting, ProjectSettings},
|
||||||
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Item, Location,
|
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Item, Location,
|
||||||
LocationLink, Project, ProjectPath, ProjectTransaction, TaskSourceKind,
|
LocationLink, Project, ProjectPath, ProjectTransaction, TaskSourceKind,
|
||||||
|
@ -10386,13 +10386,39 @@ impl Editor {
|
||||||
None => return None,
|
None => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(self.perform_format(project, FormatTrigger::Manual, cx))
|
Some(self.perform_format(project, FormatTrigger::Manual, FormatTarget::Buffer, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_selections(
|
||||||
|
&mut self,
|
||||||
|
_: &FormatSelections,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<Task<Result<()>>> {
|
||||||
|
let project = match &self.project {
|
||||||
|
Some(project) => project.clone(),
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let selections = self
|
||||||
|
.selections
|
||||||
|
.all_adjusted(cx)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
|
Some(self.perform_format(
|
||||||
|
project,
|
||||||
|
FormatTrigger::Manual,
|
||||||
|
FormatTarget::Ranges(selections),
|
||||||
|
cx,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn perform_format(
|
fn perform_format(
|
||||||
&mut self,
|
&mut self,
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
trigger: FormatTrigger,
|
trigger: FormatTrigger,
|
||||||
|
target: FormatTarget,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let buffer = self.buffer().clone();
|
let buffer = self.buffer().clone();
|
||||||
|
@ -10402,7 +10428,9 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut timeout = cx.background_executor().timer(FORMAT_TIMEOUT).fuse();
|
let mut timeout = cx.background_executor().timer(FORMAT_TIMEOUT).fuse();
|
||||||
let format = project.update(cx, |project, cx| project.format(buffers, true, trigger, cx));
|
let format = project.update(cx, |project, cx| {
|
||||||
|
project.format(buffers, true, trigger, target, cx)
|
||||||
|
});
|
||||||
|
|
||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
let transaction = futures::select_biased! {
|
let transaction = futures::select_biased! {
|
||||||
|
|
|
@ -7076,7 +7076,12 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||||
|
|
||||||
let format = editor
|
let format = editor
|
||||||
.update(cx, |editor, cx| {
|
.update(cx, |editor, cx| {
|
||||||
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
|
editor.perform_format(
|
||||||
|
project.clone(),
|
||||||
|
FormatTrigger::Manual,
|
||||||
|
FormatTarget::Buffer,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
fake_server
|
fake_server
|
||||||
|
@ -7112,7 +7117,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||||
});
|
});
|
||||||
let format = editor
|
let format = editor
|
||||||
.update(cx, |editor, cx| {
|
.update(cx, |editor, cx| {
|
||||||
editor.perform_format(project, FormatTrigger::Manual, cx)
|
editor.perform_format(project, FormatTrigger::Manual, FormatTarget::Buffer, cx)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
cx.executor().advance_clock(super::FORMAT_TIMEOUT);
|
cx.executor().advance_clock(super::FORMAT_TIMEOUT);
|
||||||
|
@ -10309,7 +10314,12 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
||||||
|
|
||||||
editor
|
editor
|
||||||
.update(cx, |editor, cx| {
|
.update(cx, |editor, cx| {
|
||||||
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
|
editor.perform_format(
|
||||||
|
project.clone(),
|
||||||
|
FormatTrigger::Manual,
|
||||||
|
FormatTarget::Buffer,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.await;
|
.await;
|
||||||
|
@ -10323,7 +10333,12 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
||||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
|
settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
|
||||||
});
|
});
|
||||||
let format = editor.update(cx, |editor, cx| {
|
let format = editor.update(cx, |editor, cx| {
|
||||||
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
|
editor.perform_format(
|
||||||
|
project.clone(),
|
||||||
|
FormatTrigger::Manual,
|
||||||
|
FormatTarget::Buffer,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
});
|
});
|
||||||
format.await.unwrap();
|
format.await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -376,6 +376,13 @@ impl EditorElement {
|
||||||
cx.propagate();
|
cx.propagate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
register_action(view, cx, |editor, action, cx| {
|
||||||
|
if let Some(task) = editor.format_selections(action, cx) {
|
||||||
|
task.detach_and_log_err(cx);
|
||||||
|
} else {
|
||||||
|
cx.propagate();
|
||||||
|
}
|
||||||
|
});
|
||||||
register_action(view, cx, Editor::restart_language_server);
|
register_action(view, cx, Editor::restart_language_server);
|
||||||
register_action(view, cx, Editor::cancel_language_server_work);
|
register_action(view, cx, Editor::cancel_language_server_work);
|
||||||
register_action(view, cx, Editor::show_character_palette);
|
register_action(view, cx, Editor::show_character_palette);
|
||||||
|
|
|
@ -27,6 +27,7 @@ use rpc::proto::{self, update_view, PeerId};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
|
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
|
||||||
|
|
||||||
|
use project::lsp_store::FormatTarget;
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
@ -722,7 +723,12 @@ impl Item for Editor {
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
if format {
|
if format {
|
||||||
this.update(&mut cx, |editor, cx| {
|
this.update(&mut cx, |editor, cx| {
|
||||||
editor.perform_format(project.clone(), FormatTrigger::Save, cx)
|
editor.perform_format(
|
||||||
|
project.clone(),
|
||||||
|
FormatTrigger::Save,
|
||||||
|
FormatTarget::Buffer,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
})?
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use std::ops::Range;
|
use crate::actions::FormatSelections;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actions::Format, selections_collection::SelectionsCollection, Copy, CopyPermalinkToLine, Cut,
|
actions::Format, selections_collection::SelectionsCollection, Copy, CopyPermalinkToLine, Cut,
|
||||||
DisplayPoint, DisplaySnapshot, Editor, EditorMode, FindAllReferences, GoToDeclaration,
|
DisplayPoint, DisplaySnapshot, Editor, EditorMode, FindAllReferences, GoToDeclaration,
|
||||||
|
@ -8,6 +7,8 @@ use crate::{
|
||||||
};
|
};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
|
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
|
||||||
|
use std::ops::Range;
|
||||||
|
use text::PointUtf16;
|
||||||
use workspace::OpenInTerminal;
|
use workspace::OpenInTerminal;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -164,6 +165,12 @@ pub fn deploy_context_menu(
|
||||||
} else {
|
} else {
|
||||||
"Reveal in File Manager"
|
"Reveal in File Manager"
|
||||||
};
|
};
|
||||||
|
let has_selections = editor
|
||||||
|
.selections
|
||||||
|
.all::<PointUtf16>(cx)
|
||||||
|
.into_iter()
|
||||||
|
.any(|s| !s.is_empty());
|
||||||
|
|
||||||
ui::ContextMenu::build(cx, |menu, _cx| {
|
ui::ContextMenu::build(cx, |menu, _cx| {
|
||||||
let builder = menu
|
let builder = menu
|
||||||
.on_blur_subscription(Subscription::new(|| {}))
|
.on_blur_subscription(Subscription::new(|| {}))
|
||||||
|
@ -175,6 +182,9 @@ pub fn deploy_context_menu(
|
||||||
.separator()
|
.separator()
|
||||||
.action("Rename Symbol", Box::new(Rename))
|
.action("Rename Symbol", Box::new(Rename))
|
||||||
.action("Format Buffer", Box::new(Format))
|
.action("Format Buffer", Box::new(Format))
|
||||||
|
.when(has_selections, |cx| {
|
||||||
|
cx.action("Format Selections", Box::new(FormatSelections))
|
||||||
|
})
|
||||||
.action(
|
.action(
|
||||||
"Code Actions",
|
"Code Actions",
|
||||||
Box::new(ToggleCodeActions {
|
Box::new(ToggleCodeActions {
|
||||||
|
|
|
@ -72,7 +72,7 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use text::{Anchor, BufferId, LineEnding};
|
use text::{Anchor, BufferId, LineEnding, Point, Selection};
|
||||||
use util::{
|
use util::{
|
||||||
debug_panic, defer, maybe, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _,
|
debug_panic, defer, maybe, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _,
|
||||||
};
|
};
|
||||||
|
@ -96,6 +96,20 @@ pub enum FormatTrigger {
|
||||||
Manual,
|
Manual,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum FormatTarget {
|
||||||
|
Buffer,
|
||||||
|
Ranges(Vec<Selection<Point>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormatTarget {
|
||||||
|
pub fn as_selections(&self) -> Option<&[Selection<Point>]> {
|
||||||
|
match self {
|
||||||
|
FormatTarget::Buffer => None,
|
||||||
|
FormatTarget::Ranges(selections) => Some(selections.as_slice()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Currently, formatting operations are represented differently depending on
|
// Currently, formatting operations are represented differently depending on
|
||||||
// whether they come from a language server or an external command.
|
// whether they come from a language server or an external command.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -161,6 +175,7 @@ impl LocalLspStore {
|
||||||
mut buffers: Vec<FormattableBuffer>,
|
mut buffers: Vec<FormattableBuffer>,
|
||||||
push_to_history: bool,
|
push_to_history: bool,
|
||||||
trigger: FormatTrigger,
|
trigger: FormatTrigger,
|
||||||
|
target: FormatTarget,
|
||||||
mut cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
) -> anyhow::Result<ProjectTransaction> {
|
) -> anyhow::Result<ProjectTransaction> {
|
||||||
// Do not allow multiple concurrent formatting requests for the
|
// Do not allow multiple concurrent formatting requests for the
|
||||||
|
@ -286,6 +301,7 @@ impl LocalLspStore {
|
||||||
if prettier_settings.allowed {
|
if prettier_settings.allowed {
|
||||||
Self::perform_format(
|
Self::perform_format(
|
||||||
&Formatter::Prettier,
|
&Formatter::Prettier,
|
||||||
|
&target,
|
||||||
server_and_buffer,
|
server_and_buffer,
|
||||||
lsp_store.clone(),
|
lsp_store.clone(),
|
||||||
buffer,
|
buffer,
|
||||||
|
@ -299,6 +315,7 @@ impl LocalLspStore {
|
||||||
} else {
|
} else {
|
||||||
Self::perform_format(
|
Self::perform_format(
|
||||||
&Formatter::LanguageServer { name: None },
|
&Formatter::LanguageServer { name: None },
|
||||||
|
&target,
|
||||||
server_and_buffer,
|
server_and_buffer,
|
||||||
lsp_store.clone(),
|
lsp_store.clone(),
|
||||||
buffer,
|
buffer,
|
||||||
|
@ -310,9 +327,8 @@ impl LocalLspStore {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}?;
|
||||||
.log_err()
|
|
||||||
.flatten();
|
|
||||||
if let Some(op) = diff {
|
if let Some(op) = diff {
|
||||||
format_operations.push(op);
|
format_operations.push(op);
|
||||||
}
|
}
|
||||||
|
@ -321,6 +337,7 @@ impl LocalLspStore {
|
||||||
for formatter in formatters.as_ref() {
|
for formatter in formatters.as_ref() {
|
||||||
let diff = Self::perform_format(
|
let diff = Self::perform_format(
|
||||||
formatter,
|
formatter,
|
||||||
|
&target,
|
||||||
server_and_buffer,
|
server_and_buffer,
|
||||||
lsp_store.clone(),
|
lsp_store.clone(),
|
||||||
buffer,
|
buffer,
|
||||||
|
@ -330,9 +347,7 @@ impl LocalLspStore {
|
||||||
&mut project_transaction,
|
&mut project_transaction,
|
||||||
&mut cx,
|
&mut cx,
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.log_err()
|
|
||||||
.flatten();
|
|
||||||
if let Some(op) = diff {
|
if let Some(op) = diff {
|
||||||
format_operations.push(op);
|
format_operations.push(op);
|
||||||
}
|
}
|
||||||
|
@ -346,6 +361,7 @@ impl LocalLspStore {
|
||||||
for formatter in formatters.as_ref() {
|
for formatter in formatters.as_ref() {
|
||||||
let diff = Self::perform_format(
|
let diff = Self::perform_format(
|
||||||
formatter,
|
formatter,
|
||||||
|
&target,
|
||||||
server_and_buffer,
|
server_and_buffer,
|
||||||
lsp_store.clone(),
|
lsp_store.clone(),
|
||||||
buffer,
|
buffer,
|
||||||
|
@ -355,9 +371,7 @@ impl LocalLspStore {
|
||||||
&mut project_transaction,
|
&mut project_transaction,
|
||||||
&mut cx,
|
&mut cx,
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.log_err()
|
|
||||||
.flatten();
|
|
||||||
if let Some(op) = diff {
|
if let Some(op) = diff {
|
||||||
format_operations.push(op);
|
format_operations.push(op);
|
||||||
}
|
}
|
||||||
|
@ -373,6 +387,7 @@ impl LocalLspStore {
|
||||||
if prettier_settings.allowed {
|
if prettier_settings.allowed {
|
||||||
Self::perform_format(
|
Self::perform_format(
|
||||||
&Formatter::Prettier,
|
&Formatter::Prettier,
|
||||||
|
&target,
|
||||||
server_and_buffer,
|
server_and_buffer,
|
||||||
lsp_store.clone(),
|
lsp_store.clone(),
|
||||||
buffer,
|
buffer,
|
||||||
|
@ -384,8 +399,14 @@ impl LocalLspStore {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
|
let formatter = Formatter::LanguageServer {
|
||||||
|
name: primary_language_server
|
||||||
|
.as_ref()
|
||||||
|
.map(|server| server.name().to_string()),
|
||||||
|
};
|
||||||
Self::perform_format(
|
Self::perform_format(
|
||||||
&Formatter::LanguageServer { name: None },
|
&formatter,
|
||||||
|
&target,
|
||||||
server_and_buffer,
|
server_and_buffer,
|
||||||
lsp_store.clone(),
|
lsp_store.clone(),
|
||||||
buffer,
|
buffer,
|
||||||
|
@ -397,9 +418,7 @@ impl LocalLspStore {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}?;
|
||||||
.log_err()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
if let Some(op) = diff {
|
if let Some(op) = diff {
|
||||||
format_operations.push(op)
|
format_operations.push(op)
|
||||||
|
@ -410,6 +429,7 @@ impl LocalLspStore {
|
||||||
// format with formatter
|
// format with formatter
|
||||||
let diff = Self::perform_format(
|
let diff = Self::perform_format(
|
||||||
formatter,
|
formatter,
|
||||||
|
&target,
|
||||||
server_and_buffer,
|
server_and_buffer,
|
||||||
lsp_store.clone(),
|
lsp_store.clone(),
|
||||||
buffer,
|
buffer,
|
||||||
|
@ -419,9 +439,7 @@ impl LocalLspStore {
|
||||||
&mut project_transaction,
|
&mut project_transaction,
|
||||||
&mut cx,
|
&mut cx,
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.log_err()
|
|
||||||
.flatten();
|
|
||||||
if let Some(op) = diff {
|
if let Some(op) = diff {
|
||||||
format_operations.push(op);
|
format_operations.push(op);
|
||||||
}
|
}
|
||||||
|
@ -483,6 +501,7 @@ impl LocalLspStore {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn perform_format(
|
async fn perform_format(
|
||||||
formatter: &Formatter,
|
formatter: &Formatter,
|
||||||
|
format_target: &FormatTarget,
|
||||||
primary_server_and_buffer: Option<(&Arc<LanguageServer>, &PathBuf)>,
|
primary_server_and_buffer: Option<(&Arc<LanguageServer>, &PathBuf)>,
|
||||||
lsp_store: WeakModel<LspStore>,
|
lsp_store: WeakModel<LspStore>,
|
||||||
buffer: &FormattableBuffer,
|
buffer: &FormattableBuffer,
|
||||||
|
@ -506,18 +525,33 @@ impl LocalLspStore {
|
||||||
language_server
|
language_server
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(FormatOperation::Lsp(
|
match format_target {
|
||||||
LspStore::format_via_lsp(
|
FormatTarget::Buffer => Some(FormatOperation::Lsp(
|
||||||
&lsp_store,
|
LspStore::format_via_lsp(
|
||||||
&buffer.handle,
|
&lsp_store,
|
||||||
buffer_abs_path,
|
&buffer.handle,
|
||||||
language_server,
|
buffer_abs_path,
|
||||||
settings,
|
language_server,
|
||||||
cx,
|
settings,
|
||||||
)
|
cx,
|
||||||
.await
|
)
|
||||||
.context("failed to format via language server")?,
|
.await
|
||||||
))
|
.context("failed to format via language server")?,
|
||||||
|
)),
|
||||||
|
FormatTarget::Ranges(selections) => Some(FormatOperation::Lsp(
|
||||||
|
LspStore::format_range_via_lsp(
|
||||||
|
&lsp_store,
|
||||||
|
&buffer.handle,
|
||||||
|
selections.as_slice(),
|
||||||
|
buffer_abs_path,
|
||||||
|
language_server,
|
||||||
|
settings,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("failed to format ranges via language server")?,
|
||||||
|
)),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -1859,10 +1893,9 @@ impl LspStore {
|
||||||
} else if matches!(range_formatting_provider, Some(p) if *p != OneOf::Left(false)) {
|
} else if matches!(range_formatting_provider, Some(p) if *p != OneOf::Left(false)) {
|
||||||
let buffer_start = lsp::Position::new(0, 0);
|
let buffer_start = lsp::Position::new(0, 0);
|
||||||
let buffer_end = buffer.update(cx, |b, _| point_to_lsp(b.max_point_utf16()))?;
|
let buffer_end = buffer.update(cx, |b, _| point_to_lsp(b.max_point_utf16()))?;
|
||||||
|
|
||||||
language_server
|
language_server
|
||||||
.request::<lsp::request::RangeFormatting>(lsp::DocumentRangeFormattingParams {
|
.request::<lsp::request::RangeFormatting>(lsp::DocumentRangeFormattingParams {
|
||||||
text_document,
|
text_document: text_document.clone(),
|
||||||
range: lsp::Range::new(buffer_start, buffer_end),
|
range: lsp::Range::new(buffer_start, buffer_end),
|
||||||
options: lsp_command::lsp_formatting_options(settings),
|
options: lsp_command::lsp_formatting_options(settings),
|
||||||
work_done_progress_params: Default::default(),
|
work_done_progress_params: Default::default(),
|
||||||
|
@ -1878,7 +1911,62 @@ impl LspStore {
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
Ok(Vec::new())
|
Ok(Vec::with_capacity(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub async fn format_range_via_lsp(
|
||||||
|
this: &WeakModel<Self>,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
selections: &[Selection<Point>],
|
||||||
|
abs_path: &Path,
|
||||||
|
language_server: &Arc<LanguageServer>,
|
||||||
|
settings: &LanguageSettings,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<Vec<(Range<Anchor>, String)>> {
|
||||||
|
let capabilities = &language_server.capabilities();
|
||||||
|
let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref();
|
||||||
|
if range_formatting_provider.map_or(false, |provider| provider == &OneOf::Left(false)) {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"{} language server does not support range formatting",
|
||||||
|
language_server.name()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let uri = lsp::Url::from_file_path(abs_path)
|
||||||
|
.map_err(|_| anyhow!("failed to convert abs path to uri"))?;
|
||||||
|
let text_document = lsp::TextDocumentIdentifier::new(uri);
|
||||||
|
|
||||||
|
let lsp_edits = {
|
||||||
|
let ranges = selections.into_iter().map(|s| {
|
||||||
|
let start = lsp::Position::new(s.start.row, s.start.column);
|
||||||
|
let end = lsp::Position::new(s.end.row, s.end.column);
|
||||||
|
lsp::Range::new(start, end)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut edits = None;
|
||||||
|
for range in ranges {
|
||||||
|
if let Some(mut edit) = language_server
|
||||||
|
.request::<lsp::request::RangeFormatting>(lsp::DocumentRangeFormattingParams {
|
||||||
|
text_document: text_document.clone(),
|
||||||
|
range,
|
||||||
|
options: lsp_command::lsp_formatting_options(settings),
|
||||||
|
work_done_progress_params: Default::default(),
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
edits.get_or_insert_with(Vec::new).append(&mut edit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
edits
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(lsp_edits) = lsp_edits {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.edits_from_lsp(buffer, lsp_edits, language_server.server_id(), None, cx)
|
||||||
|
})?
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
Ok(Vec::with_capacity(0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2648,44 +2736,44 @@ impl LspStore {
|
||||||
};
|
};
|
||||||
|
|
||||||
requests.push(
|
requests.push(
|
||||||
server
|
server
|
||||||
.request::<lsp::request::WorkspaceSymbolRequest>(
|
.request::<lsp::request::WorkspaceSymbolRequest>(
|
||||||
lsp::WorkspaceSymbolParams {
|
lsp::WorkspaceSymbolParams {
|
||||||
query: query.to_string(),
|
query: query.to_string(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.log_err()
|
.log_err()
|
||||||
.map(move |response| {
|
.map(move |response| {
|
||||||
let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
|
let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
|
||||||
lsp::WorkspaceSymbolResponse::Flat(flat_responses) => {
|
lsp::WorkspaceSymbolResponse::Flat(flat_responses) => {
|
||||||
flat_responses.into_iter().map(|lsp_symbol| {
|
flat_responses.into_iter().map(|lsp_symbol| {
|
||||||
(lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
|
(lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
|
||||||
}).collect::<Vec<_>>()
|
}).collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
lsp::WorkspaceSymbolResponse::Nested(nested_responses) => {
|
lsp::WorkspaceSymbolResponse::Nested(nested_responses) => {
|
||||||
nested_responses.into_iter().filter_map(|lsp_symbol| {
|
nested_responses.into_iter().filter_map(|lsp_symbol| {
|
||||||
let location = match lsp_symbol.location {
|
let location = match lsp_symbol.location {
|
||||||
OneOf::Left(location) => location,
|
OneOf::Left(location) => location,
|
||||||
OneOf::Right(_) => {
|
OneOf::Right(_) => {
|
||||||
log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
|
log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
|
||||||
return None
|
return None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Some((lsp_symbol.name, lsp_symbol.kind, location))
|
Some((lsp_symbol.name, lsp_symbol.kind, location))
|
||||||
}).collect::<Vec<_>>()
|
}).collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
}).unwrap_or_default();
|
}).unwrap_or_default();
|
||||||
|
|
||||||
WorkspaceSymbolsResult {
|
WorkspaceSymbolsResult {
|
||||||
lsp_adapter,
|
lsp_adapter,
|
||||||
language,
|
language,
|
||||||
worktree: worktree_handle.downgrade(),
|
worktree: worktree_handle.downgrade(),
|
||||||
worktree_abs_path,
|
worktree_abs_path,
|
||||||
lsp_symbols,
|
lsp_symbols,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.spawn(move |this, mut cx| async move {
|
cx.spawn(move |this, mut cx| async move {
|
||||||
|
@ -4579,16 +4667,16 @@ impl LspStore {
|
||||||
|
|
||||||
if registrations.remove(registration_id).is_some() {
|
if registrations.remove(registration_id).is_some() {
|
||||||
log::info!(
|
log::info!(
|
||||||
"language server {}: unregistered workspace/DidChangeWatchedFiles capability with id {}",
|
"language server {}: unregistered workspace/DidChangeWatchedFiles capability with id {}",
|
||||||
language_server_id,
|
language_server_id,
|
||||||
registration_id
|
registration_id
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"language server {}: failed to unregister workspace/DidChangeWatchedFiles capability with id {}. not registered.",
|
"language server {}: failed to unregister workspace/DidChangeWatchedFiles capability with id {}. not registered.",
|
||||||
language_server_id,
|
language_server_id,
|
||||||
registration_id
|
registration_id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.rebuild_watched_paths(language_server_id, cx);
|
self.rebuild_watched_paths(language_server_id, cx);
|
||||||
|
@ -5078,6 +5166,7 @@ impl LspStore {
|
||||||
buffers: HashSet<Model<Buffer>>,
|
buffers: HashSet<Model<Buffer>>,
|
||||||
push_to_history: bool,
|
push_to_history: bool,
|
||||||
trigger: FormatTrigger,
|
trigger: FormatTrigger,
|
||||||
|
target: FormatTarget,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<anyhow::Result<ProjectTransaction>> {
|
) -> Task<anyhow::Result<ProjectTransaction>> {
|
||||||
if let Some(_) = self.as_local() {
|
if let Some(_) = self.as_local() {
|
||||||
|
@ -5114,6 +5203,7 @@ impl LspStore {
|
||||||
formattable_buffers,
|
formattable_buffers,
|
||||||
push_to_history,
|
push_to_history,
|
||||||
trigger,
|
trigger,
|
||||||
|
target,
|
||||||
cx.clone(),
|
cx.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
@ -5172,7 +5262,7 @@ impl LspStore {
|
||||||
buffers.insert(this.buffer_store.read(cx).get_existing(buffer_id)?);
|
buffers.insert(this.buffer_store.read(cx).get_existing(buffer_id)?);
|
||||||
}
|
}
|
||||||
let trigger = FormatTrigger::from_proto(envelope.payload.trigger);
|
let trigger = FormatTrigger::from_proto(envelope.payload.trigger);
|
||||||
Ok::<_, anyhow::Error>(this.format(buffers, false, trigger, cx))
|
Ok::<_, anyhow::Error>(this.format(buffers, false, trigger, FormatTarget::Buffer, cx))
|
||||||
})??;
|
})??;
|
||||||
|
|
||||||
let project_transaction = format.await?;
|
let project_transaction = format.await?;
|
||||||
|
@ -6485,11 +6575,11 @@ impl LspStore {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let found_snapshot = snapshots
|
let found_snapshot = snapshots
|
||||||
.binary_search_by_key(&version, |e| e.version)
|
.binary_search_by_key(&version, |e| e.version)
|
||||||
.map(|ix| snapshots[ix].snapshot.clone())
|
.map(|ix| snapshots[ix].snapshot.clone())
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
anyhow!("snapshot not found for buffer {buffer_id} server {server_id} at version {version}")
|
anyhow!("snapshot not found for buffer {buffer_id} server {server_id} at version {version}")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
snapshots.retain(|snapshot| snapshot.version + OLD_VERSIONS_TO_RETAIN >= version);
|
snapshots.retain(|snapshot| snapshot.version + OLD_VERSIONS_TO_RETAIN >= version);
|
||||||
Ok(found_snapshot)
|
Ok(found_snapshot)
|
||||||
|
@ -7203,74 +7293,74 @@ impl LanguageServerWatchedPathsBuilder {
|
||||||
let project = cx.weak_model();
|
let project = cx.weak_model();
|
||||||
|
|
||||||
cx.new_model(|cx| {
|
cx.new_model(|cx| {
|
||||||
let this_id = cx.entity_id();
|
let this_id = cx.entity_id();
|
||||||
const LSP_ABS_PATH_OBSERVE: Duration = Duration::from_millis(100);
|
const LSP_ABS_PATH_OBSERVE: Duration = Duration::from_millis(100);
|
||||||
let abs_paths = self
|
let abs_paths = self
|
||||||
.abs_paths
|
.abs_paths
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(abs_path, globset)| {
|
.map(|(abs_path, globset)| {
|
||||||
let task = cx.spawn({
|
let task = cx.spawn({
|
||||||
let abs_path = abs_path.clone();
|
let abs_path = abs_path.clone();
|
||||||
let fs = fs.clone();
|
let fs = fs.clone();
|
||||||
|
|
||||||
let lsp_store = project.clone();
|
let lsp_store = project.clone();
|
||||||
|_, mut cx| async move {
|
|_, mut cx| async move {
|
||||||
maybe!(async move {
|
maybe!(async move {
|
||||||
let mut push_updates =
|
let mut push_updates =
|
||||||
fs.watch(&abs_path, LSP_ABS_PATH_OBSERVE).await;
|
fs.watch(&abs_path, LSP_ABS_PATH_OBSERVE).await;
|
||||||
while let Some(update) = push_updates.0.next().await {
|
while let Some(update) = push_updates.0.next().await {
|
||||||
let action = lsp_store
|
let action = lsp_store
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |this, cx| {
|
||||||
let Some(local) = this.as_local() else {
|
let Some(local) = this.as_local() else {
|
||||||
return ControlFlow::Break(());
|
return ControlFlow::Break(());
|
||||||
};
|
};
|
||||||
let Some(watcher) = local
|
let Some(watcher) = local
|
||||||
.language_server_watched_paths
|
.language_server_watched_paths
|
||||||
.get(&language_server_id)
|
.get(&language_server_id)
|
||||||
else {
|
else {
|
||||||
return ControlFlow::Break(());
|
return ControlFlow::Break(());
|
||||||
};
|
};
|
||||||
if watcher.entity_id() != this_id {
|
if watcher.entity_id() != this_id {
|
||||||
// This watcher is no longer registered on the project, which means that we should
|
// This watcher is no longer registered on the project, which means that we should
|
||||||
// cease operations.
|
// cease operations.
|
||||||
return ControlFlow::Break(());
|
return ControlFlow::Break(());
|
||||||
}
|
}
|
||||||
let (globs, _) = watcher
|
let (globs, _) = watcher
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.abs_paths
|
.abs_paths
|
||||||
.get(&abs_path)
|
.get(&abs_path)
|
||||||
.expect(
|
.expect(
|
||||||
"Watched abs path is not registered with a watcher",
|
"Watched abs path is not registered with a watcher",
|
||||||
);
|
);
|
||||||
let matching_entries = update
|
let matching_entries = update
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|event| globs.is_match(&event.path))
|
.filter(|event| globs.is_match(&event.path))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
this.lsp_notify_abs_paths_changed(
|
this.lsp_notify_abs_paths_changed(
|
||||||
language_server_id,
|
language_server_id,
|
||||||
matching_entries,
|
matching_entries,
|
||||||
);
|
);
|
||||||
ControlFlow::Continue(())
|
ControlFlow::Continue(())
|
||||||
})
|
})
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
if action.is_break() {
|
if action.is_break() {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
Some(())
|
||||||
Some(())
|
})
|
||||||
})
|
.await;
|
||||||
.await;
|
}
|
||||||
}
|
});
|
||||||
});
|
(abs_path, (globset, task))
|
||||||
(abs_path, (globset, task))
|
})
|
||||||
})
|
.collect();
|
||||||
.collect();
|
LanguageServerWatchedPaths {
|
||||||
LanguageServerWatchedPaths {
|
worktree_paths: self.worktree_paths,
|
||||||
worktree_paths: self.worktree_paths,
|
abs_paths,
|
||||||
abs_paths,
|
}
|
||||||
}
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2505,10 +2505,11 @@ impl Project {
|
||||||
buffers: HashSet<Model<Buffer>>,
|
buffers: HashSet<Model<Buffer>>,
|
||||||
push_to_history: bool,
|
push_to_history: bool,
|
||||||
trigger: lsp_store::FormatTrigger,
|
trigger: lsp_store::FormatTrigger,
|
||||||
|
target: lsp_store::FormatTarget,
|
||||||
cx: &mut ModelContext<Project>,
|
cx: &mut ModelContext<Project>,
|
||||||
) -> Task<anyhow::Result<ProjectTransaction>> {
|
) -> Task<anyhow::Result<ProjectTransaction>> {
|
||||||
self.lsp_store.update(cx, |lsp_store, cx| {
|
self.lsp_store.update(cx, |lsp_store, cx| {
|
||||||
lsp_store.format(buffers, push_to_history, trigger, cx)
|
lsp_store.format(buffers, push_to_history, trigger, target, cx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue