diff --git a/book/src/configuration.md b/book/src/configuration.md
index aebf5ff0..4b62ca52 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.md
@@ -50,6 +50,7 @@ signal to the Helix process on Unix operating systems, such as by using the comm
 | `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` |
 | `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `400` |
 | `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
+| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
 | `auto-info` | Whether to display info boxes | `true` |
 | `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative | `false` |
 | `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file | `[]` |
diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs
index 58e8d83d..1463ccb3 100644
--- a/helix-lsp/src/lib.rs
+++ b/helix-lsp/src/lib.rs
@@ -250,18 +250,27 @@ pub mod util {
     /// If the LS did not provide a range for the completion or the range of the
     /// primary cursor can not be used for the secondary cursor, this function
     /// can be used to find the completion range for a cursor
-    fn find_completion_range(text: RopeSlice, cursor: usize) -> (usize, usize) {
+    fn find_completion_range(text: RopeSlice, replace_mode: bool, cursor: usize) -> (usize, usize) {
         let start = cursor
             - text
                 .chars_at(cursor)
                 .reversed()
                 .take_while(|ch| chars::char_is_word(*ch))
                 .count();
-        (start, cursor)
+        let mut end = cursor;
+        if replace_mode {
+            end += text
+                .chars_at(cursor)
+                .skip(1)
+                .take_while(|ch| chars::char_is_word(*ch))
+                .count();
+        }
+        (start, end)
     }
     fn completion_range(
         text: RopeSlice,
         edit_offset: Option<(i128, i128)>,
+        replace_mode: bool,
         cursor: usize,
     ) -> Option<(usize, usize)> {
         let res = match edit_offset {
@@ -276,7 +285,7 @@ pub mod util {
                 }
                 (start_offset as usize, end_offset as usize)
             }
-            None => find_completion_range(text, cursor),
+            None => find_completion_range(text, replace_mode, cursor),
         };
         Some(res)
     }
@@ -287,6 +296,7 @@ pub mod util {
         doc: &Rope,
         selection: &Selection,
         edit_offset: Option<(i128, i128)>,
+        replace_mode: bool,
         new_text: String,
     ) -> Transaction {
         let replacement: Option<Tendril> = if new_text.is_empty() {
@@ -296,9 +306,13 @@ pub mod util {
         };
 
         let text = doc.slice(..);
-        let (removed_start, removed_end) =
-            completion_range(text, edit_offset, selection.primary().cursor(text))
-                .expect("transaction must be valid for primary selection");
+        let (removed_start, removed_end) = completion_range(
+            text,
+            edit_offset,
+            replace_mode,
+            selection.primary().cursor(text),
+        )
+        .expect("transaction must be valid for primary selection");
         let removed_text = text.slice(removed_start..removed_end);
 
         let (transaction, mut selection) = Transaction::change_by_selection_ignore_overlapping(
@@ -306,9 +320,9 @@ pub mod util {
             selection,
             |range| {
                 let cursor = range.cursor(text);
-                completion_range(text, edit_offset, cursor)
+                completion_range(text, edit_offset, replace_mode, cursor)
                     .filter(|(start, end)| text.slice(start..end) == removed_text)
-                    .unwrap_or_else(|| find_completion_range(text, cursor))
+                    .unwrap_or_else(|| find_completion_range(text, replace_mode, cursor))
             },
             |_, _| replacement.clone(),
         );
@@ -326,6 +340,7 @@ pub mod util {
         doc: &Rope,
         selection: &Selection,
         edit_offset: Option<(i128, i128)>,
+        replace_mode: bool,
         snippet: snippet::Snippet,
         line_ending: &str,
         include_placeholder: bool,
@@ -336,9 +351,13 @@ pub mod util {
         let mut off = 0i128;
         let mut mapped_doc = doc.clone();
         let mut selection_tabstops: SmallVec<[_; 1]> = SmallVec::new();
-        let (removed_start, removed_end) =
-            completion_range(text, edit_offset, selection.primary().cursor(text))
-                .expect("transaction must be valid for primary selection");
+        let (removed_start, removed_end) = completion_range(
+            text,
+            edit_offset,
+            replace_mode,
+            selection.primary().cursor(text),
+        )
+        .expect("transaction must be valid for primary selection");
         let removed_text = text.slice(removed_start..removed_end);
 
         let (transaction, selection) = Transaction::change_by_selection_ignore_overlapping(
@@ -346,9 +365,9 @@ pub mod util {
             selection,
             |range| {
                 let cursor = range.cursor(text);
-                completion_range(text, edit_offset, cursor)
+                completion_range(text, edit_offset, replace_mode, cursor)
                     .filter(|(start, end)| text.slice(start..end) == removed_text)
-                    .unwrap_or_else(|| find_completion_range(text, cursor))
+                    .unwrap_or_else(|| find_completion_range(text, replace_mode, cursor))
             },
             |replacement_start, replacement_end| {
                 let mapped_replacement_start = (replacement_start as i128 + off) as usize;
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 99c33781..6303793b 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -108,6 +108,7 @@ impl Completion {
         start_offset: usize,
         trigger_offset: usize,
     ) -> Self {
+        let replace_mode = editor.config().completion_replace;
         // Sort completion items according to their preselect status (given by the LSP server)
         items.sort_by_key(|item| !item.preselect.unwrap_or(false));
 
@@ -120,18 +121,23 @@ impl Completion {
                 offset_encoding: helix_lsp::OffsetEncoding,
                 trigger_offset: usize,
                 include_placeholder: bool,
+                replace_mode: bool,
             ) -> Transaction {
                 use helix_lsp::snippet;
                 let selection = doc.selection(view_id);
                 let text = doc.text().slice(..);
                 let primary_cursor = selection.primary().cursor(text);
 
-                let (start_offset, end_offset, new_text) = if let Some(edit) = &item.text_edit {
+                let (edit_offset, new_text) = if let Some(edit) = &item.text_edit {
                     let edit = match edit {
                         lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
                         lsp::CompletionTextEdit::InsertAndReplace(item) => {
-                            // TODO: support using "insert" instead of "replace" via user config
-                            lsp::TextEdit::new(item.replace, item.new_text.clone())
+                            let range = if replace_mode {
+                                item.replace
+                            } else {
+                                item.insert
+                            };
+                            lsp::TextEdit::new(range, item.new_text.clone())
                         }
                     };
 
@@ -157,7 +163,7 @@ impl Completion {
                     // document changed (and not just the selection) then we will
                     // likely delete the wrong text (same if we applied an edit sent by the LS)
                     debug_assert!(primary_cursor == trigger_offset);
-                    (None, Some(0), new_text)
+                    (None, new_text)
                 };
 
                 if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET))
@@ -170,8 +176,8 @@ impl Completion {
                         Ok(snippet) => util::generate_transaction_from_snippet(
                             doc.text(),
                             selection,
-                            start_offset,
-                            end_offset,
+                            edit_offset,
+                            replace_mode,
                             snippet,
                             doc.line_ending.as_str(),
                             include_placeholder,
@@ -190,8 +196,8 @@ impl Completion {
                     util::generate_transaction_from_completion_edit(
                         doc.text(),
                         selection,
-                        start_offset,
-                        end_offset,
+                        edit_offset,
+                        replace_mode,
                         new_text,
                     )
                 }
@@ -224,6 +230,7 @@ impl Completion {
                         offset_encoding,
                         trigger_offset,
                         true,
+                        replace_mode,
                     );
 
                     // initialize a savepoint
@@ -245,6 +252,7 @@ impl Completion {
                         offset_encoding,
                         trigger_offset,
                         false,
+                        replace_mode,
                     );
 
                     doc.apply(&transaction, view.id);
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index c6541105..1b4664ff 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -251,6 +251,9 @@ pub struct Config {
     )]
     pub idle_timeout: Duration,
     pub completion_trigger_len: u8,
+    /// Whether to instruct the LSP to replace the entire word when applying a completion
+    /// or to only insert new text
+    pub completion_replace: bool,
     /// Whether to display infoboxes. Defaults to true.
     pub auto_info: bool,
     pub file_picker: FilePickerConfig,
@@ -738,6 +741,7 @@ impl Default for Config {
             color_modes: false,
             soft_wrap: SoftWrap::default(),
             text_width: 80,
+            completion_replace: false,
         }
     }
 }