1''' 2Entry-point module to start the code-completion server for PyDev. 3 4@author Fabio Zadrozny 5''' 6IS_PYTHON3K = 0 7try: 8 import __builtin__ 9except ImportError: 10 import builtins as __builtin__ # Python 3.0 11 IS_PYTHON3K = 1 12 13from _pydevd_bundle.pydevd_constants import IS_JYTHON 14 15if IS_JYTHON: 16 import java.lang # @UnresolvedImport 17 SERVER_NAME = 'jycompletionserver' 18 from _pydev_bundle import _pydev_jy_imports_tipper 19 _pydev_imports_tipper = _pydev_jy_imports_tipper 20 21else: 22 # it is python 23 SERVER_NAME = 'pycompletionserver' 24 from _pydev_bundle import _pydev_imports_tipper 25 26 27from _pydev_imps._pydev_saved_modules import socket 28 29import sys 30if sys.platform == "darwin": 31 # See: https://sourceforge.net/projects/pydev/forums/forum/293649/topic/3454227 32 try: 33 import _CF # Don't fail if it doesn't work -- do it because it must be loaded on the main thread! @UnresolvedImport @UnusedImport 34 except: 35 pass 36 37 38# initial sys.path 39_sys_path = [] 40for p in sys.path: 41 # changed to be compatible with 1.5 42 _sys_path.append(p) 43 44# initial sys.modules 45_sys_modules = {} 46for name, mod in sys.modules.items(): 47 _sys_modules[name] = mod 48 49 50import traceback 51 52from _pydev_imps._pydev_saved_modules import time 53 54try: 55 import StringIO 56except: 57 import io as StringIO #Python 3.0 58 59try: 60 from urllib import quote_plus, unquote_plus 61except ImportError: 62 from urllib.parse import quote_plus, unquote_plus #Python 3.0 63 64INFO1 = 1 65INFO2 = 2 66WARN = 4 67ERROR = 8 68 69DEBUG = INFO1 | ERROR 70 71def dbg(s, prior): 72 if prior & DEBUG != 0: 73 sys.stdout.write('%s\n' % (s,)) 74# f = open('c:/temp/test.txt', 'a') 75# print_ >> f, s 76# f.close() 77 78from _pydev_bundle import pydev_localhost 79HOST = pydev_localhost.get_localhost() # Symbolic name meaning the local host 80 81MSG_KILL_SERVER = '@@KILL_SERVER_END@@' 82MSG_COMPLETIONS = '@@COMPLETIONS' 83MSG_END = 'END@@' 84MSG_INVALID_REQUEST = '@@INVALID_REQUEST' 85MSG_JYTHON_INVALID_REQUEST = '@@JYTHON_INVALID_REQUEST' 86MSG_CHANGE_DIR = '@@CHANGE_DIR:' 87MSG_OK = '@@MSG_OK_END@@' 88MSG_IMPORTS = '@@IMPORTS:' 89MSG_PYTHONPATH = '@@PYTHONPATH_END@@' 90MSG_CHANGE_PYTHONPATH = '@@CHANGE_PYTHONPATH:' 91MSG_JEDI = '@@MSG_JEDI:' 92MSG_SEARCH = '@@SEARCH' 93 94BUFFER_SIZE = 1024 95 96 97 98currDirModule = None 99 100def complete_from_dir(directory): 101 ''' 102 This is necessary so that we get the imports from the same directory where the file 103 we are completing is located. 104 ''' 105 global currDirModule 106 if currDirModule is not None: 107 if len(sys.path) > 0 and sys.path[0] == currDirModule: 108 del sys.path[0] 109 110 currDirModule = directory 111 sys.path.insert(0, directory) 112 113 114def change_python_path(pythonpath): 115 '''Changes the pythonpath (clears all the previous pythonpath) 116 117 @param pythonpath: string with paths separated by | 118 ''' 119 120 split = pythonpath.split('|') 121 sys.path = [] 122 for path in split: 123 path = path.strip() 124 if len(path) > 0: 125 sys.path.append(path) 126 127 128class Processor: 129 130 def __init__(self): 131 # nothing to do 132 return 133 134 def remove_invalid_chars(self, msg): 135 try: 136 msg = str(msg) 137 except UnicodeDecodeError: 138 pass 139 140 if msg: 141 try: 142 return quote_plus(msg) 143 except: 144 sys.stdout.write('error making quote plus in %s\n' % (msg,)) 145 raise 146 return ' ' 147 148 def format_completion_message(self, defFile, completionsList): 149 ''' 150 Format the completions suggestions in the following format: 151 @@COMPLETIONS(modFile(token,description),(token,description),(token,description))END@@ 152 ''' 153 compMsg = [] 154 compMsg.append('%s' % defFile) 155 for tup in completionsList: 156 compMsg.append(',') 157 158 compMsg.append('(') 159 compMsg.append(str(self.remove_invalid_chars(tup[0]))) # token 160 compMsg.append(',') 161 compMsg.append(self.remove_invalid_chars(tup[1])) # description 162 163 if(len(tup) > 2): 164 compMsg.append(',') 165 compMsg.append(self.remove_invalid_chars(tup[2])) # args - only if function. 166 167 if(len(tup) > 3): 168 compMsg.append(',') 169 compMsg.append(self.remove_invalid_chars(tup[3])) # TYPE 170 171 compMsg.append(')') 172 173 return '%s(%s)%s' % (MSG_COMPLETIONS, ''.join(compMsg), MSG_END) 174 175class Exit(Exception): 176 pass 177 178class CompletionServer: 179 180 def __init__(self, port): 181 self.ended = False 182 self.port = port 183 self.socket = None # socket to send messages. 184 self.exit_process_on_kill = True 185 self.processor = Processor() 186 187 188 def connect_to_server(self): 189 from _pydev_imps._pydev_saved_modules import socket 190 191 self.socket = s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 192 try: 193 s.connect((HOST, self.port)) 194 except: 195 sys.stderr.write('Error on connect_to_server with parameters: host: %s port: %s\n' % (HOST, self.port)) 196 raise 197 198 def get_completions_message(self, defFile, completionsList): 199 ''' 200 get message with completions. 201 ''' 202 return self.processor.format_completion_message(defFile, completionsList) 203 204 def get_token_and_data(self, data): 205 ''' 206 When we receive this, we have 'token):data' 207 ''' 208 token = '' 209 for c in data: 210 if c != ')': 211 token = token + c 212 else: 213 break; 214 215 return token, data.lstrip(token + '):') 216 217 def emulated_sendall(self, msg): 218 MSGLEN = 1024 * 20 219 220 totalsent = 0 221 while totalsent < MSGLEN: 222 sent = self.socket.send(msg[totalsent:]) 223 if sent == 0: 224 return 225 totalsent = totalsent + sent 226 227 228 def send(self, msg): 229 if not hasattr(self.socket, 'sendall'): 230 #Older versions (jython 2.1) 231 self.emulated_sendall(msg) 232 else: 233 if IS_PYTHON3K: 234 self.socket.sendall(bytearray(msg, 'utf-8')) 235 else: 236 self.socket.sendall(msg) 237 238 239 def run(self): 240 # Echo server program 241 try: 242 from _pydev_bundle import _pydev_log 243 log = _pydev_log.Log() 244 245 dbg(SERVER_NAME + ' connecting to java server on %s (%s)' % (HOST, self.port) , INFO1) 246 # after being connected, create a socket as a client. 247 self.connect_to_server() 248 249 dbg(SERVER_NAME + ' Connected to java server', INFO1) 250 251 252 while not self.ended: 253 data = '' 254 255 while data.find(MSG_END) == -1: 256 received = self.socket.recv(BUFFER_SIZE) 257 if len(received) == 0: 258 raise Exit() # ok, connection ended 259 if IS_PYTHON3K: 260 data = data + received.decode('utf-8') 261 else: 262 data = data + received 263 264 try: 265 try: 266 if data.find(MSG_KILL_SERVER) != -1: 267 dbg(SERVER_NAME + ' kill message received', INFO1) 268 # break if we received kill message. 269 self.ended = True 270 raise Exit() 271 272 dbg(SERVER_NAME + ' starting keep alive thread', INFO2) 273 274 if data.find(MSG_PYTHONPATH) != -1: 275 comps = [] 276 for p in _sys_path: 277 comps.append((p, ' ')) 278 self.send(self.get_completions_message(None, comps)) 279 280 else: 281 data = data[:data.rfind(MSG_END)] 282 283 if data.startswith(MSG_IMPORTS): 284 data = data[len(MSG_IMPORTS):] 285 data = unquote_plus(data) 286 defFile, comps = _pydev_imports_tipper.generate_tip(data, log) 287 self.send(self.get_completions_message(defFile, comps)) 288 289 elif data.startswith(MSG_CHANGE_PYTHONPATH): 290 data = data[len(MSG_CHANGE_PYTHONPATH):] 291 data = unquote_plus(data) 292 change_python_path(data) 293 self.send(MSG_OK) 294 295 elif data.startswith(MSG_JEDI): 296 data = data[len(MSG_JEDI):] 297 data = unquote_plus(data) 298 line, column, encoding, path, source = data.split('|', 4) 299 try: 300 import jedi # @UnresolvedImport 301 except: 302 self.send(self.get_completions_message(None, [('Error on import jedi', 'Error importing jedi', '')])) 303 else: 304 script = jedi.Script( 305 # Line +1 because it expects lines 1-based (and col 0-based) 306 source=source, 307 line=int(line) + 1, 308 column=int(column), 309 source_encoding=encoding, 310 path=path, 311 ) 312 lst = [] 313 for completion in script.completions(): 314 t = completion.type 315 if t == 'class': 316 t = '1' 317 318 elif t == 'function': 319 t = '2' 320 321 elif t == 'import': 322 t = '0' 323 324 elif t == 'keyword': 325 continue # Keywords are already handled in PyDev 326 327 elif t == 'statement': 328 t = '3' 329 330 else: 331 t = '-1' 332 333 # gen list(tuple(name, doc, args, type)) 334 lst.append((completion.name, '', '', t)) 335 self.send(self.get_completions_message('empty', lst)) 336 337 elif data.startswith(MSG_SEARCH): 338 data = data[len(MSG_SEARCH):] 339 data = unquote_plus(data) 340 (f, line, col), foundAs = _pydev_imports_tipper.search_definition(data) 341 self.send(self.get_completions_message(f, [(line, col, foundAs)])) 342 343 elif data.startswith(MSG_CHANGE_DIR): 344 data = data[len(MSG_CHANGE_DIR):] 345 data = unquote_plus(data) 346 complete_from_dir(data) 347 self.send(MSG_OK) 348 349 else: 350 self.send(MSG_INVALID_REQUEST) 351 except Exit: 352 e = sys.exc_info()[1] 353 msg = self.get_completions_message(None, [('Exit:', 'SystemExit', '')]) 354 try: 355 self.send(msg) 356 except socket.error: 357 pass # Ok, may be closed already 358 359 raise e # raise original error. 360 361 except: 362 dbg(SERVER_NAME + ' exception occurred', ERROR) 363 s = StringIO.StringIO() 364 traceback.print_exc(file=s) 365 366 err = s.getvalue() 367 dbg(SERVER_NAME + ' received error: ' + str(err), ERROR) 368 msg = self.get_completions_message(None, [('ERROR:', '%s\nLog:%s' % (err, log.get_contents()), '')]) 369 try: 370 self.send(msg) 371 except socket.error: 372 pass # Ok, may be closed already 373 374 375 finally: 376 log.clear_log() 377 378 self.socket.close() 379 self.ended = True 380 raise Exit() # connection broken 381 382 383 except Exit: 384 if self.exit_process_on_kill: 385 sys.exit(0) 386 # No need to log SystemExit error 387 except: 388 s = StringIO.StringIO() 389 exc_info = sys.exc_info() 390 391 traceback.print_exception(exc_info[0], exc_info[1], exc_info[2], limit=None, file=s) 392 err = s.getvalue() 393 dbg(SERVER_NAME + ' received error: ' + str(err), ERROR) 394 raise 395 396 397 398if __name__ == '__main__': 399 400 port = int(sys.argv[1]) # this is from where we want to receive messages. 401 402 t = CompletionServer(port) 403 dbg(SERVER_NAME + ' will start', INFO1) 404 t.run() 405