diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index 65c6954d..b1a73247 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -4,7 +4,7 @@ use crate::{
     Call, Error, OffsetEncoding, Result,
 };
 
-use helix_core::{find_workspace, path, ChangeSet, Rope};
+use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope};
 use helix_loader::{self, VERSION_AND_GIT_HASH};
 use lsp::{
     notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
@@ -276,6 +276,93 @@ impl Client {
             .expect("language server not yet initialized!")
     }
 
+    #[inline] // TODO inline?
+    pub fn supports_feature(&self, feature: LanguageServerFeature) -> bool {
+        let capabilities = match self.capabilities.get() {
+            Some(capabilities) => capabilities,
+            None => return false, // not initialized, TODO unwrap/expect instead?
+        };
+        match feature {
+            LanguageServerFeature::Format => matches!(
+                capabilities.document_formatting_provider,
+                Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
+            ),
+            LanguageServerFeature::GotoDeclaration => matches!(
+                capabilities.declaration_provider,
+                Some(
+                    lsp::DeclarationCapability::Simple(true)
+                        | lsp::DeclarationCapability::RegistrationOptions(_)
+                        | lsp::DeclarationCapability::Options(_),
+                )
+            ),
+            LanguageServerFeature::GotoDefinition => matches!(
+                capabilities.definition_provider,
+                Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
+            ),
+            LanguageServerFeature::GotoTypeDefinition => matches!(
+                capabilities.type_definition_provider,
+                Some(
+                    lsp::TypeDefinitionProviderCapability::Simple(true)
+                        | lsp::TypeDefinitionProviderCapability::Options(_),
+                )
+            ),
+            LanguageServerFeature::GotoReference => matches!(
+                capabilities.references_provider,
+                Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
+            ),
+            LanguageServerFeature::GotoImplementation => matches!(
+                capabilities.implementation_provider,
+                Some(
+                    lsp::ImplementationProviderCapability::Simple(true)
+                        | lsp::ImplementationProviderCapability::Options(_),
+                )
+            ),
+            LanguageServerFeature::SignatureHelp => capabilities.signature_help_provider.is_some(),
+            LanguageServerFeature::Hover => matches!(
+                capabilities.hover_provider,
+                Some(
+                    lsp::HoverProviderCapability::Simple(true)
+                        | lsp::HoverProviderCapability::Options(_),
+                )
+            ),
+            LanguageServerFeature::DocumentHighlight => matches!(
+                capabilities.document_highlight_provider,
+                Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
+            ),
+            LanguageServerFeature::Completion => capabilities.completion_provider.is_some(),
+            LanguageServerFeature::CodeAction => matches!(
+                capabilities.code_action_provider,
+                Some(
+                    lsp::CodeActionProviderCapability::Simple(true)
+                        | lsp::CodeActionProviderCapability::Options(_),
+                )
+            ),
+            LanguageServerFeature::WorkspaceCommand => {
+                capabilities.execute_command_provider.is_some()
+            }
+            LanguageServerFeature::DocumentSymbols => matches!(
+                capabilities.document_symbol_provider,
+                Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
+            ),
+            LanguageServerFeature::WorkspaceSymbols => matches!(
+                capabilities.workspace_symbol_provider,
+                Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
+            ),
+            LanguageServerFeature::Diagnostics => true, // there's no extra server capability
+            LanguageServerFeature::RenameSymbol => matches!(
+                capabilities.rename_provider,
+                Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_))
+            ),
+            LanguageServerFeature::InlayHints => matches!(
+                capabilities.inlay_hint_provider,
+                Some(
+                    lsp::OneOf::Left(true)
+                        | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_))
+                )
+            ),
+        }
+    }
+
     pub fn offset_encoding(&self) -> OffsetEncoding {
         self.capabilities()
             .position_encoding
@@ -1301,21 +1388,13 @@ impl Client {
         Some(self.call::<lsp::request::CodeActionRequest>(params))
     }
 
-    pub fn supports_rename(&self) -> bool {
-        let capabilities = self.capabilities.get().unwrap();
-        matches!(
-            capabilities.rename_provider,
-            Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
-        )
-    }
-
     pub fn rename_symbol(
         &self,
         text_document: lsp::TextDocumentIdentifier,
         position: lsp::Position,
         new_name: String,
     ) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
-        if !self.supports_rename() {
+        if !self.supports_feature(LanguageServerFeature::RenameSymbol) {
             return None;
         }
 
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index d602eaa2..749b0ecf 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -3236,10 +3236,8 @@ pub mod insert {
         let trigger_completion = doc
             .language_servers_with_feature(LanguageServerFeature::Completion)
             .any(|ls| {
-                let capabilities = ls.capabilities();
-
                 // TODO: what if trigger is multiple chars long
-                matches!(&capabilities.completion_provider, Some(lsp::CompletionOptions {
+                matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions {
                     trigger_characters: Some(triggers),
                     ..
                 }) if triggers.iter().any(|trigger| trigger.contains(ch)))
@@ -3252,51 +3250,39 @@ pub mod insert {
     }
 
     fn signature_help(cx: &mut Context, ch: char) {
-        use futures_util::FutureExt;
         use helix_lsp::lsp;
         // if ch matches signature_help char, trigger
-        let (view, doc) = current!(cx.editor);
-        // lsp doesn't tell us when to close the signature help, so we request
-        // the help information again after common close triggers which should
-        // return None, which in turn closes the popup.
-        let close_triggers = &[')', ';', '.'];
-        // TODO support multiple language servers (not just the first that is found)
-        let future = doc
+        let doc = doc_mut!(cx.editor);
+        // TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
+        let Some(language_server) = doc
             .language_servers_with_feature(LanguageServerFeature::SignatureHelp)
-            .find_map(|ls| {
-                let capabilities = ls.capabilities();
+            .next()
+        else {
+            return;
+        };
 
-                match capabilities {
-                    lsp::ServerCapabilities {
-                        signature_help_provider:
-                            Some(lsp::SignatureHelpOptions {
-                                trigger_characters: Some(triggers),
-                                // TODO: retrigger_characters
-                                ..
-                            }),
-                        ..
-                    } if triggers.iter().any(|trigger| trigger.contains(ch))
-                        || close_triggers.contains(&ch) =>
-                    {
-                        let pos = doc.position(view.id, ls.offset_encoding());
-                        ls.text_document_signature_help(doc.identifier(), pos, None)
-                    }
-                    _ if close_triggers.contains(&ch) => ls.text_document_signature_help(
-                        doc.identifier(),
-                        doc.position(view.id, ls.offset_encoding()),
-                        None,
-                    ),
-                    // TODO: what if trigger is multiple chars long
-                    _ => None,
-                }
-            });
+        let capabilities = language_server.capabilities();
 
-        if let Some(future) = future {
-            super::signature_help_impl_with_future(
-                cx,
-                future.boxed(),
-                SignatureHelpInvoked::Automatic,
-            )
+        if let lsp::ServerCapabilities {
+            signature_help_provider:
+                Some(lsp::SignatureHelpOptions {
+                    trigger_characters: Some(triggers),
+                    // TODO: retrigger_characters
+                    ..
+                }),
+            ..
+        } = capabilities
+        {
+            // TODO: what if trigger is multiple chars long
+            let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
+            // lsp doesn't tell us when to close the signature help, so we request
+            // the help information again after common close triggers which should
+            // return None, which in turn closes the popup.
+            let close_triggers = &[')', ';', '.'];
+
+            if is_trigger || close_triggers.contains(&ch) {
+                super::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
+            }
         }
     }
 
@@ -3310,7 +3296,7 @@ pub mod insert {
         Some(transaction)
     }
 
-    use helix_core::{auto_pairs, syntax::LanguageServerFeature};
+    use helix_core::auto_pairs;
 
     pub fn insert_char(cx: &mut Context, c: char) {
         let (view, doc) = current_ref!(cx.editor);
@@ -4065,39 +4051,44 @@ fn format_selections(cx: &mut Context) {
             .set_error("format_selections only supports a single selection for now");
         return;
     }
-    let future_offset_encoding = doc
+
+    // TODO extra LanguageServerFeature::FormatSelections?
+    // maybe such that LanguageServerFeature::Format contains it as well
+    let Some(language_server) = doc
         .language_servers_with_feature(LanguageServerFeature::Format)
-        .find_map(|language_server| {
-            let offset_encoding = language_server.offset_encoding();
-            let ranges: Vec<lsp::Range> = doc
-                .selection(view_id)
-                .iter()
-                .map(|range| range_to_lsp_range(doc.text(), *range, offset_encoding))
-                .collect();
-
-            // TODO: handle fails
-            // TODO: concurrent map over all ranges
-
-            let range = ranges[0];
-
-            let future = language_server.text_document_range_formatting(
-                doc.identifier(),
-                range,
-                lsp::FormattingOptions::default(),
-                None,
-            )?;
-            Some((future, offset_encoding))
-        });
-
-    let (future, offset_encoding) = match future_offset_encoding {
-        Some(future_offset_encoding) => future_offset_encoding,
-        None => {
-            cx.editor
-                .set_error("No configured language server supports range formatting");
-            return;
-        }
+        .find(|ls| {
+            matches!(
+                ls.capabilities().document_range_formatting_provider,
+                Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
+            )
+        })
+    else {
+        cx.editor
+            .set_error("No configured language server does not support range formatting");
+        return;
     };
 
+    let offset_encoding = language_server.offset_encoding();
+    let ranges: Vec<lsp::Range> = doc
+        .selection(view_id)
+        .iter()
+        .map(|range| range_to_lsp_range(doc.text(), *range, offset_encoding))
+        .collect();
+
+    // TODO: handle fails
+    // TODO: concurrent map over all ranges
+
+    let range = ranges[0];
+
+    let future = language_server
+        .text_document_range_formatting(
+            doc.identifier(),
+            range,
+            lsp::FormattingOptions::default(),
+            None,
+        )
+        .unwrap();
+
     let edits = tokio::task::block_in_place(|| helix_lsp::block_on(future)).unwrap_or_default();
 
     let transaction =
@@ -4247,15 +4238,15 @@ pub fn completion(cx: &mut Context) {
 
     let mut futures: FuturesUnordered<_> = doc
         .language_servers_with_feature(LanguageServerFeature::Completion)
-        // TODO this should probably already been filtered in something like "language_servers_with_feature"
         .filter(|ls| seen_language_servers.insert(ls.id()))
-        .filter_map(|language_server| {
+        .map(|language_server| {
             let language_server_id = language_server.id();
             let offset_encoding = language_server.offset_encoding();
             let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding);
-            let completion_request = language_server.completion(doc.identifier(), pos, None)?;
+            let doc_id = doc.identifier();
+            let completion_request = language_server.completion(doc_id, pos, None).unwrap();
 
-            Some(async move {
+            async move {
                 let json = completion_request.await?;
                 let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
 
@@ -4277,7 +4268,7 @@ pub fn completion(cx: &mut Context) {
                 .collect();
 
                 anyhow::Ok(items)
-            })
+            }
         })
         .collect();
 
diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs
index 6a024bed..15f8d93d 100644
--- a/helix-term/src/commands/lsp.rs
+++ b/helix-term/src/commands/lsp.rs
@@ -45,6 +45,28 @@ use std::{
     sync::Arc,
 };
 
+/// Gets the first language server that is attached to a document which supports a specific feature.
+/// If there is no configured language server that supports the feature, this displays a status message.
+/// Using this macro in a context where the editor automatically queries the LSP
+/// (instead of when the user explicitly does so via a keybind like `gd`)
+/// will spam the "No configured language server supports <feature>" status message confusingly.
+#[macro_export]
+macro_rules! language_server_with_feature {
+    ($editor:expr, $doc:expr, $feature:expr) => {{
+        let language_server = $doc.language_servers_with_feature($feature).next();
+        match language_server {
+            Some(language_server) => language_server,
+            None => {
+                $editor.set_status(format!(
+                    "No configured language server supports {}",
+                    $feature
+                ));
+                return;
+            }
+        }
+    }};
+}
+
 impl ui::menu::Item for lsp::Location {
     /// Current working directory.
     type Data = PathBuf;
@@ -361,36 +383,38 @@ pub fn symbol_picker(cx: &mut Context) {
     let mut futures: FuturesUnordered<_> = doc
         .language_servers_with_feature(LanguageServerFeature::DocumentSymbols)
         .filter(|ls| seen_language_servers.insert(ls.id()))
-        .filter_map(|ls| {
-            let request = ls.document_symbols(doc.identifier())?;
-            Some((request, ls.offset_encoding(), doc.identifier()))
-        })
-        .map(|(request, offset_encoding, doc_id)| async move {
-            let json = request.await?;
-            let response: Option<lsp::DocumentSymbolResponse> = serde_json::from_value(json)?;
-            let symbols = match response {
-                Some(symbols) => symbols,
-                None => return anyhow::Ok(vec![]),
-            };
-            // lsp has two ways to represent symbols (flat/nested)
-            // convert the nested variant to flat, so that we have a homogeneous list
-            let symbols = match symbols {
-                lsp::DocumentSymbolResponse::Flat(symbols) => symbols
-                    .into_iter()
-                    .map(|symbol| SymbolInformationItem {
-                        symbol,
-                        offset_encoding,
-                    })
-                    .collect(),
-                lsp::DocumentSymbolResponse::Nested(symbols) => {
-                    let mut flat_symbols = Vec::new();
-                    for symbol in symbols {
-                        nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding)
+        .map(|language_server| {
+            let request = language_server.document_symbols(doc.identifier()).unwrap();
+            let offset_encoding = language_server.offset_encoding();
+            let doc_id = doc.identifier();
+
+            async move {
+                let json = request.await?;
+                let response: Option<lsp::DocumentSymbolResponse> = serde_json::from_value(json)?;
+                let symbols = match response {
+                    Some(symbols) => symbols,
+                    None => return anyhow::Ok(vec![]),
+                };
+                // lsp has two ways to represent symbols (flat/nested)
+                // convert the nested variant to flat, so that we have a homogeneous list
+                let symbols = match symbols {
+                    lsp::DocumentSymbolResponse::Flat(symbols) => symbols
+                        .into_iter()
+                        .map(|symbol| SymbolInformationItem {
+                            symbol,
+                            offset_encoding,
+                        })
+                        .collect(),
+                    lsp::DocumentSymbolResponse::Nested(symbols) => {
+                        let mut flat_symbols = Vec::new();
+                        for symbol in symbols {
+                            nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding)
+                        }
+                        flat_symbols
                     }
-                    flat_symbols
-                }
-            };
-            Ok(symbols)
+                };
+                Ok(symbols)
+            }
         })
         .collect();
     let current_url = doc.url();
@@ -425,20 +449,24 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
         let mut futures: FuturesUnordered<_> = doc
             .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
             .filter(|ls| seen_language_servers.insert(ls.id()))
-            .filter_map(|ls| Some((ls.workspace_symbols(pattern.clone())?, ls.offset_encoding())))
-            .map(|(request, offset_encoding)| async move {
-                let json = request.await?;
+            .map(|language_server| {
+                let request = language_server.workspace_symbols(pattern.clone()).unwrap();
+                let offset_encoding = language_server.offset_encoding();
+                async move {
+                    let json = request.await?;
 
-                let response = serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)?
-                    .unwrap_or_default()
-                    .into_iter()
-                    .map(|symbol| SymbolInformationItem {
-                        symbol,
-                        offset_encoding,
-                    })
-                    .collect();
+                    let response =
+                        serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)?
+                            .unwrap_or_default()
+                            .into_iter()
+                            .map(|symbol| SymbolInformationItem {
+                                symbol,
+                                offset_encoding,
+                            })
+                            .collect();
 
-                anyhow::Ok(response)
+                    anyhow::Ok(response)
+                }
             })
             .collect();
 
@@ -1043,22 +1071,19 @@ where
     F: Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send,
 {
     let (view, doc) = current!(cx.editor);
-    if let Some((future, offset_encoding)) =
-        doc.run_on_first_supported_language_server(view.id, feature, |ls, encoding, pos, doc_id| {
-            Some((request_provider(ls, pos, doc_id)?, encoding))
-        })
-    {
-        cx.callback(
-            future,
-            move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
-                let items = to_locations(response);
-                goto_impl(editor, compositor, items, offset_encoding);
-            },
-        );
-    } else {
-        cx.editor
-            .set_error("No configured language server supports {feature}");
-    }
+
+    let language_server = language_server_with_feature!(cx.editor, doc, feature);
+    let offset_encoding = language_server.offset_encoding();
+    let pos = doc.position(view.id, offset_encoding);
+    let future = request_provider(language_server, pos, doc.identifier()).unwrap();
+
+    cx.callback(
+        future,
+        move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
+            let items = to_locations(response);
+            goto_impl(editor, compositor, items, offset_encoding);
+        },
+    );
 }
 
 pub fn goto_declaration(cx: &mut Context) {
@@ -1096,32 +1121,29 @@ pub fn goto_implementation(cx: &mut Context) {
 pub fn goto_reference(cx: &mut Context) {
     let config = cx.editor.config();
     let (view, doc) = current!(cx.editor);
-    if let Some((future, offset_encoding)) = doc.run_on_first_supported_language_server(
-        view.id,
-        LanguageServerFeature::GotoReference,
-        |ls, encoding, pos, doc_id| {
-            Some((
-                ls.goto_reference(
-                    doc_id,
-                    pos,
-                    config.lsp.goto_reference_include_declaration,
-                    None,
-                )?,
-                encoding,
-            ))
+
+    // TODO could probably support multiple language servers,
+    // not sure if there's a real practical use case for this though
+    let language_server =
+        language_server_with_feature!(cx.editor, doc, LanguageServerFeature::GotoReference);
+    let offset_encoding = language_server.offset_encoding();
+    let pos = doc.position(view.id, offset_encoding);
+    let future = language_server
+        .goto_reference(
+            doc.identifier(),
+            pos,
+            config.lsp.goto_reference_include_declaration,
+            None,
+        )
+        .unwrap();
+
+    cx.callback(
+        future,
+        move |editor, compositor, response: Option<Vec<lsp::Location>>| {
+            let items = response.unwrap_or_default();
+            goto_impl(editor, compositor, items, offset_encoding);
         },
-    ) {
-        cx.callback(
-            future,
-            move |editor, compositor, response: Option<Vec<lsp::Location>>| {
-                let items = response.unwrap_or_default();
-                goto_impl(editor, compositor, items, offset_encoding);
-            },
-        );
-    } else {
-        cx.editor
-            .set_error("No configured language server supports goto-reference");
-    }
+    );
 }
 
 #[derive(PartialEq, Eq, Clone, Copy)]
@@ -1145,19 +1167,15 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
             language_server.text_document_signature_help(doc.identifier(), pos, None)
         });
 
-    let future = match future {
-        Some(future) => future.boxed(),
-        None => {
-            // Do not show the message if signature help was invoked
-            // automatically on backspace, trigger characters, etc.
-            if invoked == SignatureHelpInvoked::Manual {
-                cx.editor
-                    .set_error("No configured language server supports signature-help");
-            }
-            return;
+    let Some(future) = future else {
+        // Do not show the message if signature help was invoked
+        // automatically on backspace, trigger characters, etc.
+        if invoked == SignatureHelpInvoked::Manual {
+            cx.editor.set_error("No configured language server supports signature-help");
         }
+        return;
     };
-    signature_help_impl_with_future(cx, future, invoked);
+    signature_help_impl_with_future(cx, future.boxed(), invoked);
 }
 
 pub fn signature_help_impl_with_future(
@@ -1272,22 +1290,14 @@ pub fn signature_help_impl_with_future(
 pub fn hover(cx: &mut Context) {
     let (view, doc) = current!(cx.editor);
 
+    // TODO support multiple language servers (merge UI somehow)
+    let language_server =
+        language_server_with_feature!(cx.editor, doc, LanguageServerFeature::Hover);
     // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
-    let request = doc
-        .language_servers_with_feature(LanguageServerFeature::Hover)
-        .find_map(|language_server| {
-            let pos = doc.position(view.id, language_server.offset_encoding());
-            language_server.text_document_hover(doc.identifier(), pos, None)
-        });
-
-    let future = match request {
-        Some(future) => future,
-        None => {
-            cx.editor
-                .set_error("No configured language server supports hover");
-            return;
-        }
-    };
+    let pos = doc.position(view.id, language_server.offset_encoding());
+    let future = language_server
+        .text_document_hover(doc.identifier(), pos, None)
+        .unwrap();
 
     cx.callback(
         future,
@@ -1381,34 +1391,26 @@ pub fn rename_symbol(cx: &mut Context) {
                     return;
                 }
                 let (view, doc) = current!(cx.editor);
-                let request = doc
-                    .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
-                    .find_map(|language_server| {
-                        if let Some(language_server_id) = language_server_id {
-                            if language_server.id() != language_server_id {
-                                return None;
-                            }
-                        }
-                        let offset_encoding = language_server.offset_encoding();
-                        let pos = doc.position(view.id, offset_encoding);
-                        let future = language_server.rename_symbol(
-                            doc.identifier(),
-                            pos,
-                            input.to_string(),
-                        )?;
-                        Some((future, offset_encoding))
-                    });
 
-                if let Some((future, offset_encoding)) = request {
-                    match block_on(future) {
-                        Ok(edits) => {
-                            let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
-                        }
-                        Err(err) => cx.editor.set_error(err.to_string()),
+                let Some(language_server) = doc
+                    .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
+                    .find(|ls| language_server_id.is_none() || Some(ls.id()) == language_server_id)
+                else {
+                    cx.editor.set_error("No configured language server supports symbol renaming");
+                    return;
+                };
+
+                let offset_encoding = language_server.offset_encoding();
+                let pos = doc.position(view.id, offset_encoding);
+                let future = language_server
+                    .rename_symbol(doc.identifier(), pos, input.to_string())
+                    .unwrap();
+
+                match block_on(future) {
+                    Ok(edits) => {
+                        let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
                     }
-                } else {
-                    cx.editor
-                        .set_error("No configured language server supports symbol renaming");
+                    Err(err) => cx.editor.set_error(err.to_string()),
                 }
             },
         )
@@ -1417,20 +1419,28 @@ pub fn rename_symbol(cx: &mut Context) {
         Box::new(prompt)
     }
 
-    let (view, doc) = current!(cx.editor);
+    let (view, doc) = current_ref!(cx.editor);
 
-    let prepare_rename_request = doc
+    let language_server_with_prepare_rename_support = doc
         .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
-        .find_map(|language_server| {
-            let offset_encoding = language_server.offset_encoding();
-            let pos = doc.position(view.id, offset_encoding);
-            let future = language_server.prepare_rename(doc.identifier(), pos)?;
-            Some((future, offset_encoding, language_server.id()))
+        .find(|ls| {
+            matches!(
+                ls.capabilities().rename_provider,
+                Some(lsp::OneOf::Right(lsp::RenameOptions {
+                    prepare_provider: Some(true),
+                    ..
+                }))
+            )
         });
 
-    match prepare_rename_request {
-        // Language server supports textDocument/prepareRename, use it.
-        Some((future, offset_encoding, ls_id)) => cx.callback(
+    if let Some(language_server) = language_server_with_prepare_rename_support {
+        let ls_id = language_server.id();
+        let offset_encoding = language_server.offset_encoding();
+        let pos = doc.position(view.id, offset_encoding);
+        let future = language_server
+            .prepare_rename(doc.identifier(), pos)
+            .unwrap();
+        cx.callback(
             future,
             move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| {
                 let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response)
@@ -1446,38 +1456,23 @@ pub fn rename_symbol(cx: &mut Context) {
 
                 compositor.push(prompt);
             },
-        ),
-        // Language server does not support textDocument/prepareRename, fall back
-        // to word boundary selection.
-        None => {
-            let prefill = get_prefill_from_word_boundary(cx.editor);
-
-            let prompt = create_rename_prompt(cx.editor, prefill, None);
-
-            cx.push_layer(prompt);
-        }
-    };
+        );
+    } else {
+        let prefill = get_prefill_from_word_boundary(cx.editor);
+        let prompt = create_rename_prompt(cx.editor, prefill, None);
+        cx.push_layer(prompt);
+    }
 }
 
 pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
     let (view, doc) = current!(cx.editor);
-    let future_offset_encoding = doc
-        .language_servers_with_feature(LanguageServerFeature::DocumentHighlight)
-        .find_map(|language_server| {
-            let offset_encoding = language_server.offset_encoding();
-            let pos = doc.position(view.id, offset_encoding);
-            let future =
-                language_server.text_document_document_highlight(doc.identifier(), pos, None)?;
-            Some((future, offset_encoding))
-        });
-    let (future, offset_encoding) = match future_offset_encoding {
-        Some(future_offset_encoding) => future_offset_encoding,
-        None => {
-            cx.editor
-                .set_error("No configured language server supports document-highlight");
-            return;
-        }
-    };
+    let language_server =
+        language_server_with_feature!(cx.editor, doc, LanguageServerFeature::DocumentHighlight);
+    let offset_encoding = language_server.offset_encoding();
+    let pos = doc.position(view.id, offset_encoding);
+    let future = language_server
+        .text_document_document_highlight(doc.identifier(), pos, None)
+        .unwrap();
 
     cx.callback(
         future,
@@ -1532,16 +1527,9 @@ fn compute_inlay_hints_for_view(
     let view_id = view.id;
     let doc_id = view.doc;
 
-    let mut language_servers = doc.language_servers_with_feature(LanguageServerFeature::InlayHints);
-    let language_server = language_servers.find(|language_server| {
-        matches!(
-            language_server.capabilities().inlay_hint_provider,
-            Some(
-                lsp::OneOf::Left(true)
-                    | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_))
-            )
-        )
-    })?;
+    let language_server = doc
+        .language_servers_with_feature(LanguageServerFeature::InlayHints)
+        .next()?;
 
     let doc_text = doc.text();
     let len_lines = doc_text.len_lines();
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 6f7ed174..ec328ec5 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -394,14 +394,11 @@ pub mod completers {
     pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
         let matcher = Matcher::default();
 
-        let options = match doc!(editor)
+        let Some(options) = doc!(editor)
             .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
             .find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
-        {
-            Some(options) => options,
-            None => {
-                return vec![];
-            }
+        else {
+            return vec![];
         };
 
         let mut matches: Vec<_> = options
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index bc81567e..f2f373aa 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -580,7 +580,7 @@ where
     *mut_ref = f(mem::take(mut_ref));
 }
 
-use helix_lsp::{lsp, Client, LanguageServerName, OffsetEncoding};
+use helix_lsp::{lsp, Client, LanguageServerName};
 use url::Url;
 
 impl Document {
@@ -732,21 +732,19 @@ impl Document {
 
         let text = self.text.clone();
         // finds first language server that supports formatting and then formats
-        let (offset_encoding, request) = self
+        let language_server = self
             .language_servers_with_feature(LanguageServerFeature::Format)
-            .find_map(|language_server| {
-                let offset_encoding = language_server.offset_encoding();
-                let request = language_server.text_document_formatting(
-                    self.identifier(),
-                    lsp::FormattingOptions {
-                        tab_size: self.tab_width() as u32,
-                        insert_spaces: matches!(self.indent_style, IndentStyle::Spaces(_)),
-                        ..Default::default()
-                    },
-                    None,
-                )?;
-                Some((offset_encoding, request))
-            })?;
+            .next()?;
+        let offset_encoding = language_server.offset_encoding();
+        let request = language_server.text_document_formatting(
+            self.identifier(),
+            lsp::FormattingOptions {
+                tab_size: self.tab_width() as u32,
+                insert_spaces: matches!(self.indent_style, IndentStyle::Spaces(_)),
+                ..Default::default()
+            },
+            None,
+        )?;
 
         let fut = async move {
             let edits = request.await.unwrap_or_else(|e| {
@@ -1445,7 +1443,6 @@ impl Document {
         self.language_servers.remove(name)
     }
 
-    // TODO filter also based on LSP capabilities?
     pub fn language_servers_with_feature(
         &self,
         feature: LanguageServerFeature,
@@ -1453,7 +1450,10 @@ impl Document {
         self.language_config().into_iter().flat_map(move |config| {
             config.language_servers.iter().filter_map(move |features| {
                 let ls = &**self.language_servers.get(&features.name)?;
-                if ls.is_initialized() && features.has_feature(feature) {
+                if ls.is_initialized()
+                    && ls.supports_feature(feature)
+                    && features.has_feature(feature)
+                {
                     Some(ls)
                 } else {
                     None
@@ -1466,23 +1466,6 @@ impl Document {
         self.language_servers().any(|l| l.id() == id)
     }
 
-    pub fn run_on_first_supported_language_server<T, P>(
-        &self,
-        view_id: ViewId,
-        feature: LanguageServerFeature,
-        request_provider: P,
-    ) -> Option<T>
-    where
-        P: Fn(&Client, OffsetEncoding, lsp::Position, lsp::TextDocumentIdentifier) -> Option<T>,
-    {
-        self.language_servers_with_feature(feature)
-            .find_map(|language_server| {
-                let offset_encoding = language_server.offset_encoding();
-                let pos = self.position(view_id, offset_encoding);
-                request_provider(language_server, offset_encoding, pos, self.identifier())
-            })
-    }
-
     pub fn diff_handle(&self) -> Option<&DiffHandle> {
         self.diff_handle.as_ref()
     }