diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index 95f3ea34..9fa118fb 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -359,7 +359,7 @@ impl Client {
                     }),
                     rename: Some(lsp::RenameClientCapabilities {
                         dynamic_registration: Some(false),
-                        prepare_support: Some(false),
+                        prepare_support: Some(true),
                         prepare_support_default_behavior: None,
                         honors_change_annotations: Some(false),
                     }),
@@ -1034,6 +1034,29 @@ impl Client {
         Some(self.call::<lsp::request::DocumentSymbolRequest>(params))
     }
 
+    pub fn prepare_rename(
+        &self,
+        text_document: lsp::TextDocumentIdentifier,
+        position: lsp::Position,
+    ) -> Option<impl Future<Output = Result<Value>>> {
+        let capabilities = self.capabilities.get().unwrap();
+
+        match capabilities.rename_provider {
+            Some(lsp::OneOf::Right(lsp::RenameOptions {
+                prepare_provider: Some(true),
+                ..
+            })) => (),
+            _ => return None,
+        }
+
+        let params = lsp::TextDocumentPositionParams {
+            text_document,
+            position,
+        };
+
+        Some(self.call::<lsp::request::PrepareRenameRequest>(params))
+    }
+
     // empty string to get all symbols
     pub fn workspace_symbols(&self, query: String) -> Option<impl Future<Output = Result<Value>>> {
         let capabilities = self.capabilities.get().unwrap();
diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs
index d59eebdb..08519366 100644
--- a/helix-term/src/commands/lsp.rs
+++ b/helix-term/src/commands/lsp.rs
@@ -1232,49 +1232,115 @@ pub fn hover(cx: &mut Context) {
 }
 
 pub fn rename_symbol(cx: &mut Context) {
-    let (view, doc) = current_ref!(cx.editor);
-    let text = doc.text().slice(..);
-    let primary_selection = doc.selection(view.id).primary();
-    let prefill = if primary_selection.len() > 1 {
-        primary_selection
-    } else {
-        use helix_core::textobject::{textobject_word, TextObject};
-        textobject_word(text, primary_selection, TextObject::Inside, 1, false)
+    fn get_prefill_from_word_boundary(editor: &Editor) -> String {
+        let (view, doc) = current_ref!(editor);
+        let text = doc.text().slice(..);
+        let primary_selection = doc.selection(view.id).primary();
+        if primary_selection.len() > 1 {
+            primary_selection
+        } else {
+            use helix_core::textobject::{textobject_word, TextObject};
+            textobject_word(text, primary_selection, TextObject::Inside, 1, false)
+        }
+        .fragment(text)
+        .into()
     }
-    .fragment(text)
-    .into();
-    ui::prompt_with_input(
-        cx,
-        "rename-to:".into(),
-        prefill,
-        None,
-        ui::completers::none,
-        move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
-            if event != PromptEvent::Validate {
-                return;
+
+    fn get_prefill_from_lsp_response(
+        editor: &Editor,
+        offset_encoding: OffsetEncoding,
+        response: Option<lsp::PrepareRenameResponse>,
+    ) -> Result<String, &'static str> {
+        match response {
+            Some(lsp::PrepareRenameResponse::Range(range)) => {
+                let text = doc!(editor).text();
+
+                Ok(lsp_range_to_range(text, range, offset_encoding)
+                    .ok_or("lsp sent invalid selection range for rename")?
+                    .fragment(text.slice(..))
+                    .into())
             }
+            Some(lsp::PrepareRenameResponse::RangeWithPlaceholder { placeholder, .. }) => {
+                Ok(placeholder)
+            }
+            Some(lsp::PrepareRenameResponse::DefaultBehavior { .. }) => {
+                Ok(get_prefill_from_word_boundary(editor))
+            }
+            None => Err("lsp did not respond to prepare rename request"),
+        }
+    }
 
-            let (view, doc) = current!(cx.editor);
-            let language_server = language_server!(cx.editor, doc);
-            let offset_encoding = language_server.offset_encoding();
+    fn create_rename_prompt(editor: &Editor, prefill: String) -> Box<ui::Prompt> {
+        let prompt = ui::Prompt::new(
+            "rename-to:".into(),
+            None,
+            ui::completers::none,
+            move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
+                if event != PromptEvent::Validate {
+                    return;
+                }
 
-            let pos = doc.position(view.id, offset_encoding);
+                let (view, doc) = current!(cx.editor);
+                let language_server = language_server!(cx.editor, doc);
+                let offset_encoding = language_server.offset_encoding();
 
-            let future =
-                match language_server.rename_symbol(doc.identifier(), pos, input.to_string()) {
-                    Some(future) => future,
-                    None => {
-                        cx.editor
-                            .set_error("Language server does not support symbol renaming");
+                let pos = doc.position(view.id, offset_encoding);
+
+                let future =
+                    match language_server.rename_symbol(doc.identifier(), pos, input.to_string()) {
+                        Some(future) => future,
+                        None => {
+                            cx.editor
+                                .set_error("Language server does not support symbol renaming");
+                            return;
+                        }
+                    };
+                match block_on(future) {
+                    Ok(edits) => apply_workspace_edit(cx.editor, offset_encoding, &edits),
+                    Err(err) => cx.editor.set_error(err.to_string()),
+                }
+            },
+        )
+        .with_line(prefill, editor);
+
+        Box::new(prompt)
+    }
+
+    let (view, doc) = current!(cx.editor);
+    let language_server = language_server!(cx.editor, doc);
+    let offset_encoding = language_server.offset_encoding();
+
+    let pos = doc.position(view.id, offset_encoding);
+
+    match language_server.prepare_rename(doc.identifier(), pos) {
+        // Language server supports textDocument/prepareRename, use it.
+        Some(future) => cx.callback(
+            future,
+            move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| {
+                let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response)
+                {
+                    Ok(p) => p,
+                    Err(e) => {
+                        editor.set_error(e);
                         return;
                     }
                 };
-            match block_on(future) {
-                Ok(edits) => apply_workspace_edit(cx.editor, offset_encoding, &edits),
-                Err(err) => cx.editor.set_error(err.to_string()),
-            }
-        },
-    );
+
+                let prompt = create_rename_prompt(editor, prefill);
+
+                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);
+
+            cx.push_layer(prompt);
+        }
+    };
 }
 
 pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {