Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new completion API #866

Merged
merged 71 commits into from
Mar 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
2a13eb5
Don't trigger AC on word_separators
predragnikolic Jan 4, 2020
0812061
Merge branch 'master' into add_auto_complete_ignored_triggers_setting
predragnikolic Jan 4, 2020
7cff503
Add back complete_all_chars
predragnikolic Jan 4, 2020
5dd6977
fix pycodestyle error
predragnikolic Jan 4, 2020
0a15bf6
Update plugin/completion.py
predragnikolic Jan 8, 2020
3ea2e42
Remove ignored_trigger_chars
predragnikolic Jan 8, 2020
0b58895
Integrate new completion api
predrag-codetribe Jan 13, 2020
12e196e
fix typo
predrag-codetribe Jan 13, 2020
8320d05
Support snippets
predragnikolic Jan 13, 2020
a9e7672
Merge branch 'master' into add_auto_complete_ignored_triggers_setting
predragnikolic Jan 13, 2020
c43c278
Insert textEdit completion,
predragnikolic Jan 13, 2020
e21b1a9
update text icon and remove unused function
predragnikolic Jan 13, 2020
a86c249
FIX AttributeError: module 'asyncio' has no attribute 'get_running_loop'
predragnikolic Jan 14, 2020
2f5aeae
Use text edit Range
predragnikolic Jan 25, 2020
59c6c0b
Merge branch 'fix-running-tests' into add_auto_complete_ignored_trigg…
predragnikolic Jan 25, 2020
534ae8a
use text edit range
predragnikolic Jan 25, 2020
c3d8807
make flake and mypy happy
predragnikolic Jan 27, 2020
40ef4be
Merge branch 'master' into add_auto_complete_ignored_triggers_setting
predragnikolic Jan 28, 2020
ea29e0b
Add typings and remove unused class property
predragnikolic Jan 28, 2020
7d78abb
When choosing a completion item the typed text (prefix) will be delet…
predragnikolic Jan 28, 2020
97ae903
Add cache
predragnikolic Jan 28, 2020
ea4dd30
Ignore mypy error instead of casting a type
predragnikolic Jan 30, 2020
8a11721
revert back timeout
predragnikolic Jan 30, 2020
f9d8bb9
Remove committing flag
predragnikolic Jan 30, 2020
793ec40
better comment
predragnikolic Jan 30, 2020
10ad58a
revert accidentally committed code
predragnikolic Jan 30, 2020
d2505b0
remove last_text_command as it is not necessary
predragnikolic Jan 30, 2020
542b489
remove completion_hint_type setting
predragnikolic Jan 30, 2020
d5a5d62
remove quotes from types
predragnikolic Jan 30, 2020
d7fd65d
Merge branch 'master' into add_auto_complete_ignored_triggers_setting
predragnikolic Feb 2, 2020
f46548a
Use text_edit['range'].end and remove duplicated functions
predragnikolic Feb 3, 2020
ff9e0ae
Skip calculating the offset by inserting the removed prefix first. An…
predragnikolic Feb 4, 2020
2f504f7
Add types and remove trigger_chars from instance properties
predragnikolic Feb 4, 2020
896dcef
Fix flake
predragnikolic Feb 4, 2020
87ab9e3
remove auto_complete_selector
predragnikolic Feb 4, 2020
1744b16
Handle multiple cursor completions
predragnikolic Feb 8, 2020
5782b46
Can be(will be probably) reverted. This is my try to fix the completi…
predragnikolic Feb 8, 2020
316d29d
Change keyword icon "κ" to something that looks like a key => "☌"
predragnikolic Feb 9, 2020
64627ee
Merge branch 'st4000-exploration' into add_auto_complete_ignored_trig…
predragnikolic Feb 9, 2020
8dd14b3
Revert "Change keyword icon "κ" to something that looks like a key =>…
predragnikolic Feb 9, 2020
f47083e
Remove if check for document_position
predragnikolic Feb 9, 2020
542245e
fix typo
predragnikolic Feb 9, 2020
44a3b2a
erase regoion for snippet, replace region for plain text
predragnikolic Feb 15, 2020
33496ac
don't allocate a dict
predragnikolic Feb 15, 2020
7b154e6
remove indent
predragnikolic Feb 15, 2020
c0805be
fix other failing tests
predragnikolic Feb 15, 2020
b528475
Use set_response and await_message in favour of test_compmpletion pro…
predragnikolic Feb 15, 2020
2497a45
Send a resolve request only if we don't have apply_additional_edits o…
predragnikolic Feb 17, 2020
9fe640a
Merge branch 'master' into add_auto_complete_ignored_triggers_setting
predragnikolic Feb 17, 2020
a99cff7
Add E2E tests for prefering insert text over label and text edit over…
predragnikolic Feb 17, 2020
49ccda6
add test to apply additional edits only one time
predragnikolic Feb 17, 2020
cc475e9
set "auto_complete_preserve_order" to "none" in tests
predragnikolic Feb 17, 2020
61099bc
make mypy and pyflake happy
predragnikolic Feb 17, 2020
9e54c83
Remove "complete_all_chars" setting in favor of sublime default "auto…
predragnikolic Feb 20, 2020
6fed5d3
Add test for intelephense
predragnikolic Feb 20, 2020
dae5eba
remove old completion samples
predragnikolic Feb 20, 2020
1ccb7ab
Fix sublime removing $ from the prefix
predragnikolic Feb 22, 2020
036e432
Revert "erase regoion for snippet, replace region for plain text"
predragnikolic Feb 22, 2020
d974c22
Workaround for TextEdit range not being valid when selection a comple…
predragnikolic Mar 23, 2020
e3f580c
Better handle multicursor text edits
predragnikolic Mar 23, 2020
f207bf2
Move save lines to on_query_completions, and clean up the a little
predragnikolic Mar 23, 2020
4468f8a
make flake8 and mypy happy
predragnikolic Mar 23, 2020
f38aaa0
Merge branch 'master' into add_auto_complete_ignored_triggers_setting
predragnikolic Mar 23, 2020
bc687e5
fix typo
predragnikolic Mar 23, 2020
07f7824
Dont use static class variables for saving the Line contents, because…
predragnikolic Mar 26, 2020
51bbd4d
fix for commit_completion re-triggers the completion panel "forever"
predragnikolic Mar 26, 2020
b5c0de6
Better handle multicursor text edits
predragnikolic Mar 27, 2020
34ae9e1
use transform_region to restore file contents
predragnikolic Mar 27, 2020
18c47e3
Resolve the completion promise in the handle_error
predragnikolic Mar 28, 2020
6e596e4
remove hiding auto_complete gloably after commit_completions
predragnikolic Mar 28, 2020
9d0d7ea
Merge branch 'st4000-exploration' into add_auto_complete_ignored_trig…
predragnikolic Mar 28, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions LSP.sublime-settings
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,6 @@
// Show symbol action links in hover popup if available
"show_symbol_action_links": false,

