Skip to content

Commit

Permalink
Use filterText as trigger, if it's present (#990)
Browse files Browse the repository at this point in the history
The LSP-filterText is truly the suitable thing for the ST-trigger.

LSP-filterText -> ST-trigger
LSP-label -> ST-annotation
LSP-detail -> ST-details

OR

LSP-label -> ST-trigger
LSP-detail -> ST-annotation

We'll handle the LSP-documentation at later stage. It's too expensive to compute all docstrings in format_completion.
  • Loading branch information
rwols authored Apr 28, 2020
1 parent d936b04 commit fb209f6
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 88 deletions.
21 changes: 18 additions & 3 deletions plugin/completion.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import html
import sublime
import sublime_plugin

Expand Down Expand Up @@ -192,6 +193,7 @@ def on_query_completions(self, prefix: str, locations: List[int]) -> Optional[su
return completion_list

def format_completion(self, item: dict) -> sublime.CompletionItem:
# This is a hot function. Don't do heavy computations or IO in this function.
item_kind = item.get("kind")
if item_kind:
kind = completion_kinds.get(item_kind, sublime.KIND_AMBIGUOUS)
Expand Down Expand Up @@ -220,12 +222,25 @@ def format_completion(self, item: dict) -> sublime.CompletionItem:
convert = self.view.text_point_utf16
item["native_region"] = (convert(row, start_col_utf16), convert(row, end_col_utf16))

lsp_label = item["label"]
lsp_filter_text = item.get("filterText")
lsp_detail = item.get("detail") or ""
if lsp_filter_text:
st_trigger = lsp_filter_text
st_annotation = lsp_label
st_details = html.escape(lsp_detail.replace('\n', ' '))
else:
st_trigger = lsp_label
st_annotation = lsp_detail
st_details = ''

return sublime.CompletionItem.command_completion(
trigger=item["label"],
trigger=st_trigger,
command="lsp_select_completion_item",
args=item,
annotation=item.get('detail') or "",
kind=kind
annotation=st_annotation,
kind=kind,
details=st_details
)

def handle_response(self, response: Optional[Union[dict, List]], completion_list: sublime.CompletionList) -> None:
Expand Down
3 changes: 2 additions & 1 deletion stubs/sublime.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,8 @@ class CompletionItem:
command: str,
args: dict = {},
annotation: str = "",
kind: Tuple[int, str, str] = KIND_AMBIGUOUS
kind: Tuple[int, str, str] = KIND_AMBIGUOUS,
details: str = ""
) -> 'CompletionItem':
...

Expand Down
227 changes: 143 additions & 84 deletions tests/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from setup import CI, SUPPORTED_SYNTAX, TextDocumentTestCase, add_config, remove_config, text_config
from unittesting import DeferrableTestCase
import sublime
from sublime_plugin import view_event_listeners, ViewEventListener


additional_edits = {
Expand Down Expand Up @@ -160,83 +159,124 @@ def test_var_prefix_using_label(self) -> 'Generator':

def test_var_prefix_added_in_insertText(self) -> 'Generator':
"""
Powershell: label='true', insertText='$true' (see https://github.com/sublimelsp/LSP/issues/294)
https://github.com/sublimelsp/LSP/issues/294
User types '$env:U', server replaces '$env:U' with '$env:USERPROFILE'
"""
yield from self.verify(
completion_items=[{
"insertText": "$true",
"label": "true",
"textEdit": {
"newText": "$true",
"range": {
"end": {
"character": 5,
"line": 0
},
"start": {
"character": 0,
"line": 0
}
'filterText': '$env:USERPROFILE',
'insertText': '$env:USERPROFILE',
'sortText': '0006USERPROFILE',
'label': 'USERPROFILE',
'additionalTextEdits': None,
'detail': None,
'data': None,
'kind': 6,
'command': None,
'textEdit': {
'newText': '$env:USERPROFILE',
'range': {
'end': {'line': 0, 'character': 6},
'start': {'line': 0, 'character': 0}
}
}
},
'commitCharacters': None,
'range': None,
'documentation': None
}],
insert_text="$",
expected_text="$true")
insert_text="$env:U",
expected_text="$env:USERPROFILE")

def test_var_prefix_added_in_label(self) -> 'Generator':
def test_pure_insertion_text_edit(self) -> 'Generator':
"""
PHP language server: label='$someParam', textEdit='someParam' (https://github.com/sublimelsp/LSP/issues/368)
https://github.com/sublimelsp/LSP/issues/368
User types '$so', server returns pure insertion completion 'meParam', completing it to '$someParam'.
THIS TEST FAILS
"""
yield from self.verify(
completion_items=[{
'label': '$what',
'textEdit': {
'newText': 'meParam',
'range': {
'start': {
'line': 0,
'character': 0
},
'end': {
'line': 0,
'character': 1
}
},
'newText': '$what'
}
'end': {'character': 4, 'line': 0},
'start': {'character': 4, 'line': 0} # pure insertion!
}
},
'label': '$someParam',
'filterText': None,
'data': None,
'command': None,
'detail': 'null',
'insertText': None,
'additionalTextEdits': None,
'sortText': None,
'documentation': None,
'kind': 6
}],
insert_text="$",
expected_text="$what")
insert_text="$so",
expected_text="$someParam")

