149 lines
5.7 KiB
Python
149 lines
5.7 KiB
Python
import logging
|
|
import string
|
|
import logging.config
|
|
|
|
import threading
|
|
import socketserver
|
|
from functools import partial
|
|
|
|
from pyls.python_ls import PythonLanguageServer, _StreamHandlerWrapper, PARENT_PROCESS_WATCH_INTERVAL, flatten, merge
|
|
import pyls.lsp as lsp
|
|
from pyls.workspace import Workspace
|
|
import pyls.uris as uris
|
|
import pyls._utils as _utils
|
|
|
|
from .config import config
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def start_tcp_lang_server(bind_addr, port, check_parent_process, handler_class):
|
|
if not issubclass(handler_class, PythonLanguageServer):
|
|
raise ValueError('Handler class must be an instance of PythonLanguageServer')
|
|
|
|
def shutdown_server(check_parent_process, *args):
|
|
# pylint: disable=unused-argument
|
|
if check_parent_process:
|
|
log.debug('Shutting down server')
|
|
# Shutdown call must be done on a thread, to prevent deadlocks
|
|
stop_thread = threading.Thread(target=server.shutdown)
|
|
stop_thread.start()
|
|
|
|
# Construct a custom wrapper class around the user's handler_class
|
|
wrapper_class = type(
|
|
handler_class.__name__ + 'Handler',
|
|
(_StreamHandlerWrapper,),
|
|
{'DELEGATE_CLASS': partial(handler_class,
|
|
check_parent_process=check_parent_process),
|
|
'SHUTDOWN_CALL': partial(shutdown_server, check_parent_process)}
|
|
)
|
|
|
|
server = socketserver.TCPServer((bind_addr, port), wrapper_class, bind_and_activate=False)
|
|
server.allow_reuse_address = True
|
|
|
|
try:
|
|
server.server_bind()
|
|
server.server_activate()
|
|
log.info('Serving %s on (%s, %s)', handler_class.__name__, bind_addr, port)
|
|
server.serve_forever()
|
|
finally:
|
|
log.info('Shutting down')
|
|
server.server_close()
|
|
|
|
|
|
def start_io_lang_server(rfile, wfile, check_parent_process, handler_class):
|
|
if not issubclass(handler_class, PythonLanguageServer):
|
|
raise ValueError('Handler class must be an instance of PythonLanguageServer')
|
|
log.info('Starting %s IO language server', handler_class.__name__)
|
|
server = handler_class(rfile, wfile, check_parent_process)
|
|
server.start()
|
|
|
|
|
|
class ConfigurationLanguageServer(PythonLanguageServer):
|
|
def __init__(self, rx, tx, check_parent_process):
|
|
PythonLanguageServer.__init__(self, rx, tx, check_parent_process)
|
|
|
|
def m_initialize(self, processId=None, rootUri=None, rootPath=None, initializationOptions=None, **_kwargs):
|
|
log.debug('Language server initialized with %s %s %s %s', processId, rootUri, rootPath, initializationOptions)
|
|
if rootUri is None:
|
|
rootUri = uris.from_fs_path(rootPath) if rootPath is not None else ''
|
|
|
|
self.workspaces.pop(self.root_uri, None)
|
|
self.root_uri = rootUri
|
|
self.config = config.ConfigurationConfig(rootUri, initializationOptions or {},
|
|
processId, _kwargs.get('capabilities', {}))
|
|
self.workspace = Workspace(rootUri, self._endpoint, self.config)
|
|
self.workspaces[rootUri] = self.workspace
|
|
self._dispatchers = self._hook('pyls_dispatchers')
|
|
self._hook('pyls_initialize')
|
|
|
|
if self._check_parent_process and processId is not None and self.watching_thread is None:
|
|
def watch_parent_process(pid):
|
|
# exit when the given pid is not alive
|
|
if not _utils.is_process_alive(pid):
|
|
log.info("parent process %s is not alive, exiting!", pid)
|
|
self.m_exit()
|
|
else:
|
|
threading.Timer(PARENT_PROCESS_WATCH_INTERVAL, watch_parent_process, args=[pid]).start()
|
|
|
|
self.watching_thread = threading.Thread(target=watch_parent_process, args=(processId,))
|
|
self.watching_thread.daemon = True
|
|
self.watching_thread.start()
|
|
# Get our capabilities
|
|
return {'capabilities': self.capabilities()}
|
|
|
|
def completions(self, doc_uri, position):
|
|
completions = self._hook('pyls_completions', doc_uri, position=position)
|
|
return {
|
|
'isIncomplete': False,
|
|
'items': flatten(completions)
|
|
}
|
|
|
|
def capabilities(self):
|
|
server_capabilities = {
|
|
'codeActionProvider': True,
|
|
'codeLensProvider': {
|
|
'resolveProvider': False, # We may need to make this configurable
|
|
},
|
|
'completionProvider': {
|
|
'resolveProvider': False, # We know everything ahead of time
|
|
'triggerCharacters': [l for l in string.ascii_letters]
|
|
},
|
|
'documentFormattingProvider': True,
|
|
'documentHighlightProvider': True,
|
|
'documentRangeFormattingProvider': True,
|
|
'documentSymbolProvider': True,
|
|
'definitionProvider': True,
|
|
'executeCommandProvider': {
|
|
'commands': flatten(self._hook('pyls_commands'))
|
|
},
|
|
'hoverProvider': True,
|
|
'referencesProvider': True,
|
|
'renameProvider': True,
|
|
'foldingRangeProvider': True,
|
|
'signatureHelpProvider': {
|
|
'triggerCharacters': [],
|
|
},
|
|
'hover': {
|
|
"contentFormat": "markdown",
|
|
},
|
|
'textDocumentSync': {
|
|
'change': lsp.TextDocumentSyncKind.INCREMENTAL,
|
|
'save': {
|
|
'includeText': True,
|
|
},
|
|
'openClose': True,
|
|
},
|
|
'workspace': {
|
|
'workspaceFolders': {
|
|
'supported': True,
|
|
'changeNotifications': True
|
|
}
|
|
},
|
|
'experimental': merge(self._hook('pyls_experimental_capabilities'))
|
|
}
|
|
log.info('Server capabilities: %s', server_capabilities)
|
|
return server_capabilities
|
|
|