// Request completions for all characters if set to true,
// or just after trigger characters only otherwise.
"complete_all_chars": true,

// Controls which hints the completion panel displays
// "auto": completion details if available or kind otherwise
// "detail": completion details if available
// "kind": completion kind if available
// "none": completion item label only
"completion_hint_type": "auto",

// Disable Sublime Text's explicit and word completion.
"only_show_lsp_completions": false,

Expand Down
2 changes: 0 additions & 2 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ Add these settings to your Sublime settings, Syntax-specific settings and/or in

### Package settings (LSP)

* `complete_all_chars` `true` *request completions for all characters, not just trigger characters*
* `only_show_lsp_completions` `false` *disable sublime word completion and snippets from autocomplete lists*
* `completion_hint_type` `"auto"` *override automatic completion hints with "detail", "kind" or "none"*
* `show_references_in_quick_panel` `false` *show symbol references in Sublime's quick panel instead of the bottom panel*
* `show_view_status` `true` *show permanent language server status in the status bar*
* `auto_show_diagnostics_panel` `always` (`never`, `saved`) *open the diagnostics panel automatically if there are diagnostics*
Expand Down
381 changes: 120 additions & 261 deletions plugin/completion.py

Large diffs are not rendered by default.

127 changes: 57 additions & 70 deletions plugin/core/completion.py
Original file line number Diff line number Diff line change
@@ -1,76 +1,63 @@
from .protocol import CompletionItemKind, Range
from .types import Settings
from .logging import debug
import sublime
from .restore_lines import RestoreLines
from .typing import Tuple, Optional, Dict, List, Union


