-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
PR: Migrate introspection services to use the Language Server Protocol (LSP) #4751
Changes from 1 commit
4ee91c9
4d0c22e
6e2139f
db63b91
c399889
8b0f176
536fef9
ea464c6
f7fcdf4
e25d285
49ec0f6
9afeb75
b1ec308
2184681
96db142
2e583dd
20f65d6
388cdd2
9a54c69
09637f5
ae707a0
7959110
a46774e
20d8ee1
a98b8ce
78d4276
23e0ead
387bf35
9e16e7a
884d95e
280f725
bc384e7
2db33c3
94fd388
48d5536
b0e6979
d7c0f21
4b95a4b
acfed93
7ba5ff1
f177bda
e2beec1
1e125d5
54187c4
11c5d5a
8a574aa
6909f5b
a5fd36a
2191f6c
20183a8
63b0888
d627e21
45b607f
330ba83
e5d9d92
dda4660
3afde13
b3c7bbd
1db53b2
ccc39d8
db0c317
12db7db
bf4fcbe
a1c960b
a0398c4
4ae4ee0
85253fb
354392e
b9f9e29
7330308
7c4bcb0
1e37637
dc07070
02062d0
a029c2f
a9dadde
8490670
b750784
72778e0
46d9fed
69711a8
e21f0d3
79bd920
8ae9fac
3940e54
d9d07ee
c2f777f
9ab3c25
c499108
229f30a
9ee5ee9
cc719ad
3cb2853
5fdd128
3e00469
a6a5cae
b5969ff
3c89543
f3aa040
16cee99
88fcb65
58b3340
cc0ce6f
02d90a5
3b1203d
329a03c
47ecb9d
15cd3f5
ad88cf0
7c70fd6
5c529be
c66aa54
63085cc
5d30978
2053cc9
e459fa2
70b6c7f
bc781cb
9b0b29f
c78087d
a1222e7
7e6618c
63c628f
41b5563
40a3016
ac66380
0471faa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
# Copyright © Spyder Project Contributors | ||
# Licensed under the terms of the MIT License | ||
# (see spyder/__init__.py for details) | ||
|
||
|
||
"""Code introspection and linting utillites.""" | ||
|
||
from spyder.config.base import DEV | ||
|
||
|
||
# Language server communication verbosity at server logs. | ||
TRACE = 'messages' | ||
if DEV: | ||
TRACE = 'verbose' | ||
|
||
|
||
# Spyder editor capabilities | ||
EDITOR_CAPABILITES = { | ||
"workspace": { | ||
"applyEdit": True, | ||
"workspaceEdit": { | ||
"documentChanges": False | ||
}, | ||
"didChangeConfiguration": { | ||
"dynamicRegistration": True | ||
}, | ||
"didChangeWatchedFiles": { | ||
"dynamicRegistration": True | ||
}, | ||
"symbol": { | ||
"dynamicRegistration": True | ||
}, | ||
"executeCommand": { | ||
"dynamicRegistration": True | ||
} | ||
}, | ||
"textDocument": { | ||
"synchronization": { | ||
"dynamicRegistration": True, | ||
"willSave": True, | ||
"willSaveWaitUntil": True, | ||
"didSave": True | ||
}, | ||
"completion": { | ||
"dynamicRegistration": True, | ||
"completionItem": { | ||
"snippetSupport": True | ||
} | ||
}, | ||
"hover": { | ||
"dynamicRegistration": True | ||
}, | ||
"signatureHelp": { | ||
"dynamicRegistration": True | ||
}, | ||
"references": { | ||
"dynamicRegistration": True | ||
}, | ||
"documentHighlight": { | ||
"dynamicRegistration": True | ||
}, | ||
"documentSymbol": { | ||
"dynamicRegistration": True | ||
}, | ||
"formatting": { | ||
"dynamicRegistration": True | ||
}, | ||
"rangeFormatting": { | ||
"dynamicRegistration": True | ||
}, | ||
"onTypeFormatting": { | ||
"dynamicRegistration": True | ||
}, | ||
"definition": { | ||
"dynamicRegistration": True | ||
}, | ||
"codeAction": { | ||
"dynamicRegistration": True | ||
}, | ||
"codeLens": { | ||
"dynamicRegistration": True | ||
}, | ||
"documentLink": { | ||
"dynamicRegistration": True | ||
}, | ||
"rename": { | ||
"dynamicRegistration": True | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
"""Spyder MS Language Server v3.0 client implementation.""" | ||
|
||
import os | ||
import sys | ||
import zmq | ||
import json | ||
import time | ||
import socket | ||
import logging | ||
import argparse | ||
import subprocess | ||
import coloredlogs | ||
import os.path as osp | ||
from threading import Thread, Lock | ||
from spyder.py3compat import PY2, getcwd | ||
from spyder.utils.code_analysis import EDITOR_CAPABILITES, TRACE | ||
|
||
if PY2: | ||
import pathlib2 as pathlib | ||
else: | ||
import pathlib | ||
|
||
|
||
TIMEOUT = 5000 | ||
PID = os.getpid() | ||
|
||
|
||
parser = argparse.ArgumentParser( | ||
description='ZMQ Python-based MS Language-Server v3.0 client for Spyder') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So do you plan to use zmq sockets? Why not regular sockets? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well I saw that the current implementation is based on ZMQ, but we could use also dedicated sockets per each editor connection, what do you prefer @ccordoba12 @goanpeca? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shall we continue using zmq? @ccordoba12 @goanpeca There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have no opinion, if using zmq makes the core easier to read and write, then why not? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, please do. I think Zmq sockets are more robust than plain sockets. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ZMQ will be then! |
||
|
||
parser.add_argument('--zmq-port', | ||
default=7000, | ||
help="ZMQ port to be contacted") | ||
parser.add_argument('--server-host', | ||
default='127.0.0.1', | ||
help='Host that serves the ls-server') | ||
parser.add_argument('--server-port', | ||
default=2087, | ||
help="Deployment port of the ls-server") | ||
parser.add_argument('--folder', | ||
default=getcwd(), | ||
help="Initial current working directory used to " | ||
"initialize ls-server") | ||
parser.add_argument('--server', | ||
default='pyls', | ||
help='Instruction executed to start the language server') | ||
parser.add_argument('--external-server', | ||
action="store_true", | ||
help="Do not start a local server") | ||
|
||
|
||
LOG_FORMAT = ('%(levelname) -10s %(asctime)s %(name) -30s %(funcName) ' | ||
'-35s %(lineno) -5d: %(message)s') | ||
|
||
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT) | ||
logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT) | ||
logging.basicConfig(level=logging.ERROR, format=LOG_FORMAT) | ||
|
||
LOGGER = logging.getLogger(__name__) | ||
# LOGGER.setLevel(logging.DEBUG) | ||
coloredlogs.install(level='debug') | ||
|
||
|
||
class IncomingMessageThread(Thread): | ||
def __init__(self): | ||
Thread.__init__(self) | ||
self.stopped = False | ||
self.mutex = Lock() | ||
|
||
def initialize(self, sock, zmq_sock): | ||
self.socket = sock | ||
self.zmq_sock = zmq_sock | ||
|
||
def run(self): | ||
while True: | ||
with self.mutex: | ||
if self.stopped: | ||
break | ||
try: | ||
recv = self.socket.recv(4096) | ||
LOGGER.debug(recv) | ||
self.zmq_sock.send_pyobj(recv) | ||
except socket.error: | ||
pass | ||
|
||
def stop(self): | ||
with self.mutex: | ||
self.stopped = True | ||
|
||
|
||
class LanguageServerClient: | ||
"""Implementation of a v3.0 compilant language server client.""" | ||
CONTENT_LENGTH = 'Content-Length: {0}\r\n\r\n' | ||
|
||
def __init__(self, host='127.0.0.1', port=2087, workspace=getcwd(), | ||
use_external_server=False, zmq_port=7000, | ||
server='pyls', server_args=['--tcp']): | ||
self.host = host | ||
self.port = port | ||
self.workspace = pathlib.Path(osp.abspath(workspace)).as_uri() | ||
self.request_seq = 1 | ||
|
||
self.server = None | ||
self.is_local_server_running = not use_external_server | ||
if not use_external_server: | ||
LOGGER.info('Starting server: {0} {1} on {2}:{3}'.format( | ||
server, ' '.join(server_args), self.host, self.port)) | ||
exec_line = [server, '--host', str(self.host), '--port', | ||
str(self.port)] + server_args | ||
LOGGER.info(' '.join(exec_line)) | ||
|
||
self.server = subprocess.Popen( | ||
exec_line, | ||
stdout=subprocess.PIPE, | ||
stderr=subprocess.PIPE) | ||
|
||
LOGGER.info('Waiting server to start...') | ||
time.sleep(2) | ||
|
||
LOGGER.info('Connecting to language server at {0}:{1}'.format( | ||
self.host, self.port)) | ||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
self.socket.connect((self.host, int(self.port))) | ||
self.socket.setblocking(False) | ||
|
||
LOGGER.info('Initializing server connection...') | ||
self.__initialize() | ||
|
||
LOGGER.info('Starting ZMQ connection...') | ||
self.context = zmq.Context() | ||
self.zmq_socket = self.context.socket(zmq.PAIR) | ||
self.zmq_socket.connect("tcp://localhost:{0}".format(zmq_port)) | ||
self.zmq_socket.send_pyobj(zmq_port) | ||
|
||
LOGGER.info('Creating consumer Thread...') | ||
self.reading_thread = IncomingMessageThread() | ||
self.reading_thread.initialize(self.socket, self.zmq_socket) | ||
|
||
def __initialize(self): | ||
method = 'initialize' | ||
params = { | ||
'processId': PID, | ||
'rootUri': self.workspace, | ||
'capabilities': EDITOR_CAPABILITES, | ||
'trace': TRACE | ||
} | ||
request = self.__compose_request(method, params) | ||
self.__send_request(request) | ||
|
||
def start(self): | ||
LOGGER.info('Ready to recieve/attend requests and responses!') | ||
self.reading_thread.start() | ||
self.__listen() | ||
|
||
def stop(self): | ||
LOGGER.info('Sending shutdown instruction to server') | ||
self.shutdown() | ||
LOGGER.info('Stopping language server') | ||
self.exit() | ||
if self.is_local_server_running: | ||
LOGGER.info('Closing language server process...') | ||
self.server.terminate() | ||
LOGGER.info('Closing consumer thread...') | ||
self.reading_thread.stop() | ||
|
||
def shutdown(self): | ||
method = 'shutdown' | ||
params = {} | ||
request = self.__compose_request(method, params) | ||
self.__send_request(request) | ||
|
||
def exit(self): | ||
method = 'exit' | ||
params = {} | ||
request = self.__compose_request(method, params) | ||
self.__send_request(request) | ||
|
||
def __listen(self): | ||
while True: | ||
events = self.zmq_socket.poll(TIMEOUT) | ||
requests = [] | ||
while events > 0: | ||
client_request = self.zmq_socket.recv_pyobj() | ||
LOGGER.debug("Client Event: {0}".format(client_request)) | ||
requests.append(client_request) | ||
server_request = self.__compose_request('None', {}) | ||
self.__send_request(server_request) | ||
|
||
def __compose_request(self, method, params): | ||
request = { | ||
"jsonrpc": "2.0", | ||
"id": self.request_seq, | ||
"method": method, | ||
"params": params | ||
} | ||
return request | ||
|
||
def __send_request(self, request): | ||
json_req = json.dumps(request) | ||
content = bytes(json_req.encode('utf-8')) | ||
content_length = len(content) | ||
|
||
LOGGER.debug('Sending request of type: {0}'.format(request['method'])) | ||
LOGGER.debug(json_req) | ||
|
||
self.socket.send(self.CONTENT_LENGTH.format(content_length)) | ||
self.socket.send(content) | ||
self.request_seq += 1 | ||
|
||
|
||
if __name__ == '__main__': | ||
args, unknownargs = parser.parse_known_args() | ||
client = LanguageServerClient(host=args.server_host, | ||
port=args.server_port, | ||
workspace=args.folder, | ||
zmq_port=args.zmq_port, | ||
use_external_server=args.external_server, | ||
server=args.server, | ||
server_args=unknownargs) | ||
try: | ||
client.start() | ||
except KeyboardInterrupt: | ||
client.stop() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the editor supports all the following functionalities?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know what these terms refer to. Could you add a comment above each one to understand what they mean?