mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-15 11:53:49 +07:00
406 lines
13 KiB
Python
406 lines
13 KiB
Python
'''
|
|
Entry-point module to start the code-completion server for PyDev.
|
|
|
|
@author Fabio Zadrozny
|
|
'''
|
|
import sys
|
|
IS_PYTHON_3_ONWARDS = sys.version_info[0] >= 3
|
|
|
|
if not IS_PYTHON_3_ONWARDS:
|
|
import __builtin__
|
|
else:
|
|
import builtins as __builtin__ # Python 3.0
|
|
|
|
from _pydevd_bundle.pydevd_constants import IS_JYTHON
|
|
|
|
if IS_JYTHON:
|
|
import java.lang # @UnresolvedImport
|
|
SERVER_NAME = 'jycompletionserver'
|
|
from _pydev_bundle import _pydev_jy_imports_tipper
|
|
_pydev_imports_tipper = _pydev_jy_imports_tipper
|
|
|
|
else:
|
|
# it is python
|
|
SERVER_NAME = 'pycompletionserver'
|
|
from _pydev_bundle import _pydev_imports_tipper
|
|
|
|
|
|
from _pydev_imps._pydev_saved_modules import socket
|
|
|
|
import sys
|
|
if sys.platform == "darwin":
|
|
# See: https://sourceforge.net/projects/pydev/forums/forum/293649/topic/3454227
|
|
try:
|
|
import _CF # Don't fail if it doesn't work -- do it because it must be loaded on the main thread! @UnresolvedImport @UnusedImport
|
|
except:
|
|
pass
|
|
|
|
|
|
# initial sys.path
|
|
_sys_path = []
|
|
for p in sys.path:
|
|
# changed to be compatible with 1.5
|
|
_sys_path.append(p)
|
|
|
|
# initial sys.modules
|
|
_sys_modules = {}
|
|
for name, mod in sys.modules.items():
|
|
_sys_modules[name] = mod
|
|
|
|
|
|
import traceback
|
|
|
|
from _pydev_imps._pydev_saved_modules import time
|
|
|
|
try:
|
|
import StringIO
|
|
except:
|
|
import io as StringIO #Python 3.0
|
|
|
|
try:
|
|
from urllib import quote_plus, unquote_plus
|
|
except ImportError:
|
|
from urllib.parse import quote_plus, unquote_plus #Python 3.0
|
|
|
|
INFO1 = 1
|
|
INFO2 = 2
|
|
WARN = 4
|
|
ERROR = 8
|
|
|
|
DEBUG = INFO1 | ERROR
|
|
|
|
def dbg(s, prior):
|
|
if prior & DEBUG != 0:
|
|
sys.stdout.write('%s\n' % (s,))
|
|
# f = open('c:/temp/test.txt', 'a')
|
|
# print_ >> f, s
|
|
# f.close()
|
|
|
|
from _pydev_bundle import pydev_localhost
|
|
HOST = pydev_localhost.get_localhost() # Symbolic name meaning the local host
|
|
|
|
MSG_KILL_SERVER = '@@KILL_SERVER_END@@'
|
|
MSG_COMPLETIONS = '@@COMPLETIONS'
|
|
MSG_END = 'END@@'
|
|
MSG_INVALID_REQUEST = '@@INVALID_REQUEST'
|
|
MSG_JYTHON_INVALID_REQUEST = '@@JYTHON_INVALID_REQUEST'
|
|
MSG_CHANGE_DIR = '@@CHANGE_DIR:'
|
|
MSG_OK = '@@MSG_OK_END@@'
|
|
MSG_IMPORTS = '@@IMPORTS:'
|
|
MSG_PYTHONPATH = '@@PYTHONPATH_END@@'
|
|
MSG_CHANGE_PYTHONPATH = '@@CHANGE_PYTHONPATH:'
|
|
MSG_JEDI = '@@MSG_JEDI:'
|
|
MSG_SEARCH = '@@SEARCH'
|
|
|
|
BUFFER_SIZE = 1024
|
|
|
|
|
|
|
|
currDirModule = None
|
|
|
|
def complete_from_dir(directory):
|
|
'''
|
|
This is necessary so that we get the imports from the same directory where the file
|
|
we are completing is located.
|
|
'''
|
|
global currDirModule
|
|
if currDirModule is not None:
|
|
if len(sys.path) > 0 and sys.path[0] == currDirModule:
|
|
del sys.path[0]
|
|
|
|
currDirModule = directory
|
|
sys.path.insert(0, directory)
|
|
|
|
|
|
def change_python_path(pythonpath):
|
|
'''Changes the pythonpath (clears all the previous pythonpath)
|
|
|
|
@param pythonpath: string with paths separated by |
|
|
'''
|
|
|
|
split = pythonpath.split('|')
|
|
sys.path = []
|
|
for path in split:
|
|
path = path.strip()
|
|
if len(path) > 0:
|
|
sys.path.append(path)
|
|
|
|
|
|
class Processor:
|
|
|
|
def __init__(self):
|
|
# nothing to do
|
|
return
|
|
|
|
def remove_invalid_chars(self, msg):
|
|
try:
|
|
msg = str(msg)
|
|
except UnicodeDecodeError:
|
|
pass
|
|
|
|
if msg:
|
|
try:
|
|
return quote_plus(msg)
|
|
except:
|
|
sys.stdout.write('error making quote plus in %s\n' % (msg,))
|
|
raise
|
|
return ' '
|
|
|
|
def format_completion_message(self, defFile, completionsList):
|
|
'''
|
|
Format the completions suggestions in the following format:
|
|
@@COMPLETIONS(modFile(token,description),(token,description),(token,description))END@@
|
|
'''
|
|
compMsg = []
|
|
compMsg.append('%s' % defFile)
|
|
for tup in completionsList:
|
|
compMsg.append(',')
|
|
|
|
compMsg.append('(')
|
|
compMsg.append(str(self.remove_invalid_chars(tup[0]))) # token
|
|
compMsg.append(',')
|
|
compMsg.append(self.remove_invalid_chars(tup[1])) # description
|
|
|
|
if(len(tup) > 2):
|
|
compMsg.append(',')
|
|
compMsg.append(self.remove_invalid_chars(tup[2])) # args - only if function.
|
|
|
|
if(len(tup) > 3):
|
|
compMsg.append(',')
|
|
compMsg.append(self.remove_invalid_chars(tup[3])) # TYPE
|
|
|
|
compMsg.append(')')
|
|
|
|
return '%s(%s)%s' % (MSG_COMPLETIONS, ''.join(compMsg), MSG_END)
|
|
|
|
class Exit(Exception):
|
|
pass
|
|
|
|
class CompletionServer:
|
|
|
|
def __init__(self, port):
|
|
self.ended = False
|
|
self.port = port
|
|
self.socket = None # socket to send messages.
|
|
self.exit_process_on_kill = True
|
|
self.processor = Processor()
|
|
|
|
|
|
def connect_to_server(self):
|
|
from _pydev_imps._pydev_saved_modules import socket
|
|
|
|
self.socket = s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
try:
|
|
s.connect((HOST, self.port))
|
|
except:
|
|
sys.stderr.write('Error on connect_to_server with parameters: host: %s port: %s\n' % (HOST, self.port))
|
|
raise
|
|
|
|
def get_completions_message(self, defFile, completionsList):
|
|
'''
|
|
get message with completions.
|
|
'''
|
|
return self.processor.format_completion_message(defFile, completionsList)
|
|
|
|
def get_token_and_data(self, data):
|
|
'''
|
|
When we receive this, we have 'token):data'
|
|
'''
|
|
token = ''
|
|
for c in data:
|
|
if c != ')':
|
|
token = token + c
|
|
else:
|
|
break;
|
|
|
|
return token, data.lstrip(token + '):')
|
|
|
|
def emulated_sendall(self, msg):
|
|
MSGLEN = 1024 * 20
|
|
|
|
totalsent = 0
|
|
while totalsent < MSGLEN:
|
|
sent = self.socket.send(msg[totalsent:])
|
|
if sent == 0:
|
|
return
|
|
totalsent = totalsent + sent
|
|
|
|
|
|
def send(self, msg):
|
|
if not hasattr(self.socket, 'sendall'):
|
|
#Older versions (jython 2.1)
|
|
self.emulated_sendall(msg)
|
|
else:
|
|
if IS_PYTHON_3_ONWARDS:
|
|
self.socket.sendall(bytearray(msg, 'utf-8'))
|
|
else:
|
|
self.socket.sendall(msg)
|
|
|
|
|
|
def run(self):
|
|
# Echo server program
|
|
try:
|
|
from _pydev_bundle import _pydev_log
|
|
log = _pydev_log.Log()
|
|
|
|
dbg(SERVER_NAME + ' connecting to java server on %s (%s)' % (HOST, self.port) , INFO1)
|
|
# after being connected, create a socket as a client.
|
|
self.connect_to_server()
|
|
|
|
dbg(SERVER_NAME + ' Connected to java server', INFO1)
|
|
|
|
|
|
while not self.ended:
|
|
data = ''
|
|
|
|
while data.find(MSG_END) == -1:
|
|
received = self.socket.recv(BUFFER_SIZE)
|
|
if len(received) == 0:
|
|
raise Exit() # ok, connection ended
|
|
if IS_PYTHON_3_ONWARDS:
|
|
data = data + received.decode('utf-8')
|
|
else:
|
|
data = data + received
|
|
|
|
try:
|
|
try:
|
|
if data.find(MSG_KILL_SERVER) != -1:
|
|
dbg(SERVER_NAME + ' kill message received', INFO1)
|
|
# break if we received kill message.
|
|
self.ended = True
|
|
raise Exit()
|
|
|
|
dbg(SERVER_NAME + ' starting keep alive thread', INFO2)
|
|
|
|
if data.find(MSG_PYTHONPATH) != -1:
|
|
comps = []
|
|
for p in _sys_path:
|
|
comps.append((p, ' '))
|
|
self.send(self.get_completions_message(None, comps))
|
|
|
|
else:
|
|
data = data[:data.rfind(MSG_END)]
|
|
|
|
if data.startswith(MSG_IMPORTS):
|
|
data = data[len(MSG_IMPORTS):]
|
|
data = unquote_plus(data)
|
|
defFile, comps = _pydev_imports_tipper.generate_tip(data, log)
|
|
self.send(self.get_completions_message(defFile, comps))
|
|
|
|
elif data.startswith(MSG_CHANGE_PYTHONPATH):
|
|
data = data[len(MSG_CHANGE_PYTHONPATH):]
|
|
data = unquote_plus(data)
|
|
change_python_path(data)
|
|
self.send(MSG_OK)
|
|
|
|
elif data.startswith(MSG_JEDI):
|
|
data = data[len(MSG_JEDI):]
|
|
data = unquote_plus(data)
|
|
line, column, encoding, path, source = data.split('|', 4)
|
|
try:
|
|
import jedi # @UnresolvedImport
|
|
except:
|
|
self.send(self.get_completions_message(None, [('Error on import jedi', 'Error importing jedi', '')]))
|
|
else:
|
|
script = jedi.Script(
|
|
# Line +1 because it expects lines 1-based (and col 0-based)
|
|
source=source,
|
|
line=int(line) + 1,
|
|
column=int(column),
|
|
source_encoding=encoding,
|
|
path=path,
|
|
)
|
|
lst = []
|
|
for completion in script.completions():
|
|
t = completion.type
|
|
if t == 'class':
|
|
t = '1'
|
|
|
|
elif t == 'function':
|
|
t = '2'
|
|
|
|
elif t == 'import':
|
|
t = '0'
|
|
|
|
elif t == 'keyword':
|
|
continue # Keywords are already handled in PyDev
|
|
|
|
elif t == 'statement':
|
|
t = '3'
|
|
|
|
else:
|
|
t = '-1'
|
|
|
|
# gen list(tuple(name, doc, args, type))
|
|
lst.append((completion.name, '', '', t))
|
|
self.send(self.get_completions_message('empty', lst))
|
|
|
|
elif data.startswith(MSG_SEARCH):
|
|
data = data[len(MSG_SEARCH):]
|
|
data = unquote_plus(data)
|
|
(f, line, col), foundAs = _pydev_imports_tipper.search_definition(data)
|
|
self.send(self.get_completions_message(f, [(line, col, foundAs)]))
|
|
|
|
elif data.startswith(MSG_CHANGE_DIR):
|
|
data = data[len(MSG_CHANGE_DIR):]
|
|
data = unquote_plus(data)
|
|
complete_from_dir(data)
|
|
self.send(MSG_OK)
|
|
|
|
else:
|
|
self.send(MSG_INVALID_REQUEST)
|
|
except Exit:
|
|
e = sys.exc_info()[1]
|
|
msg = self.get_completions_message(None, [('Exit:', 'SystemExit', '')])
|
|
try:
|
|
self.send(msg)
|
|
except socket.error:
|
|
pass # Ok, may be closed already
|
|
|
|
raise e # raise original error.
|
|
|
|
except:
|
|
dbg(SERVER_NAME + ' exception occurred', ERROR)
|
|
s = StringIO.StringIO()
|
|
traceback.print_exc(file=s)
|
|
|
|
err = s.getvalue()
|
|
dbg(SERVER_NAME + ' received error: ' + str(err), ERROR)
|
|
msg = self.get_completions_message(None, [('ERROR:', '%s\nLog:%s' % (err, log.get_contents()), '')])
|
|
try:
|
|
self.send(msg)
|
|
except socket.error:
|
|
pass # Ok, may be closed already
|
|
|
|
|
|
finally:
|
|
log.clear_log()
|
|
|
|
self.socket.close()
|
|
self.ended = True
|
|
raise Exit() # connection broken
|
|
|
|
|
|
except Exit:
|
|
if self.exit_process_on_kill:
|
|
sys.exit(0)
|
|
# No need to log SystemExit error
|
|
except:
|
|
s = StringIO.StringIO()
|
|
exc_info = sys.exc_info()
|
|
|
|
traceback.print_exception(exc_info[0], exc_info[1], exc_info[2], limit=None, file=s)
|
|
err = s.getvalue()
|
|
dbg(SERVER_NAME + ' received error: ' + str(err), ERROR)
|
|
raise
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
port = int(sys.argv[1]) # this is from where we want to receive messages.
|
|
|
|
t = CompletionServer(port)
|
|
dbg(SERVER_NAME + ' will start', INFO1)
|
|
t.run()
|