completion_item_kind_names = {v: k for k, v in CompletionItemKind.__dict__.items()}


def get_completion_hint(item: dict, settings: Settings) -> Optional[str]:
# choose hint based on availability and user preference
hint = None
if settings.completion_hint_type == "auto":
hint = item.get("detail")
if not hint:
kind = item.get("kind")
if kind:
hint = completion_item_kind_names[kind]
elif settings.completion_hint_type == "detail":
hint = item.get("detail")
elif settings.completion_hint_type == "kind":
kind = item.get("kind")
if kind:
hint = completion_item_kind_names.get(kind)
return hint


def format_completion(item: dict, word_col: int, settings: Settings) -> Tuple[str, str]:
# Sublime handles snippets automatically, so we don't have to care about insertTextFormat.
trigger = item["label"]

hint = get_completion_hint(item, settings)

# label is an alternative for insertText if neither textEdit nor insertText is provided
replacement = text_edit_text(item, word_col) or item.get("insertText") or trigger

if replacement[0] != trigger[0]:
# fix some common cases when server sends different start on label and replacement.
if replacement[0] == '$':
trigger = '$' + trigger # add missing $
elif replacement[0] == '-':
trigger = '-' + trigger # add missing -
elif trigger[0] == ':':
replacement = ':' + replacement # add missing :
elif trigger[0] == '$':
trigger = trigger[1:] # remove leading $
elif trigger[0] == ' ' or trigger[0] == '•':
trigger = trigger[1:] # remove clangd insertion indicator
else:
debug("WARNING: Replacement prefix does not match trigger '{}'".format(trigger))

if len(replacement) > 0 and replacement[0] == '$': # sublime needs leading '$' escaped.
replacement = '\\$' + replacement[1:]
# only return trigger with a hint if available
return "\t ".join((trigger, hint)) if hint else trigger, replacement


def text_edit_text(item: dict, word_col: int) -> Optional[str]:
text_edit = item.get('textEdit')
if text_edit:
edit_range, edit_text = text_edit.get("range"), text_edit.get("newText")
if edit_range and edit_text:
edit_range = Range.from_lsp(edit_range)

# debug('textEdit from col {}, {} applied at col {}'.format(
# edit_range.start.col, edit_range.end.col, word_col))

if edit_range.start.col <= word_col:
# if edit starts at current word, we can use it.
# if edit starts before current word, use the whole thing and we'll fix it up later.
return edit_text

return None
completion_kinds = {
1: (sublime.KIND_ID_MARKUP, "Ξ", "Text"),
2: (sublime.KIND_ID_FUNCTION, "λ", "Method"),
3: (sublime.KIND_ID_FUNCTION, "λ", "Function"),
4: (sublime.KIND_ID_FUNCTION, "c", "Constructor"),
5: (sublime.KIND_ID_VARIABLE, "f", "Field"),
6: (sublime.KIND_ID_VARIABLE, "v", "Variable"),
7: (sublime.KIND_ID_TYPE, "c", "Class"),
8: (sublime.KIND_ID_TYPE, "i", "Interface"),
9: (sublime.KIND_ID_NAMESPACE, "◪", "Module"),
rwols marked this conversation as resolved.
Show resolved Hide resolved
10: (sublime.KIND_ID_VARIABLE, "ρ", "Property"),
11: (sublime.KIND_ID_VARIABLE, "u", "Unit"),
12: (sublime.KIND_ID_VARIABLE, "ν", "Value"),
13: (sublime.KIND_ID_TYPE, "ε", "Enum"),
14: (sublime.KIND_ID_KEYWORD, "κ", "Keyword"),
15: (sublime.KIND_ID_SNIPPET, "s", "Snippet"),
16: (sublime.KIND_ID_AMBIGUOUS, "c", "Color"),
17: (sublime.KIND_ID_AMBIGUOUS, "#", "File"),
18: (sublime.KIND_ID_AMBIGUOUS, "⇢", "Reference"),
19: (sublime.KIND_ID_AMBIGUOUS, "ƒ", "Folder"),
20: (sublime.KIND_ID_TYPE, "ε", "EnumMember"),
21: (sublime.KIND_ID_VARIABLE, "π", "Constant"),
22: (sublime.KIND_ID_TYPE, "s", "Struct"),
23: (sublime.KIND_ID_FUNCTION, "e", "Event"),
24: (sublime.KIND_ID_KEYWORD, "ο", "Operator"),
25: (sublime.KIND_ID_TYPE, "τ", "Type Parameter")
}


def format_completion(item: dict, restore_lines: RestoreLines) -> sublime.CompletionItem:
trigger = item.get('label') or ""
annotation = item.get('detail') or ""
kind = sublime.KIND_AMBIGUOUS

item_kind = item.get("kind")
if item_kind:
kind = completion_kinds.get(item_kind, sublime.KIND_AMBIGUOUS)

is_deprecated = item.get("deprecated", False)
if is_deprecated:
list_kind = list(kind)
list_kind[1] = '⚠'
list_kind[2] = "⚠ {} - Deprecated".format(list_kind[2])
kind = tuple(list_kind) # type: ignore
rwols marked this conversation as resolved.
Show resolved Hide resolved

return sublime.CompletionItem.command_completion(
trigger,
command="lsp_select_completion_item",
args={
"item": item,
"restore_lines_dict": restore_lines.to_dict()
},
annotation=annotation,
kind=kind
)


def parse_completion_response(response: Optional[Union[Dict, List]]) -> Tuple[List[Dict], bool]:
Expand Down
5 changes: 5 additions & 0 deletions plugin/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ class CompletionItemKind(object):
completion_item_kinds = list(range(CompletionItemKind.Text, CompletionItemKind.TypeParameter + 1))


class InsertTextFormat:
PlainText = 1
Snippet = 2


class DocumentHighlightKind(object):
Unknown = 0
Text = 1
Expand Down
51 changes: 51 additions & 0 deletions plugin/core/restore_lines.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import sublime
from .typing import List


class RestoreLines:
def __init__(self):
self.saved_lines = [] # type: List[dict]
Copy link
Member Author

@predragnikolic predragnikolic Mar 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to correct me, I don't know if I used the new API correctly :)

Here is my feedback:
I want to highlight the differences between the new api, and the code before it:

The code before it did this:

  1. It copied the row, text, cursor position of each line, right before sending the request.
  2. On selecting a completion item, it inserted back the text on each row.
    END RESULT: It brought the lines in a state like they were when the completions were requested.

The code in this commit does this.

  1. It copies the change_id, change_region(the region passed to the transform_region_from), text and cursor position for each line, right before sending the request.
  2. On selecting a completion item, the transform_region_from returns a transform_region. transform_region is used for removing and inserting back the previous text for each line.
 view.erase(edit, transform_region)
 view.insert(edit, transform_region.begin(), saved_line['text'])

// I could probably use view.replace instead of erase and insert

END RESULT: It brings the lines in a state like they were when the completions were requested.