def test_space_added_in_label(self) -> 'Generator':
"""
Clangd: label=" const", insertText="const" (https://github.com/sublimelsp/LSP/issues/368)
"""
yield from self.verify(
completion_items=[{'label': ' const', 'insertText': 'const'}],
insert_text='',
expected_text="const")
completion_items=[{
"label": " const",
"sortText": "3f400000const",
"kind": 14,
"textEdit": {
"newText": "const",
"range": {
"end": {
"character": 1,
"line": 0
},
"start": {
"character": 3,
"line": 0
}
}
},
"insertTextFormat": 2,
"insertText": "const",
"filterText": "const",
"score": 6
}],
insert_text=' co',
expected_text=" const") # NOT 'const'

def test_dash_missing_from_label(self) -> 'Generator':
"""
Powershell: label="UniqueId", insertText="-UniqueId" (https://github.com/sublimelsp/LSP/issues/572)
Powershell: label="UniqueId", trigger="-UniqueIdd, text to be inserted = "-UniqueId"
(https://github.com/sublimelsp/LSP/issues/572)
"""
yield from self.verify(
completion_items=[{
'label': 'UniqueId',
'insertText': '-UniqueId',
'textEdit': {
'range': {
'start': {
'character': 0,
'line': 0
},
'end': {
'character': 1,
'line': 0
}
"filterText": "-UniqueId",
"documentation": None,
"textEdit": {
"range": {
"start": {"character": 14, "line": 0},
"end": {"character": 15, "line": 0}
},
'newText': '-UniqueId'
}
"newText": "-UniqueId"
},
"commitCharacters": None,
"command": None,
"label": "UniqueId",
"insertText": "-UniqueId",
"additionalTextEdits": None,
"data": None,
"range": None,
"insertTextFormat": 1,
"sortText": "0001UniqueId",
"kind": 6,
"detail": "[string[]]"
}],
insert_text="u",
expected_text="-UniqueId")
Expand Down Expand Up @@ -268,55 +308,74 @@ def test_edit_before_cursor(self) -> 'Generator':

def test_edit_after_nonword(self) -> 'Generator':
"""
Metals: List.| selects label instead of textedit
See https://github.com/sublimelsp/LSP/issues/645
https://github.com/sublimelsp/LSP/issues/645
"""
yield from self.verify(
completion_items=[{
'insertTextFormat': 2,
'label': 'apply[A](xs: A*): List[A]',
'textEdit': {
'newText': 'apply($0)',
'range': {
'start': {
'line': 0,
'character': 5
"textEdit": {
"newText": "apply($0)",
"range": {
"end": {
"line": 0,
"character": 5
},
'end': {
'line': 0,
'character': 5
"start": {
"line": 0,
"character": 5
}
}
}
},
"label": "apply[A](xs: A*): List[A]",
"sortText": "00000",
"preselect": True,
"insertTextFormat": 2,
"filterText": "apply",
"data": {
"symbol": "scala/collection/immutable/List.apply().",
"target": "file:/home/user/workspace/testproject/?id=root"
},
"kind": 2
}],
insert_text="List.",
expected_text='List.apply()')

def test_implement_all_members_quirk(self) -> 'Generator':
def test_filter_text_is_not_a_prefix_of_label(self) -> 'Generator':
"""
Metals: "Implement all members" should just select the newText.
Metals: "Implement all members"
The filterText is 'e', so when the user types 'e', one of the completion items should be
"Implement all members".
VSCode doesn't show the filterText in this case; it'll only show "Implement all members".
c.f. https://github.com/microsoft/language-server-protocol/issues/898#issuecomment-593968008
In SublimeText, we always show the filterText (a.k.a. trigger).
This is one of the more confusing and contentious completion items.
https://github.com/sublimelsp/LSP/issues/771
"""
yield from self.verify(
completion_items=[{
'insertTextFormat': 2,
'label': 'Implement all members',
'textEdit': {
'newText': 'def foo: Int \u003d ${0:???}\n def boo: Int \u003d ${0:???}',
'range': {
'start': {
'line': 0,
'character': 0
},
'end': {
'line': 0,
'character': 1
}
}
"label": "Implement all members",
"kind": 12,
"sortText": "00002",
"filterText": "e",
"insertTextFormat": 2,
"textEdit": {
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 1}
},
"newText": "def foo: Int \u003d ${0:???}\n def boo: Int \u003d ${0:???}"
},
"data": {
"target": "file:/Users/ckipp/Documents/scala-workspace/test-project/?id\u003droot",
"symbol": "local6"
}
}],
insert_text="I",
expected_text='def foo: Int = ???\n def boo: Int = ???')
insert_text='e',
expected_text='def foo: Int \u003d ???\n def boo: Int \u003d ???')

def test_additional_edits(self) -> 'Generator':
yield from self.verify(
Expand Down

0 comments on commit fb209f6

Please sign in to comment.