Close #1162 - Implement completion support with LSP servers

This commit is contained in:
w0rp 2018-04-22 12:28:12 +01:00
parent 20241c87ef
commit d8a673515a
No known key found for this signature in database
GPG Key ID: 0FC1ECAA8C81CD83
9 changed files with 455 additions and 19 deletions

View File

@ -222,9 +222,7 @@ too. See `:help ale-fix` for detailed information.
ALE offers some support for completion via hijacking of omnicompletion while you ALE offers some support for completion via hijacking of omnicompletion while you
type. All of ALE's completion information must come from Language Server type. All of ALE's completion information must come from Language Server
Protocol linters, or similar protocols. At the moment, completion is only Protocol linters, or from `tsserver` for TypeSript.
supported for TypeScript code with `tsserver`, when `tsserver` is enabled. You
can enable completion like so:
```vim ```vim
" Enable completion where available. " Enable completion where available.

View File

@ -25,4 +25,5 @@ call ale#linter#Define('python', {
\ 'command_callback': 'ale_linters#python#pyls#GetCommand', \ 'command_callback': 'ale_linters#python#pyls#GetCommand',
\ 'language_callback': 'ale_linters#python#pyls#GetLanguage', \ 'language_callback': 'ale_linters#python#pyls#GetLanguage',
\ 'project_root_callback': 'ale#python#FindProjectRoot', \ 'project_root_callback': 'ale#python#FindProjectRoot',
\ 'completion_filter': 'ale#completion#python#CompletionItemFilter',
\}) \})

View File