You may have noticed that I did this a bit differently, than what John did in his gist.
https://gist.github.com/jskinner/4dfa6d86f848e1e1e583883507369b67#file-async_edits-py-L238-L244

    def fill_completions(self, edits):
        # The server has sent us a series of edits it wants to be made when
        # the completion command is run, however the user may have edited the
        # buffer since the request want sent. Adjust the response from the
        # server to account for these edits
        for c in self.changes_since_sent:
            edits = [(transform_region(r, c), text) for r, text in edits]

If I done the same thing as John did in his gist:
I would have to adjust the response from the server to account for all view edits that occurred,
and apply all TextChange-s to all completion items.
But that to me looks like a lot of work.

For me it looks easier to adjust the view state to the state it was when the request is send,
than to adjust the response from the server to account all the possible edits that may have had occurred.

Either way, I really want to hear the feedback from you all
And don't take me wrong with what I just said :)


def save_lines(self, locations: List[int], view: sublime.View) -> None:
change_id = view.change_id()

for point in locations:
line = view.line(point)
change_region = (line.begin(), line.end())
text = view.substr(line)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anything which prevents the line to be stored directly? Doing so would avoid converting region->tuple->region.

Copy link

@FichteFoll FichteFoll Mar 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regions can't be serialized for transmission in run_command, which is required for the commit completion command.

However, you should be able to use tuple(region) now that regions are iterable. I also think this step should happen at the communication edge were it is necessary to do so and not for the internal storage to make this kind of relation more obvious. I'm still not seing where the use case I assumed actually occurs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guess the python 3.3 version of sublime.Region does not support iteration. This will be available as soon as we can move to python 3.8

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know you could do that tuple(region) with a Region. Cool :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'Region' object is not iterable.

    saved_line['change_region'] = tuple(saved_line['change_region'])
TypeError: 'Region' object is not iterable

Looks like the LSP plugin is still using python 3.3.


self.saved_lines.append({
"change_id": change_id,
"change_region": change_region,
"text": text,
# cursor will be use retore the cursor the te exact position
"cursor": point
})

def to_dict(self):
return {
"saved_lines": self.saved_lines
}

@staticmethod
def from_dict(dictionary):
restore_lines = RestoreLines()
restore_lines.saved_lines = dictionary["saved_lines"]
return restore_lines

def restore_lines(self, edit: sublime.Edit, view: sublime.View) -> None:
# restore lines contents
# insert back lines from the bottom to top
for saved_line in reversed(self.saved_lines):
change_id = saved_line['change_id']
begin, end = saved_line['change_region']
change_region = sublime.Region(begin, end)

transform_region = view.transform_region_from(change_region, change_id)
view.erase(edit, transform_region)
view.insert(edit, transform_region.begin(), saved_line['text'])

