Skip to content

Instantly share code, notes, and snippets.

@icholy
Last active May 16, 2024 19:17
Show Gist options
  • Save icholy/8e2164c77d0c2bbac245 to your computer and use it in GitHub Desktop.
Save icholy/8e2164c77d0c2bbac245 to your computer and use it in GitHub Desktop.
Python wrapper around TypeScript TSServer
#!/usr/bin/env python
#
# Copyright (C) 2015 Google Inc.
#
# This file is part of YouCompleteMe.
#
# YouCompleteMe is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# YouCompleteMe is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
import json
import logging
import subprocess
from threading import Lock, Event, Thread
from ycmd import utils
BINARY_NOT_FOUND_MESSAGE = ( 'tsserver not found. '
'TypeScript 1.5 or higher is required' )
_logger = logging.getLogger( __name__ )
class ResponseEvent( Event ):
"Used for blocking the SendRequest method until the response is available"
def __init__( self ):
super( ResponseEvent, self ).__init__()
self._response = None
def SetResponse( self, response ):
self._response = response
self.set()
def GetResponse( self ):
if not self.is_set():
self.wait()
return self._response
class TSServer:
"""
Wrapper for for TSServer which is bundled with TypeScript 1.5
See the protocol here:
https://github.com/Microsoft/TypeScript/blob/2cb0dfd99dc2896958b75e44303d8a7a32e5dc33/src/server/protocol.d.ts
"""
def __init__( self, user_options ):
# Used to prevent threads from concurrently reading and writing to
# the tsserver process' stdout and stdin
self._lock = Lock()
binarypath = utils.PathToFirstExistingExecutable( [ 'tsserver' ] )
if not binarypath:
_logger.error( BINARY_NOT_FOUND_MESSAGE )
raise RuntimeError( BINARY_NOT_FOUND_MESSAGE )
# Each request sent to tsserver must have a sequence id.
# Responses contain the id sent in the corresponding request.
self._sequenceid = 0
# TSServer ignores the fact that newlines are two characters on Windows
# (\r\n) instead of one on other platforms (\n), so we use the
# universal_newlines option to convert those newlines to \n. See the issue
# https://github.com/Microsoft/TypeScript/issues/3403
# TODO: remove this option when the issue is fixed.
# We also need to redirect the error stream to the output one on Windows.
self._tsserver_handle = utils.SafePopen( binarypath,
stdout = subprocess.PIPE,
stdin = subprocess.PIPE,
stderr = subprocess.STDOUT,
universal_newlines = True )
# When a request message is sent and requires a response, a ResponseEvent
# is placed in this dictionary and
self._response_events = {}
Thread( target = self._MessageReaderLoop ).start()
def SendRequest( self, command, arguments = None, wait_for_response = True ):
"""Send a request message to TSServer."""
seq = self._NextSequenceId()
request = {
'seq': seq,
'type': 'request',
'command': command
}
if arguments:
request[ 'arguments' ] = arguments
# If the request expects a response, use an Event to block
# until the response is available
if wait_for_response:
event = ResponseEvent()
self._response_events[ seq ] = event
self._WriteMessage( request )
return event.GetResponse()
self._WriteMessage( request )
def _NextSequenceId( self ):
seq = self._sequenceid
self._sequenceid += 1
return seq
def _WriteMessage( self, message ):
with self._lock:
self._tsserver_handle.stdin.write( json.dumps( message ) )
self._tsserver_handle.stdin.write( "\n" )
def _MessageReaderLoop( self ):
while True:
try:
message = self._ReadMessage()
if message[ 'type' ] == 'event':
self._HandleEvent( message )
if message[ 'type' ] == 'response':
self._HandleResponse( message )
except Exception as e:
_logger.error( e )
def _ReadMessage( self ):
"""Read a response message from TSServer."""
# The headers are pretty similar to HTTP.
# At the time of writing, 'Content-Length' is the only supplied header.
headers = {}
while True:
headerline = self._tsserver_handle.stdout.readline()
if not headerline:
break
key, value = headerline.split( ':', 1 )
headers[ key.strip() ] = value.strip()
# The response message is a JSON object which comes back on one line.
# Since this might change in the future, we use the 'Content-Length'
# header.
if 'Content-Length' not in headers:
raise RuntimeError( "Missing 'Content-Length' header" )
contentlength = int( headers[ 'Content-Length' ] )
message = json.loads( self._tsserver_handle.stdout.read( contentlength ) )
return message
def _HandleResponse( self, response ):
seq = response[ 'request_seq' ]
if seq in self._response_events:
self._response_events[ seq ].SetResponse( response )
del self._response_events[ seq ]
else:
_logger.info( 'Recieved unhandled response (sequence {0})'.format( seq ) )
def _HandleEvent( self, event ):
"""Handle event message from TSServer."""
# We ignore events for now since we don't have a use for them.
eventname = event[ 'event' ]
_logger.info( 'Recieved {0} event from tsserver'.format( eventname ) )
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment