diff --git a/Default.sublime-keymap b/Default.sublime-keymap
index 0c17f8203..541512998 100644
--- a/Default.sublime-keymap
+++ b/Default.sublime-keymap
@@ -25,6 +25,25 @@
}
]
},
+ // Insert/Replace Completions
+ {
+ "command": "lsp_commit_completion_with_opposite_insert_mode",
+ "keys": [
+ "alt+enter"
+ ],
+ "context": [
+ {
+ "key": "auto_complete_visible",
+ "operator": "equal",
+ "operand": true
+ },
+ {
+ "key": "lsp.session_with_capability",
+ "operator": "equal",
+ "operand": "completionProvider"
+ }
+ ]
+ },
// Save all open files with lsp_save
// {
// "command": "lsp_save_all",
diff --git a/LSP.sublime-settings b/LSP.sublime-settings
index 298957989..40fc2c275 100644
--- a/LSP.sublime-settings
+++ b/LSP.sublime-settings
@@ -169,6 +169,14 @@
// sorting algorithm and instead uses the sorting defined by the relevant language server.
"inhibit_word_completions": true,
+ // The mode used for inserting completions:
+ // - `insert` would insert the completion text in a middle of the word
+ // - `replace` would replace the existing word with a new completion text
+ // An LSP keybinding `lsp_commit_completion_with_opposite_insert_mode`
+ // can be used to insert completion using the opposite mode to the one selected here.
+ // Note: Must be supported by the language server.
+ "completion_insert_mode": "insert",
+
// Show symbol references in Sublime's quick panel instead of the bottom panel.
"show_references_in_quick_panel": false,
diff --git a/boot.py b/boot.py
index ef9cf6b4b..4cfb2c029 100644
--- a/boot.py
+++ b/boot.py
@@ -5,6 +5,7 @@
# Please keep this list sorted (Edit -> Sort Lines)
from .plugin.code_actions import LspCodeActionsCommand
from .plugin.code_lens import LspCodeLensCommand
+from .plugin.completion import LspCommitCompletionWithOppositeInsertMode
from .plugin.completion import LspResolveDocsCommand
from .plugin.completion import LspSelectCompletionItemCommand
from .plugin.configuration import LspDisableLanguageServerGloballyCommand
diff --git a/docs/src/features.md b/docs/src/features.md
index 545383ba1..a328f84c5 100644
--- a/docs/src/features.md
+++ b/docs/src/features.md
@@ -15,6 +15,11 @@ The LSP package enhances the auto-complete list with results provided by the lan
To show the documentation popup you can click the **More** link in the bottom of the autocomplete,
or you can use the default sublime keybinding F12 to trigger it.
+To insert or replace a completion item using the opposite "completion_insert_mode" setting value, the following keybinding can be used alt+enter.
+Note, this feature can only be used if **Replace** or **Insert** are shown at the bottom of the autocomplete popup.
+
+[Example GIF 3](https://user-images.githubusercontent.com/22029477/189607770-1a8018f6-1fd1-40de-b6d9-be1f657dfc0d.gif)
+
## Goto Definition
[Example GIF 1](https://user-images.githubusercontent.com/6579999/128551655-bfd55991-70a9-43da-a54a-f8d4cb3244c4.gif)
diff --git a/docs/src/keyboard_shortcuts.md b/docs/src/keyboard_shortcuts.md
index c2a78270e..753f1b7e5 100644
--- a/docs/src/keyboard_shortcuts.md
+++ b/docs/src/keyboard_shortcuts.md
@@ -18,17 +18,18 @@ Refer to the [Customization section](customization.md#keyboard-shortcuts-key-bin
| Goto Declaration | unbound | `lsp_symbol_declaration`
| Goto Definition | unbound
suggested: f12 | `lsp_symbol_definition`
| Goto Implementation | unbound | `lsp_symbol_implementation`
-| Goto Symbol | unbound
suggested: ctrl r | `lsp_document_symbols`
| Goto Symbol in Project | unbound
suggested: ctrl shift r | `lsp_workspace_symbols`
+| Goto Symbol | unbound
suggested: ctrl r | `lsp_document_symbols`
| Goto Type Definition | unbound | `lsp_symbol_type_definition`
+| Hover Popup | unbound | `lsp_hover`
+| Insert/Replace Completions | alt enter | `lsp_commit_completion_with_opposite_insert_mode`
| Next Diagnostic | F4 | -
| Previous Diagnostic | shift F4 | -
| Rename | unbound | `lsp_symbol_rename`
| Restart Server | unbound | `lsp_restart_server`
| Run Code Action | unbound | `lsp_code_actions`
-| Run Source Action | unbound | `lsp_code_actions` (with args: `{"only_kinds": ["source"]}`)
| Run Code Lens | unbound | `lsp_code_lens`
+| Run Source Action | unbound | `lsp_code_actions` (with args: `{"only_kinds": ["source"]}`)
| Signature Help | ctrl alt space | `lsp_signature_help_show`
-| Hover Popup | unbound | `lsp_hover`
| Toggle Diagnostics Panel | ctrl alt m | `lsp_show_diagnostics_panel`
| Toggle Log Panel | unbound | `lsp_toggle_server_panel`
diff --git a/plugin/completion.py b/plugin/completion.py
index c4dd1e513..3d24b08f5 100644
--- a/plugin/completion.py
+++ b/plugin/completion.py
@@ -1,8 +1,9 @@
from .core.edit import parse_text_edit
from .core.logging import debug
-from .core.protocol import Request, InsertTextFormat, Range, CompletionItem
+from .core.protocol import InsertReplaceEdit, TextEdit, RangeLsp, Request, InsertTextFormat, Range, CompletionItem
from .core.registry import LspTextCommand
-from .core.typing import List, Dict, Optional, Generator, Union
+from .core.settings import userprefs
+from .core.typing import List, Dict, Optional, Generator, Union, cast
from .core.views import FORMAT_STRING, FORMAT_MARKUP_CONTENT
from .core.views import MarkdownLangMap
from .core.views import minihtml
@@ -16,6 +17,17 @@
SessionName = str
+def get_text_edit_range(text_edit: Union[TextEdit, InsertReplaceEdit]) -> RangeLsp:
+ if 'insert' in text_edit and 'replace' in text_edit:
+ text_edit = cast(InsertReplaceEdit, text_edit)
+ insert_mode = userprefs().completion_insert_mode
+ if LspCommitCompletionWithOppositeInsertMode.active:
+ insert_mode = 'replace' if insert_mode == 'insert' else 'insert'
+ return text_edit.get(insert_mode) # type: ignore
+ text_edit = cast(TextEdit, text_edit)
+ return text_edit['range']
+
+
class LspResolveDocsCommand(LspTextCommand):
completions = {} # type: Dict[SessionName, List[CompletionItem]]
@@ -77,12 +89,21 @@ def _on_navigate(self, url: str) -> None:
webbrowser.open(url)
+class LspCommitCompletionWithOppositeInsertMode(LspTextCommand):
+ active = False
+
+ def run(self, edit: sublime.Edit, event: Optional[dict] = None) -> None:
+ LspCommitCompletionWithOppositeInsertMode.active = True
+ self.view.run_command("commit_completion")
+ LspCommitCompletionWithOppositeInsertMode.active = False
+
+
class LspSelectCompletionItemCommand(LspTextCommand):
def run(self, edit: sublime.Edit, item: CompletionItem, session_name: str) -> None:
text_edit = item.get("textEdit")
if text_edit:
new_text = text_edit["newText"].replace("\r", "")
- edit_region = range_to_region(Range.from_lsp(text_edit['range']), self.view)
+ edit_region = range_to_region(Range.from_lsp(get_text_edit_range(text_edit)), self.view)
for region in self._translated_regions(edit_region):
self.view.erase(edit, region)
else:
diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py
index 623a5c5da..a21880987 100644
--- a/plugin/core/protocol.py
+++ b/plugin/core/protocol.py
@@ -323,6 +323,12 @@ class SemanticTokenModifiers:
'description': str
}, total=False)
+InsertReplaceEdit = TypedDict('InsertReplaceEdit', {
+ 'newText': str,
+ 'insert': RangeLsp,
+ 'replace': RangeLsp
+}, total=True)
+
CompletionItem = TypedDict('CompletionItem', {
'additionalTextEdits': List[TextEdit],
'command': Command,
@@ -341,7 +347,7 @@ class SemanticTokenModifiers:
'preselect': bool,
'sortText': str,
'tags': List[int],
- 'textEdit': TextEdit
+ 'textEdit': Union[TextEdit, InsertReplaceEdit]
}, total=False)
CompletionList = TypedDict('CompletionList', {
diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py
index 28647c062..eecbf2fe0 100644
--- a/plugin/core/sessions.py
+++ b/plugin/core/sessions.py
@@ -246,6 +246,7 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor
"resolveSupport": {
"properties": ["detail", "documentation", "additionalTextEdits"]
},
+ "insertReplaceSupport": True,
"insertTextModeSupport": {
"valueSet": [InsertTextMode.AdjustIndentation]
},
diff --git a/plugin/core/types.py b/plugin/core/types.py
index dd7d00779..1f9011dfe 100644
--- a/plugin/core/types.py
+++ b/plugin/core/types.py
@@ -195,6 +195,7 @@ class Settings:
inhibit_snippet_completions = None # type: bool
inhibit_word_completions = None # type: bool
link_highlight_style = None # type: str
+ completion_insert_mode = None # type: str
log_debug = None # type: bool
log_max_size = None # type: int
log_server = None # type: List[str]
@@ -241,6 +242,7 @@ def r(name: str, default: Union[bool, int, str, list, dict]) -> None:
r("lsp_format_on_save", False)
r("on_save_task_timeout_ms", 2000)
r("only_show_lsp_completions", False)
+ r("completion_insert_mode", 'insert')
r("popup_max_characters_height", 1000)
r("popup_max_characters_width", 120)
r("semantic_highlighting", False)
diff --git a/plugin/core/views.py b/plugin/core/views.py
index 42d4cf8c5..8d98d1cfa 100644
--- a/plugin/core/views.py
+++ b/plugin/core/views.py
@@ -1015,6 +1015,10 @@ def format_completion(
if item.get('deprecated') or CompletionItemTag.Deprecated in item.get('tags', []):
annotation = "DEPRECATED - " + annotation if annotation else "DEPRECATED"
+ insert_replace_support_html = get_insert_replace_support_html(item)
+ if insert_replace_support_html:
+ details.append(insert_replace_support_html)
+
completion = sublime.CompletionItem.command_completion(
trigger=trigger,
command='lsp_select_completion_item',
@@ -1039,3 +1043,13 @@ def format_code_actions_for_quick_panel(
if code_action.get('isPreferred', False):
selected_index = idx
return items, selected_index
+
+
+def get_insert_replace_support_html(item: CompletionItem) -> Optional[str]:
+ text_edit = item.get('textEdit')
+ if text_edit and 'insert' in text_edit and 'replace' in text_edit:
+ insert_mode = userprefs().completion_insert_mode
+ oposite_insert_mode = 'Replace' if insert_mode == 'insert' else 'Insert'
+ command_url = sublime.command_url("lsp_commit_completion_with_opposite_insert_mode")
+ return "{}".format(command_url, oposite_insert_mode)
+ return None
diff --git a/sublime-package.json b/sublime-package.json
index 1cd7743fb..77f059d39 100644
--- a/sublime-package.json
+++ b/sublime-package.json
@@ -464,6 +464,12 @@
"default": true,
"markdownDescription": "Disable Sublime Text's word completions. \"word\" completion means Sublime Text's internal completer that takes words from the current buffer you're editing and presents them in the auto-complete widget."
},
+ "completion_insert_mode": {
+ "type": "string",
+ "default": "insert",
+ "enum": ["insert", "replace"],
+ "markdownDescription": "The mode used for inserting completions:\n\n - `insert` would insert the completion text in a middle of the word\n\n - `replace` would replace the existing word with a new completion text\n\n An LSP keybinding `lsp_commit_completion_with_opposite_insert_mode`\n can be used to insert completion using the opposite mode to the one selected here.\n\n Note: Must be supported by the language server."
+ },
"show_references_in_quick_panel": {
"type": "boolean",
"default": false,
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 0afa86947..bb7bba9d2 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -47,7 +47,7 @@ def move_cursor(self, row: int, col: int) -> None:
s.clear()
s.add(point)
- def create_commit_completion_closure(self) -> Callable[[], bool]:
+ def create_commit_completion_closure(self, commit_completion_command="commit_completion") -> Callable[[], bool]:
committed = False
current_change_count = self.view.change_count()
@@ -57,7 +57,7 @@ def commit_completion() -> bool:
nonlocal committed
nonlocal current_change_count
if not committed:
- self.view.run_command("commit_completion")
+ self.view.run_command(commit_completion_command)
committed = True
return self.view.change_count() > current_change_count
@@ -67,6 +67,10 @@ def select_completion(self) -> 'Generator':
self.view.run_command('auto_complete')
yield self.create_commit_completion_closure()
+ def shift_select_completion(self) -> 'Generator':
+ self.view.run_command('auto_complete')
+ yield self.create_commit_completion_closure("lsp_commit_completion_with_opposite_insert_mode")
+
def read_file(self) -> str:
return self.view.substr(sublime.Region(0, self.view.size()))
@@ -603,6 +607,36 @@ def test_text_edit_plaintext_with_multiple_lines_indented(self) -> Generator[Non
# the "b" should be intended one level deeper
self.assertEqual(self.read_file(), '\t\n\ta\n\t\tb')
+ def test_insert_insert_mode(self) -> 'Generator':
+ self.type('{{ title }}')
+ self.move_cursor(0, 5) # Put the cursor inbetween 'i' and 't'
+ self.set_response("textDocument/completion", [{
+ 'label': 'title',
+ 'textEdit': {
+ 'newText': 'title',
+ 'insert': {'start': {'line': 0, 'character': 3}, 'end': {'line': 0, 'character': 5}},
+ 'replace': {'start': {'line': 0, 'character': 3}, 'end': {'line': 0, 'character': 8}}
+ }
+ }])
+ yield from self.select_completion()
+ yield from self.await_message("textDocument/completion")
+ self.assertEqual(self.read_file(), '{{ titletle }}')
+
+ def test_replace_insert_mode(self) -> 'Generator':
+ self.type('{{ title }}')
+ self.move_cursor(0, 4) # Put the cursor inbetween 't' and 'i'
+ self.set_response("textDocument/completion", [{
+ 'label': 'turtle',
+ 'textEdit': {
+ 'newText': 'turtle',
+ 'insert': {'start': {'line': 0, 'character': 3}, 'end': {'line': 0, 'character': 4}},
+ 'replace': {'start': {'line': 0, 'character': 3}, 'end': {'line': 0, 'character': 8}}
+ }
+ }])
+ yield from self.shift_select_completion() # commit the opposite insert mode
+ yield from self.await_message("textDocument/completion")
+ self.assertEqual(self.read_file(), '{{ turtle }}')
+
def test_show_deprecated_flag(self) -> None:
item_with_deprecated_flag = {
"label": 'hello',