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:
Ihnat Aŭtuška 2024-10-16 16:58:37 +03:00 committed by GitHub
parent eb76065ad3
commit 84df3a0cad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 322 additions and 159 deletions

View file

@ -27,6 +27,7 @@ use language::{
use live_kit_client::MacOSDisplay;
use lsp::LanguageServerId;
use parking_lot::Mutex;
use project::lsp_store::FormatTarget;
use project::{
lsp_store::FormatTrigger, search::SearchQuery, search::SearchResult, DiagnosticSummary,
HoverBlockKind, Project, ProjectPath,
@ -4417,6 +4418,7 @@ async fn test_formatting_buffer(
HashSet::from_iter([buffer_b.clone()]),
true,
FormatTrigger::Save,
FormatTarget::Buffer,
cx,
)
})
@ -4450,6 +4452,7 @@ async fn test_formatting_buffer(
HashSet::from_iter([buffer_b.clone()]),
true,
FormatTrigger::Save,
FormatTarget::Buffer,
cx,
)
})
@ -4555,6 +4558,7 @@ async fn test_prettier_formatting_buffer(
HashSet::from_iter([buffer_b.clone()]),
true,
FormatTrigger::Save,
FormatTarget::Buffer,
cx,
)
})
@ -4574,6 +4578,7 @@ async fn test_prettier_formatting_buffer(
HashSet::from_iter([buffer_a.clone()]),
true,
FormatTrigger::Manual,
FormatTarget::Buffer,
cx,
)
})

View file

