Skip to content

Commit

Permalink
Merge branch 'main' into feat/diag-annotations
Browse files Browse the repository at this point in the history
* main:
  Add insert-replace support for completions (#1809)
  • Loading branch information
rchl committed Sep 12, 2022
2 parents 33a0ef0 + 1be82e1 commit ea8186e
Show file tree
Hide file tree
Showing 12 changed files with 127 additions and 9 deletions.
19 changes: 19 additions & 0 deletions Default.sublime-keymap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions LSP.sublime-settings
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down
1 change: 1 addition & 0 deletions boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docs/src/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <kbd>F12</kbd> to trigger it.

To insert or replace a completion item using the opposite "completion_insert_mode" setting value, the following keybinding can be used <kbd>alt+enter</kbd>.
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)
Expand Down
7 changes: 4 additions & 3 deletions docs/src/keyboard_shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,18 @@ Refer to the [Customization section](customization.md#keyboard-shortcuts-key-bin
| Goto Declaration | unbound | `lsp_symbol_declaration`
| Goto Definition | unbound<br>suggested: <kbd>f12</kbd> | `lsp_symbol_definition`
| Goto Implementation | unbound | `lsp_symbol_implementation`
| Goto Symbol | unbound<br>suggested: <kbd>ctrl</kbd> <kbd>r</kbd> | `lsp_document_symbols`
| Goto Symbol in Project | unbound<br>suggested: <kbd>ctrl</kbd> <kbd>shift</kbd> <kbd>r</kbd> | `lsp_workspace_symbols`
| Goto Symbol | unbound<br>suggested: <kbd>ctrl</kbd> <kbd>r</kbd> | `lsp_document_symbols`
| Goto Type Definition | unbound | `lsp_symbol_type_definition`
| Hover Popup | unbound | `lsp_hover`
| Insert/Replace Completions | <kbd>alt</kbd> <kbd>enter</kbd> | `lsp_commit_completion_with_opposite_insert_mode`
| Next Diagnostic | <kbd>F4</kbd> | -
| Previous Diagnostic | <kbd>shift</kbd> <kbd>F4</kbd> | -
| 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 | <kbd>ctrl</kbd> <kbd>alt</kbd> <kbd>space</kbd> | `lsp_signature_help_show`
| Hover Popup | unbound | `lsp_hover`
| Toggle Diagnostics Panel | <kbd>ctrl</kbd> <kbd>alt</kbd> <kbd>m</kbd> | `lsp_show_diagnostics_panel`
| Toggle Log Panel | unbound | `lsp_toggle_server_panel`
27 changes: 24 additions & 3 deletions plugin/completion.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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]]
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 7 additions & 1 deletion plugin/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -341,7 +347,7 @@ class SemanticTokenModifiers:
'preselect': bool,
'sortText': str,
'tags': List[int],
'textEdit': TextEdit
'textEdit': Union[TextEdit, InsertReplaceEdit]
}, total=False)

CompletionList = TypedDict('CompletionList', {
Expand Down
1 change: 1 addition & 0 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
},
Expand Down
2 changes: 2 additions & 0 deletions plugin/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions plugin/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 "<a href='{}'>{}</a>".format(command_url, oposite_insert_mode)
return None
6 changes: 6 additions & 0 deletions sublime-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
38 changes: 36 additions & 2 deletions tests/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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

Expand All @@ -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()))

Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit ea8186e

Please sign in to comment.