@ -28,6 +28,7 @@ let s:LSP_COMPLETION_REFERENCE_KIND = 18
" the insert cursor is. If one of these matches, we'll check for completions. " the insert cursor is. If one of these matches, we'll check for completions.
let s:should_complete_map = { let s:should_complete_map = {
\ '<default>': '\v[a-zA-Z$_][a-zA-Z$_0-9]*$|\.$', \ '<default>': '\v[a-zA-Z$_][a-zA-Z$_0-9]*$|\.$',
\ 'rust': '\v[a-zA-Z$_][a-zA-Z$_0-9]*$|\.$|::$',
\} \}
" Regular expressions for finding the start column to replace with completion. " Regular expressions for finding the start column to replace with completion.
@ -38,6 +39,7 @@ let s:omni_start_map = {
" A map of exact characters for triggering LSP completions. " A map of exact characters for triggering LSP completions.
let s:trigger_character_map = { let s:trigger_character_map = {
\ '<default>': ['.'], \ '<default>': ['.'],
\ 'rust': ['.', '::'],
\} \}
function! s:GetFiletypeValue(map, filetype) abort function! s:GetFiletypeValue(map, filetype) abort
@ -215,7 +217,21 @@ function! ale#completion#ParseTSServerCompletionEntryDetails(response) abort
return l:results return l:results
endfunction endfunction
function! ale#completion#NullFilter(buffer, item) abort
return 1
endfunction
function! ale#completion#ParseLSPCompletions(response) abort function! ale#completion#ParseLSPCompletions(response) abort
let l:buffer = bufnr('')
let l:info = get(b:, 'ale_completion_info', {})
let l:Filter = get(l:info, 'completion_filter', v:null)
if l:Filter is v:null
let l:Filter = function('ale#completion#NullFilter')
else
let l:Filter = ale#util#GetFunction(l:Filter)
endif
let l:item_list = [] let l:item_list = []
if type(get(a:response, 'result')) is type([]) if type(get(a:response, 'result')) is type([])
@ -228,6 +244,16 @@ function! ale#completion#ParseLSPCompletions(response) abort
let l:results = [] let l:results = []
for l:item in l:item_list for l:item in l:item_list
if !call(l:Filter, [l:buffer, l:item])
continue
endif
let l:word = matchstr(l:item.label, '\v^[^(]+')
if empty(l:word)
continue
endif
" See :help complete-items for Vim completion kinds " See :help complete-items for Vim completion kinds
if l:item.kind is s:LSP_COMPLETION_METHOD_KIND if l:item.kind is s:LSP_COMPLETION_METHOD_KIND
let l:kind = 'm' let l:kind = 'm'
@ -244,11 +270,11 @@ function! ale#completion#ParseLSPCompletions(response) abort
endif endif
call add(l:results, { call add(l:results, {
\ 'word': l:item.label, \ 'word': l:word,
\ 'kind': l:kind, \ 'kind': l:kind,
\ 'icase': 1, \ 'icase': 1,
\ 'menu': l:item.detail, \ 'menu': l:item.detail,
\ 'info': l:item.documentation, \ 'info': get(l:item, 'documentation', ''),
\}) \})
endfor endfor
@ -349,6 +375,10 @@ function! s:GetLSPCompletions(linter) abort
if l:request_id if l:request_id
let b:ale_completion_info.conn_id = l:id let b:ale_completion_info.conn_id = l:id
let b:ale_completion_info.request_id = l:request_id let b:ale_completion_info.request_id = l:request_id
if has_key(a:linter, 'completion_filter')
let b:ale_completion_info.completion_filter = a:linter.completion_filter
endif
endif endif
endfunction endfunction
@ -378,10 +408,7 @@ function! ale#completion#GetCompletions() abort
for l:linter in ale#linter#Get(&filetype) for l:linter in ale#linter#Get(&filetype)
if !empty(l:linter.lsp) if !empty(l:linter.lsp)
if l:linter.lsp is# 'tsserver' call s:GetLSPCompletions(l:linter)
\|| get(g:, 'ale_completion_experimental_lsp_support', 0)
call s:GetLSPCompletions(l:linter)
endif
endif endif
endfor endfor
endfunction endfunction

View File

@ -0,0 +1,3 @@
function! ale#completion#python#CompletionItemFilter(buffer, item) abort
return a:item.label !~# '\v^__[a-z_]+__'
endfunction

View File

@ -194,6 +194,14 @@ function! ale#linter#PreProcess(linter) abort
if !s:IsCallback(l:obj.project_root_callback) if !s:IsCallback(l:obj.project_root_callback)
throw '`project_root_callback` must be a callback for LSP linters' throw '`project_root_callback` must be a callback for LSP linters'
endif endif
if has_key(a:linter, 'completion_filter')
let l:obj.completion_filter = a:linter.completion_filter
if !s:IsCallback(l:obj.completion_filter)
throw '`completion_filter` must be a callback'
endif
endif
endif endif
let l:obj.output_stream = get(a:linter, 'output_stream', 'stdout') let l:obj.output_stream = get(a:linter, 'output_stream', 'stdout')

View File

@ -2,12 +2,9 @@ Before:
Save g:ale_completion_enabled Save g:ale_completion_enabled
Save g:ale_completion_delay Save g:ale_completion_delay
Save g:ale_completion_max_suggestions Save g:ale_completion_max_suggestions
Save g:ale_completion_experimental_lsp_support
Save &l:omnifunc Save &l:omnifunc
Save &l:completeopt Save &l:completeopt
unlet! g:ale_completion_experimental_lsp_support
let g:ale_completion_enabled = 1 let g:ale_completion_enabled = 1
let g:get_completions_called = 0 let g:get_completions_called = 0
let g:feedkeys_calls = [] let g:feedkeys_calls = []
@ -43,7 +40,6 @@ After:
unlet! b:ale_completion_response unlet! b:ale_completion_response
unlet! b:ale_completion_parser unlet! b:ale_completion_parser
unlet! b:ale_complete_done_time unlet! b:ale_complete_done_time
unlet! g:ale_completion_experimental_lsp_support
delfunction CheckCompletionCalled delfunction CheckCompletionCalled

View File

@ -17,3 +17,20 @@ Execute(Completion should not be done after parens in TypeScript):
Execute(Completion prefixes should work for other filetypes): Execute(Completion prefixes should work for other filetypes):
AssertEqual 'ab', ale#completion#GetPrefix('xxxyyyzzz', 3, 14) AssertEqual 'ab', ale#completion#GetPrefix('xxxyyyzzz', 3, 14)
Given rust():
let abc = y.
let abc = String::
let foo = (ab)
Execute(Completion should be done after dots in Rust):
AssertEqual '.', ale#completion#GetPrefix(&filetype, 1, 13)
Execute(Completion should be done after colons in Rust):
AssertEqual '::', ale#completion#GetPrefix(&filetype, 2, 19)
Execute(Completion should be done after words in parens in Rust):
AssertEqual 'ab', ale#completion#GetPrefix(&filetype, 3, 14)
Execute(Completion should not be done after parens in Rust):
AssertEqual '', ale#completion#GetPrefix(&filetype, 3, 15)

View File

@ -2,12 +2,9 @@ Before:
Save g:ale_completion_delay Save g:ale_completion_delay
Save g:ale_completion_max_suggestions Save g:ale_completion_max_suggestions
Save g:ale_completion_info Save g:ale_completion_info
Save g:ale_completion_experimental_lsp_support
Save &l:omnifunc Save &l:omnifunc
Save &l:completeopt Save &l:completeopt
unlet! g:ale_completion_experimental_lsp_support
let g:ale_completion_enabled = 1 let g:ale_completion_enabled = 1
call ale#test#SetDirectory('/testplugin/test/completion') call ale#test#SetDirectory('/testplugin/test/completion')
@ -44,7 +41,6 @@ After:
unlet! b:ale_completion_parser unlet! b:ale_completion_parser
unlet! b:ale_complete_done_time unlet! b:ale_complete_done_time
unlet! b:ale_linters unlet! b:ale_linters
unlet! g:ale_completion_experimental_lsp_support
call ale#test#RestoreDirectory() call ale#test#RestoreDirectory()
call ale#linter#Reset() call ale#linter#Reset()
@ -136,8 +132,6 @@ Given python(Some Python file):
bazxyzxyzxyz bazxyzxyzxyz
Execute(The right message should be sent for the initial LSP request): Execute(The right message should be sent for the initial LSP request):
let g:ale_completion_experimental_lsp_support = 1
runtime ale_linters/python/pyls.vim runtime ale_linters/python/pyls.vim
let b:ale_linters = ['pyls'] let b:ale_linters = ['pyls']
" The cursor position needs to match what was saved before. " The cursor position needs to match what was saved before.

View File

@ -0,0 +1,392 @@
After:
unlet! b:ale_completion_info
Execute(Should handle Rust completion results correctly):
AssertEqual
\ [
\ {'word': 'new', 'menu': 'pub fn new() -> String', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'with_capacity', 'menu': 'pub fn with_capacity(capacity: usize) -> String', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'from_utf8', 'menu': 'pub fn from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error>', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'from_utf8_lossy', 'menu': 'pub fn from_utf8_lossy<''a>(v: &''a [u8]) -> Cow<''a, str>', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'from_utf16', 'menu': 'pub fn from_utf16(v: &[u16]) -> Result<String, FromUtf16Error>', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'from_utf16_lossy', 'menu': 'pub fn from_utf16_lossy(v: &[u16]) -> String', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'from_raw_parts', 'menu': 'pub unsafe fn from_raw_parts(buf: *mut u8, length: usize, capacity: usize) -> String', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'from_utf8_unchecked', 'menu': 'pub unsafe fn from_utf8_unchecked(bytes: Vec<u8>) -> String', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'from_iter', 'menu': 'fn from_iter<I: IntoIterator<Item = char>>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'from_iter', 'menu': 'fn from_iter<I: IntoIterator<Item = &''a char>>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'from_iter', 'menu': 'fn from_iter<I: IntoIterator<Item = &''a str>>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'from_iter', 'menu': 'fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'from_iter', 'menu': 'fn from_iter<I: IntoIterator<Item = Cow<''a, str>>>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'Searcher', 'menu': 'type Searcher = <&''b str as Pattern<''a>>::Searcher;', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'default', 'menu': 'fn default() -> String', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'Output', 'menu': 'type Output = String;', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'Target', 'menu': 'type Target = str;', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'Err', 'menu': 'type Err = ParseError;', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'from_str', 'menu': 'fn from_str(s: &str) -> Result<String, ParseError>', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'from', 'menu': 'fn from(s: &''a str) -> String', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'from', 'menu': 'fn from(s: Box<str>) -> String', 'info': '', 'kind': 'f', 'icase': 1},
\ {'word': 'from', 'menu': 'fn from(s: Cow<''a, str>) -> String', 'info': '', 'kind': 'f', 'icase': 1},
\],
\ ale#completion#ParseLSPCompletions({
\ "jsonrpc":"2.0",
\ "id":65,
\ "result":[
\ {
\ "label":"new",
\ "kind":3,
\ "detail":"pub fn new() -> String"
\ },
\ {
\ "label":"with_capacity",
\ "kind":3,
\ "detail":"pub fn with_capacity(capacity: usize) -> String"
\ },
\ {
\ "label":"from_utf8",
\ "kind":3,
\ "detail":"pub fn from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error>"
\ },
\ {
\ "label":"from_utf8_lossy",
\ "kind":3,
\ "detail":"pub fn from_utf8_lossy<'a>(v: &'a [u8]) -> Cow<'a, str>"
\ },
\ {
\ "label":"from_utf16",
\ "kind":3,
\ "detail":"pub fn from_utf16(v: &[u16]) -> Result<String, FromUtf16Error>"
\ },
\ {
\ "label":"from_utf16_lossy",
\ "kind":3,
\ "detail":"pub fn from_utf16_lossy(v: &[u16]) -> String"
\ },
\ {
\ "label":"from_raw_parts",
\ "kind":3,
\ "detail":"pub unsafe fn from_raw_parts(buf: *mut u8, length: usize, capacity: usize) -> String"
\ },
\ {
\ "label":"from_utf8_unchecked",
\ "kind":3,
\ "detail":"pub unsafe fn from_utf8_unchecked(bytes: Vec<u8>) -> String"
\ },
\ {
\ "label":"from_iter",
\ "kind":3,
\ "detail":"fn from_iter<I: IntoIterator<Item = char>>(iter: I) -> String"
\ },
\ {
\ "label":"from_iter",
\ "kind":3,
\ "detail":"fn from_iter<I: IntoIterator<Item = &'a char>>(iter: I) -> String"
\ },
\ {
\ "label":"from_iter",
\ "kind":3,
\ "detail":"fn from_iter<I: IntoIterator<Item = &'a str>>(iter: I) -> String"
\ },
\ {
\ "label":"from_iter",
\ "kind":3,
\ "detail":"fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> String"
\ },
\ {
\ "label":"from_iter",
\ "kind":3,
\ "detail":"fn from_iter<I: IntoIterator<Item = Cow<'a, str>>>(iter: I) -> String"
\ },
\ {
\ "label":"Searcher",
\ "kind":8,
\ "detail":"type Searcher = <&'b str as Pattern<'a>>::Searcher;"
\ },
\ {
\ "label":"default",
\ "kind":3,
\ "detail":"fn default() -> String"
\ },
\ {
\ "label":"Output",
\ "kind":8,
\ "detail":"type Output = String;"
\ },
\ {
\ "label":"Output",
\ "kind":8,
\ "detail":"type Output = str;"
\ },
\ {
\ "label":"Output",
\ "kind":8,
\ "detail":"type Output = str;"
\ },
\ {
\ "label":"Output",
\ "kind":8,
\ "detail":"type Output = str;"
\ },
\ {
\ "label":"Output",
\ "kind":8,
\ "detail":"type Output = str;"
\ },
\ {
\ "label":"Output",
\ "kind":8,
\ "detail":"type Output = str;"
\ },
\ {
\ "label":"Output",
\ "kind":8,
\ "detail":"type Output = str;"
\ },
\ {
\ "label":"Target",
\ "kind":8,
\ "detail":"type Target = str;"
\ },
\ {
\ "label":"Err",
\ "kind":8,
\ "detail":"type Err = ParseError;"
\ },
\ {
\ "label":"from_str",
\ "kind":3,
\ "detail":"fn from_str(s: &str) -> Result<String, ParseError>"
\ },
\ {
\ "label":"from",
\ "kind":3,
\ "detail":"fn from(s: &'a str) -> String"
\ },
\ {
\ "label":"from",
\ "kind":3,
\ "detail":"fn from(s: Box<str>) -> String"
\ },
\ {
\ "label":"from",
\ "kind":3,
\ "detail":"fn from(s: Cow<'a, str>) -> String"
\ }
\ ]
\ })
Execute(Should handle Python completion results correctly):
let b:ale_completion_info = {
\ 'completion_filter': 'ale#completion#python#CompletionItemFilter',
\}
AssertEqual
\ [
\ {'word': 'what', 'menu': 'example-python-project.bar.Bar', 'info': "what()\n\n", 'kind': 'f', 'icase': 1},
\ ],
\ ale#completion#ParseLSPCompletions({
\ "jsonrpc":"2.0",
\ "id":6,
\ "result":{
\ "isIncomplete":v:false,
\ "items":[
\ {
\ "label":"what()",
\ "kind":3,
\ "detail":"example-python-project.bar.Bar",
\ "documentation":"what()\n\n",
\ "sortText":"awhat",
\ "insertText":"what"
\ },
\ {
\ "label":"__class__",
\ "kind":7,
\ "detail":"object",
\ "documentation":"type(object_or_name, bases, dict)\ntype(object) -> the object's type\ntype(name, bases, dict) -> a new type",
\ "sortText":"z__class__",
\ "insertText":"__class__"
\ },
\ {
\ "label":"__delattr__(name)",
\ "kind":3,
\ "detail":"object",
\ "documentation":"Implement delattr(self, name).",
\ "sortText":"z__delattr__",
\ "insertText":"__delattr__"
\ },
\ {
\ "label":"__dir__()",
\ "kind":3,
\ "detail":"object",
\ "documentation":"__dir__() -> list\ndefault dir() implementation",
\ "sortText":"z__dir__",
\ "insertText":"__dir__"
\ },
\ {
\ "label":"__doc__",
\ "kind":18,
\ "detail":"object",
\ "documentation":"str(object='') -> str\nstr(bytes_or_buffer[, encoding[, errors]]) -> str\n\nCreate a new string object from the given object. If encoding or\nerrors is specified, then the object must expose a data buffer\nthat will be decoded using the given encoding and error handler.\nOtherwise, returns the result of object.__str__() (if defined)\nor repr(object).\nencoding defaults to sys.getdefaultencoding().\nerrors defaults to 'strict'.",
\ "sortText":"z__doc__",
\ "insertText":"__doc__"
\ },
\ {
\ "label":"__eq__(value)",
\ "kind":3,
\ "detail":"object",
\ "documentation":"Return self==value.",
\ "sortText":"z__eq__",
\ "insertText":"__eq__"
\ },
\ {
\ "label":"__format__()",
\ "kind":3,
\ "detail":"object",
\ "documentation":"default object formatter",
\ "sortText":"z__format__",
\ "insertText":"__format__"
\ },
\ {
\ "label":"__ge__(value)",
\ "kind":3,
\ "detail":"object",
\ "documentation":"Return self>=value.",
\ "sortText":"z__ge__",
\ "insertText":"__ge__"
\ },
\ {
\ "label":"__getattribute__(name)",
\ "kind":3,
\ "detail":"object",
\ "documentation":"Return getattr(self, name).",
\ "sortText":"z__getattribute__",
\ "insertText":"__getattribute__"
\ },
\ {
\ "label":"__gt__(value)",
\ "kind":3,
\ "detail":"object",
\ "documentation":"Return self>value.",
\ "sortText":"z__gt__",
\ "insertText":"__gt__"
\ },
\ {
\ "label":"__hash__()",
\ "kind":3,
\ "detail":"object",
\ "documentation":"Return hash(self).",
\ "sortText":"z__hash__",
\ "insertText":"__hash__"
\ },
\ {
\ "label":"__init__(args, kwargs)",
\ "kind":3,
\ "detail":"object",
\ "documentation":"Initialize self.\u00a0\u00a0See help(type(self)) for accurate signature.",
\ "sortText":"z__init__",
\ "insertText":"__init__"
\ },
\ {
\ "label":"__init_subclass__()",
\ "kind":3,
\ "detail":"object",
\ "documentation":"This method is called when a class is subclassed.\n\nThe default implementation does nothing. It may be\noverridden to extend subclasses.",
\ "sortText":"z__init_subclass__",
\ "insertText":"__init_subclass__"
\ },
\ {
\ "label":"__le__(value)",
\ "kind":3,
\ "detail":"object",
\ "documentation":"Return self<=value.",
\ "sortText":"z__le__",
\ "insertText":"__le__"
\ },
\ {
\ "label":"__lt__(value)",
\ "kind":3,
\ "detail":"object",
\ "documentation":"Return self<value.",
\ "sortText":"z__lt__",
\ "insertText":"__lt__"
\ },
\ {
\ "label":"__ne__(value)",
\ "kind":3,
\ "detail":"object",
\ "documentation":"Return self!=value.",
\ "sortText":"z__ne__",
\ "insertText":"__ne__"
\ },
\ {
\ "label":"__new__(kwargs)",
\ "kind":3,
\ "detail":"object",
\ "documentation":"Create and return a new object.\u00a0\u00a0See help(type) for accurate signature.",
\ "sortText":"z__new__",
\ "insertText":"__new__"
\ },
\ {
\ "label":"__reduce__()",
\ "kind":3,
\ "detail":"object",
\ "documentation":"helper for pickle",
\ "sortText":"z__reduce__",
\ "insertText":"__reduce__"
\ },
\ {
\ "label":"__reduce_ex__()",
\ "kind":3,
\ "detail":"object",
\ "documentation":"helper for pickle",
\ "sortText":"z__reduce_ex__",
\ "insertText":"__reduce_ex__"
\ },
\ {
\ "label":"__repr__()",
\ "kind":3,
\ "detail":"object",
\ "documentation":"Return repr(self).",
\ "sortText":"z__repr__",
\ "insertText":"__repr__"
\ },
\ {
\ "label":"__setattr__(name, value)",
\ "kind":3,
\ "detail":"object",
\ "documentation":"Implement setattr(self, name, value).",
\ "sortText":"z__setattr__",
\ "insertText":"__setattr__"
\ },
\ {
\ "label":"__sizeof__()",
\ "kind":3,
\ "detail":"object",
\ "documentation":"__sizeof__() -> int\nsize of object in memory, in bytes",
\ "sortText":"z__sizeof__",
\ "insertText":"__sizeof__"
\ },
\ {
\ "label":"__str__()",
\ "kind":3,
\ "detail":"object",
\ "documentation":"Return str(self).",
\ "sortText":"z__str__",
\ "insertText":"__str__"
\ },
\ {
\ "label":"__subclasshook__()",
\ "kind":3,
\ "detail":"object",
\ "documentation":"Abstract classes can override this to customize issubclass().\n\nThis is invoked early on by abc.ABCMeta.__subclasscheck__().\nIt should return True, False or NotImplemented.\u00a0\u00a0If it returns\nNotImplemented, the normal algorithm is used.\u00a0\u00a0Otherwise, it\noverrides the normal algorithm (and the outcome is cached).",
\ "sortText":"z__subclasshook__",
\ "insertText":"__subclasshook__"
\ }
\ ]
\ }
\ })