# restore old cursor position
view.sel().clear()
for saved_line in self.saved_lines:
view.sel().add(saved_line["cursor"])
3 changes: 2 additions & 1 deletion plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ def get_initialize_params(workspace_folders: List[WorkspaceFolder], config: Clie
},
"completion": {
"completionItem": {
"snippetSupport": True
"snippetSupport": True,
"deprecatedSupport": True
},
"completionItemKind": {
"valueSet": completion_item_kinds
Expand Down
2 changes: 0 additions & 2 deletions plugin/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,6 @@ def update_settings(settings: Settings, settings_obj: sublime.Settings) -> None:
settings.show_code_actions_bulb = read_bool_setting(settings_obj, "show_code_actions_bulb", False)
settings.show_symbol_action_links = read_bool_setting(settings_obj, "show_symbol_action_links", False)
settings.only_show_lsp_completions = read_bool_setting(settings_obj, "only_show_lsp_completions", False)
settings.complete_all_chars = read_bool_setting(settings_obj, "complete_all_chars", True)
settings.completion_hint_type = read_str_setting(settings_obj, "completion_hint_type", "auto")
settings.show_references_in_quick_panel = read_bool_setting(settings_obj, "show_references_in_quick_panel", False)
settings.disabled_capabilities = read_array_setting(settings_obj, "disabled_capabilities", [])
settings.log_debug = read_bool_setting(settings_obj, "log_debug", False)
Expand Down
2 changes: 0 additions & 2 deletions plugin/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ def __init__(self) -> None:
self.diagnostics_gutter_marker = "dot"
self.show_code_actions_bulb = False
self.show_symbol_action_links = False
self.complete_all_chars = False
self.completion_hint_type = "auto"
self.show_references_in_quick_panel = False
self.disabled_capabilities = [] # type: List[str]
self.log_debug = True
Expand Down
45 changes: 43 additions & 2 deletions stubs/sublime.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# NOTE: This dynamically typed stub was automatically generated by stubgen.

from typing import Any, Optional, Callable, Sequence, Tuple, Union, List, Sized
from typing import Any, Optional, Callable, Sequence, Tuple, Union, List, Sized, Iterator


class _LogWriter:
Expand Down Expand Up @@ -55,6 +55,7 @@ CLASS_LINE_END = ... # type: int
CLASS_EMPTY_LINE = ... # type: int
INHIBIT_WORD_COMPLETIONS = ... # type: int
INHIBIT_EXPLICIT_COMPLETIONS = ... # type: int
DYNAMIC_COMPLETIONS = ... # type: int
DIALOG_CANCEL = ... # type: int
DIALOG_YES = ... # type: int
DIALOG_NO = ... # type: int
Expand All @@ -67,6 +68,24 @@ UI_ELEMENT_OPEN_FILES = ... # type: int
LAYOUT_INLINE = ... # type: int
LAYOUT_BELOW = ... # type: int
LAYOUT_BLOCK = ... # type: int
KIND_ID_AMBIGUOUS = ... # type: int
KIND_ID_KEYWORD = ... # type: int
KIND_ID_TYPE = ... # type: int
KIND_ID_FUNCTION = ... # type: int
KIND_ID_NAMESPACE = ... # type: int
KIND_ID_NAVIGATION = ... # type: int
KIND_ID_MARKUP = ... # type: int
KIND_ID_VARIABLE = ... # type: int
KIND_ID_SNIPPET = ... # type: int
KIND_AMBIGUOUS = ... # type: Tuple[int, str, str]
KIND_KEYWORD = ... # type: Tuple[int, str, str]
KIND_TYPE = ... # type: Tuple[int, str, str]
KIND_FUNCTION = ... # type: Tuple[int, str, str]
KIND_NAMESPACE = ... # type: Tuple[int, str, str]
KIND_NAVIGATION = ... # type: Tuple[int, str, str]
KIND_MARKUP = ... # type: Tuple[int, str, str]
KIND_VARIABLE = ... # type: Tuple[int, str, str]
KIND_SNIPPET = ... # type: Tuple[int, str, str]


class Settings:
Expand Down Expand Up @@ -238,6 +257,22 @@ def get_macro() -> Sequence[dict]:
...


class CompletionItem:
@classmethod
def command_completion(cls,
trigger: str,
command: str,
args: dict = {},
annotation: str = "",
kind: Tuple[int, str, str] = KIND_AMBIGUOUS
) -> 'CompletionItem':
...

class CompletionList:
def set_completions(self, completions: List[CompletionItem], flags: int = 0) -> None:
...


class Window:
window_id = ... # type: int
settings_object = ... # type: Settings
Expand Down Expand Up @@ -478,6 +513,12 @@ class Selection(Sized):
def __len__(self) -> int:
...

def __iter__(self) -> Iterator[Region]:
...

def __next__(self) -> Region:
...

def __getitem__(self, index: int) -> Region:
...

Expand All @@ -491,7 +532,7 @@ class Selection(Sized):
def clear(self) -> None:
...

def add(self, x: Region) -> None:
def add(self, x: Union[Region, int]) -> None:
...

def add_all(self, regions: Sequence[Region]) -> None:
Expand Down
Loading