@ -237,6 +237,7 @@ gpui::actions!(
ToggleFold,
ToggleFoldRecursive,
Format,
FormatSelections,
GoToDeclaration,
GoToDeclarationSplit,
GoToDefinition,

View file

@ -122,7 +122,7 @@ use multi_buffer::{
use ordered_float::OrderedFloat;
use parking_lot::{Mutex, RwLock};
use project::{
lsp_store::FormatTrigger,
lsp_store::{FormatTarget, FormatTrigger},
project_settings::{GitGutterSetting, ProjectSettings},
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Item, Location,
LocationLink, Project, ProjectPath, ProjectTransaction, TaskSourceKind,
@ -10386,13 +10386,39 @@ impl Editor {
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(
&mut self,
project: Model<Project>,
trigger: FormatTrigger,
target: FormatTarget,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let buffer = self.buffer().clone();
@ -10402,7 +10428,9 @@ impl Editor {
}
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 {
let transaction = futures::select_biased! {

View file

@ -7076,7 +7076,12 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
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,
)
})
.unwrap();
fake_server
@ -7112,7 +7117,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
});
let format = editor
.update(cx, |editor, cx| {
editor.perform_format(project, FormatTrigger::Manual, cx)
editor.perform_format(project, FormatTrigger::Manual, FormatTarget::Buffer, cx)
})
.unwrap();
cx.executor().advance_clock(super::FORMAT_TIMEOUT);
@ -10309,7 +10314,12 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
editor
.update(cx, |editor, cx| {
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
editor.perform_format(
project.clone(),
FormatTrigger::Manual,
FormatTarget::Buffer,
cx,
)
})
.unwrap()
.await;
@ -10323,7 +10333,12 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
});
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();
assert_eq!(

View file

@ -376,6 +376,13 @@ impl EditorElement {
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::cancel_language_server_work);
register_action(view, cx, Editor::show_character_palette);

View file

@ -27,6 +27,7 @@ use rpc::proto::{self, update_view, PeerId};
use settings::Settings;
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
use project::lsp_store::FormatTarget;
use std::{
any::TypeId,
borrow::Cow,
@ -722,7 +723,12 @@ impl Item for Editor {
cx.spawn(|this, mut cx| async move {
if format {
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?;
}

View file

@ -1,5 +1,4 @@
use std::ops::Range;
use crate::actions::FormatSelections;
use crate::{
actions::Format, selections_collection::SelectionsCollection, Copy, CopyPermalinkToLine, Cut,
DisplayPoint, DisplaySnapshot, Editor, EditorMode, FindAllReferences, GoToDeclaration,
@ -8,6 +7,8 @@ use crate::{
};
use gpui::prelude::FluentBuilder;
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
use std::ops::Range;
use text::PointUtf16;
use workspace::OpenInTerminal;
#[derive(Debug)]
@ -164,6 +165,12 @@ pub fn deploy_context_menu(
} else {
"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| {
let builder = menu
.on_blur_subscription(Subscription::new(|| {}))
@ -175,6 +182,9 @@ pub fn deploy_context_menu(
.separator()
.action("Rename Symbol", Box::new(Rename))
.action("Format Buffer", Box::new(Format))
.when(has_selections, |cx| {
cx.action("Format Selections", Box::new(FormatSelections))
})
.action(
"Code Actions",
Box::new(ToggleCodeActions {

View file

@ -72,7 +72,7 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
use text::{Anchor, BufferId, LineEnding};
use text::{Anchor, BufferId, LineEnding, Point, Selection};
use util::{
debug_panic, defer, maybe, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _,
};
@ -96,6 +96,20 @@ pub enum FormatTrigger {
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
// whether they come from a language server or an external command.
#[derive(Debug)]
@ -161,6 +175,7 @@ impl LocalLspStore {
mut buffers: Vec<FormattableBuffer>,
push_to_history: bool,
trigger: FormatTrigger,
target: FormatTarget,
mut cx: AsyncAppContext,
) -> anyhow::Result<ProjectTransaction> {
// Do not allow multiple concurrent formatting requests for the
@ -286,6 +301,7 @@ impl LocalLspStore {
if prettier_settings.allowed {
Self::perform_format(
&Formatter::Prettier,
&target,
server_and_buffer,
lsp_store.clone(),
buffer,
@ -299,6 +315,7 @@ impl LocalLspStore {
} else {
Self::perform_format(
&Formatter::LanguageServer { name: None },
&target,
server_and_buffer,
lsp_store.clone(),
buffer,
@ -310,9 +327,8 @@ impl LocalLspStore {
)
.await
}
}
.log_err()
.flatten();
}?;
if let Some(op) = diff {
format_operations.push(op);
}
@ -321,6 +337,7 @@ impl LocalLspStore {
for formatter in formatters.as_ref() {
let diff = Self::perform_format(
formatter,
&target,
server_and_buffer,
lsp_store.clone(),
buffer,
@ -330,9 +347,7 @@ impl LocalLspStore {
&mut project_transaction,
&mut cx,
)
.await
.log_err()
.flatten();
.await?;
if let Some(op) = diff {
format_operations.push(op);
}
@ -346,6 +361,7 @@ impl LocalLspStore {
for formatter in formatters.as_ref() {
let diff = Self::perform_format(
formatter,
&target,
server_and_buffer,
lsp_store.clone(),
buffer,
@ -355,9 +371,7 @@ impl LocalLspStore {
&mut project_transaction,
&mut cx,
)
.await
.log_err()
.flatten();
.await?;
if let Some(op) = diff {
format_operations.push(op);
}
@ -373,6 +387,7 @@ impl LocalLspStore {
if prettier_settings.allowed {
Self::perform_format(
&Formatter::Prettier,
&target,
server_and_buffer,
lsp_store.clone(),
buffer,
@ -384,8 +399,14 @@ impl LocalLspStore {
)
.await
} else {
let formatter = Formatter::LanguageServer {
name: primary_language_server
.as_ref()
.map(|server| server.name().to_string()),
};
Self::perform_format(
&Formatter::LanguageServer { name: None },
&formatter,
&target,
server_and_buffer,
lsp_store.clone(),
buffer,
@ -397,9 +418,7 @@ impl LocalLspStore {
)
.await
}
}
.log_err()
.flatten();
}?;
if let Some(op) = diff {
format_operations.push(op)
@ -410,6 +429,7 @@ impl LocalLspStore {
// format with formatter
let diff = Self::perform_format(
formatter,
&target,
server_and_buffer,
lsp_store.clone(),
buffer,
@ -419,9 +439,7 @@ impl LocalLspStore {
&mut project_transaction,
&mut cx,
)
.await
.log_err()
.flatten();
.await?;
if let Some(op) = diff {
format_operations.push(op);
}
@ -483,6 +501,7 @@ impl LocalLspStore {
#[allow(clippy::too_many_arguments)]
async fn perform_format(
formatter: &Formatter,
format_target: &FormatTarget,
primary_server_and_buffer: Option<(&Arc<LanguageServer>, &PathBuf)>,
lsp_store: WeakModel<LspStore>,
buffer: &FormattableBuffer,
@ -506,18 +525,33 @@ impl LocalLspStore {
language_server
};
Some(FormatOperation::Lsp(
LspStore::format_via_lsp(
&lsp_store,
&buffer.handle,
buffer_abs_path,
language_server,
settings,
cx,
)
.await
.context("failed to format via language server")?,
))
match format_target {
FormatTarget::Buffer => Some(FormatOperation::Lsp(
LspStore::format_via_lsp(
&lsp_store,
&buffer.handle,
buffer_abs_path,
language_server,
settings,
cx,
)
.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 {
None
}
@ -1859,10 +1893,9 @@ impl LspStore {
} else if matches!(range_formatting_provider, Some(p) if *p != OneOf::Left(false)) {
let buffer_start = lsp::Position::new(0, 0);
let buffer_end = buffer.update(cx, |b, _| point_to_lsp(b.max_point_utf16()))?;
language_server
.request::<lsp::request::RangeFormatting>(lsp::DocumentRangeFormattingParams {
text_document,
text_document: text_document.clone(),
range: lsp::Range::new(buffer_start, buffer_end),
options: lsp_command::lsp_formatting_options(settings),
work_done_progress_params: Default::default(),
@ -1878,7 +1911,62 @@ impl LspStore {
})?
.await
} 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(
server
.request::<lsp::request::WorkspaceSymbolRequest>(
lsp::WorkspaceSymbolParams {
query: query.to_string(),
..Default::default()
},
)
.log_err()
.map(move |response| {
let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
lsp::WorkspaceSymbolResponse::Flat(flat_responses) => {
flat_responses.into_iter().map(|lsp_symbol| {
(lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
}).collect::<Vec<_>>()
}
lsp::WorkspaceSymbolResponse::Nested(nested_responses) => {
nested_responses.into_iter().filter_map(|lsp_symbol| {
let location = match lsp_symbol.location {
OneOf::Left(location) => location,
OneOf::Right(_) => {
log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
return None
}
};
Some((lsp_symbol.name, lsp_symbol.kind, location))
}).collect::<Vec<_>>()
}
}).unwrap_or_default();
server
.request::<lsp::request::WorkspaceSymbolRequest>(
lsp::WorkspaceSymbolParams {
query: query.to_string(),
..Default::default()
},
)
.log_err()
.map(move |response| {
let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
lsp::WorkspaceSymbolResponse::Flat(flat_responses) => {
flat_responses.into_iter().map(|lsp_symbol| {
(lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
}).collect::<Vec<_>>()
}
lsp::WorkspaceSymbolResponse::Nested(nested_responses) => {
nested_responses.into_iter().filter_map(|lsp_symbol| {
let location = match lsp_symbol.location {
OneOf::Left(location) => location,
OneOf::Right(_) => {
log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
return None
}
};
Some((lsp_symbol.name, lsp_symbol.kind, location))
}).collect::<Vec<_>>()
}
}).unwrap_or_default();
WorkspaceSymbolsResult {
lsp_adapter,
language,
worktree: worktree_handle.downgrade(),
worktree_abs_path,
lsp_symbols,
}
}),
);
WorkspaceSymbolsResult {
lsp_adapter,
language,
worktree: worktree_handle.downgrade(),
worktree_abs_path,
lsp_symbols,
}
}),
);
}
cx.spawn(move |this, mut cx| async move {
@ -4579,16 +4667,16 @@ impl LspStore {
if registrations.remove(registration_id).is_some() {
log::info!(
"language server {}: unregistered workspace/DidChangeWatchedFiles capability with id {}",
language_server_id,
registration_id
);
"language server {}: unregistered workspace/DidChangeWatchedFiles capability with id {}",
language_server_id,
registration_id
);
} else {
log::warn!(
"language server {}: failed to unregister workspace/DidChangeWatchedFiles capability with id {}. not registered.",
language_server_id,
registration_id
);
"language server {}: failed to unregister workspace/DidChangeWatchedFiles capability with id {}. not registered.",
language_server_id,
registration_id
);
}
self.rebuild_watched_paths(language_server_id, cx);
@ -5078,6 +5166,7 @@ impl LspStore {
buffers: HashSet<Model<Buffer>>,
push_to_history: bool,
trigger: FormatTrigger,
target: FormatTarget,
cx: &mut ModelContext<Self>,
) -> Task<anyhow::Result<ProjectTransaction>> {
if let Some(_) = self.as_local() {
@ -5114,6 +5203,7 @@ impl LspStore {
formattable_buffers,
push_to_history,
trigger,
target,
cx.clone(),
)
.await;
@ -5172,7 +5262,7 @@ impl LspStore {
buffers.insert(this.buffer_store.read(cx).get_existing(buffer_id)?);
}
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?;
@ -6485,11 +6575,11 @@ impl LspStore {
})?;
let found_snapshot = snapshots
.binary_search_by_key(&version, |e| e.version)
.map(|ix| snapshots[ix].snapshot.clone())
.map_err(|_| {
anyhow!("snapshot not found for buffer {buffer_id} server {server_id} at version {version}")
})?;
.binary_search_by_key(&version, |e| e.version)
.map(|ix| snapshots[ix].snapshot.clone())
.map_err(|_| {
anyhow!("snapshot not found for buffer {buffer_id} server {server_id} at version {version}")
})?;
snapshots.retain(|snapshot| snapshot.version + OLD_VERSIONS_TO_RETAIN >= version);
Ok(found_snapshot)
@ -7203,74 +7293,74 @@ impl LanguageServerWatchedPathsBuilder {
let project = cx.weak_model();
cx.new_model(|cx| {
let this_id = cx.entity_id();
const LSP_ABS_PATH_OBSERVE: Duration = Duration::from_millis(100);
let abs_paths = self
.abs_paths
.into_iter()
.map(|(abs_path, globset)| {
let task = cx.spawn({
let abs_path = abs_path.clone();
let fs = fs.clone();
let this_id = cx.entity_id();
const LSP_ABS_PATH_OBSERVE: Duration = Duration::from_millis(100);
let abs_paths = self
.abs_paths
.into_iter()
.map(|(abs_path, globset)| {
let task = cx.spawn({
let abs_path = abs_path.clone();
let fs = fs.clone();
let lsp_store = project.clone();
|_, mut cx| async move {
maybe!(async move {
let mut push_updates =
fs.watch(&abs_path, LSP_ABS_PATH_OBSERVE).await;
while let Some(update) = push_updates.0.next().await {
let action = lsp_store
.update(&mut cx, |this, cx| {
let Some(local) = this.as_local() else {
return ControlFlow::Break(());
};
let Some(watcher) = local
.language_server_watched_paths
.get(&language_server_id)
else {
return ControlFlow::Break(());
};
if watcher.entity_id() != this_id {
// This watcher is no longer registered on the project, which means that we should
// cease operations.
return ControlFlow::Break(());
}
let (globs, _) = watcher
.read(cx)
.abs_paths
.get(&abs_path)
.expect(
"Watched abs path is not registered with a watcher",
);
let matching_entries = update
.into_iter()
.filter(|event| globs.is_match(&event.path))
.collect::<Vec<_>>();
this.lsp_notify_abs_paths_changed(
language_server_id,
matching_entries,
);
ControlFlow::Continue(())
})
.ok()?;
let lsp_store = project.clone();
|_, mut cx| async move {
maybe!(async move {
let mut push_updates =
fs.watch(&abs_path, LSP_ABS_PATH_OBSERVE).await;
while let Some(update) = push_updates.0.next().await {
let action = lsp_store
.update(&mut cx, |this, cx| {
let Some(local) = this.as_local() else {
return ControlFlow::Break(());
};
let Some(watcher) = local
.language_server_watched_paths
.get(&language_server_id)
else {
return ControlFlow::Break(());
};
if watcher.entity_id() != this_id {
// This watcher is no longer registered on the project, which means that we should
// cease operations.
return ControlFlow::Break(());
}
let (globs, _) = watcher
.read(cx)
.abs_paths
.get(&abs_path)
.expect(
"Watched abs path is not registered with a watcher",
);
let matching_entries = update
.into_iter()
.filter(|event| globs.is_match(&event.path))
.collect::<Vec<_>>();
this.lsp_notify_abs_paths_changed(
language_server_id,
matching_entries,
);
ControlFlow::Continue(())
})
.ok()?;
if action.is_break() {
break;
if action.is_break() {
break;
}
}
}
Some(())
})
.await;
}
});
(abs_path, (globset, task))
})
.collect();
LanguageServerWatchedPaths {
worktree_paths: self.worktree_paths,
abs_paths,
}
})
Some(())
})
.await;
}
});
(abs_path, (globset, task))
})
.collect();
LanguageServerWatchedPaths {
worktree_paths: self.worktree_paths,
abs_paths,
}
})
}
}

View file

@ -2505,10 +2505,11 @@ impl Project {
buffers: HashSet<Model<Buffer>>,
push_to_history: bool,
trigger: lsp_store::FormatTrigger,
target: lsp_store::FormatTarget,
cx: &mut ModelContext<Project>,
) -> Task<anyhow::Result<ProjectTransaction>> {
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)
})
}