Exemplo n.º 1
0
class LSPClient(QObject, LSPMethodProviderMixIn):
    """Language Server Protocol v3.0 client implementation."""
    #: Signal to inform the editor plugin that the client has
    #  started properly and it's ready to be used.
    sig_initialize = Signal(dict, str)

    #: Signal to report internal server errors through Spyder's
    #  facilities.
    sig_server_error = Signal(str)

    #: Signal to warn the user when either the transport layer or the
    #  server went down
    sig_went_down = Signal(str)

    def __init__(self, parent,
                 server_settings={},
                 folder=getcwd_or_home(),
                 language='python'):
        QObject.__init__(self)
        self.manager = parent
        self.zmq_in_socket = None
        self.zmq_out_socket = None
        self.zmq_in_port = None
        self.zmq_out_port = None
        self.transport = None
        self.server = None
        self.stdio_pid = None
        self.notifier = None
        self.language = language

        self.initialized = False
        self.ready_to_close = False
        self.request_seq = 1
        self.req_status = {}
        self.watched_files = {}
        self.watched_folders = {}
        self.req_reply = {}
        self.server_unresponsive = False
        self.transport_unresponsive = False

        # Select a free port to start the server.
        # NOTE: Don't use the new value to set server_setttings['port']!!
        # That's not required because this doesn't really correspond to a
        # change in the config settings of the server. Else a server
        # restart would be generated when doing a
        # workspace/didChangeConfiguration request.
        if not server_settings['external']:
            self.server_port = select_port(
                default_port=server_settings['port'])
        else:
            self.server_port = server_settings['port']
        self.server_host = server_settings['host']

        self.external_server = server_settings.get('external', False)
        self.stdio = server_settings.get('stdio', False)

        # Setting stdio on implies that external_server is off
        if self.stdio and self.external_server:
            error = ('If server is set to use stdio communication, '
                     'then it cannot be an external server')
            logger.error(error)
            raise AssertionError(error)

        self.folder = folder
        self.configurations = server_settings.get('configurations', {})
        self.client_capabilites = CLIENT_CAPABILITES
        self.server_capabilites = SERVER_CAPABILITES
        self.context = zmq.Context()

        # To set server args
        self._server_args = server_settings.get('args', '')
        self._server_cmd = server_settings['cmd']

        # Save requests name and id. This is only necessary for testing.
        self._requests = []

    def _get_log_filename(self, kind):
        """
        Get filename to redirect server or transport logs to in
        debugging mode.

        Parameters
        ----------
        kind: str
            It can be "server" or "transport".
        """
        if get_debug_level() == 0:
            return None

        fname = '{0}_{1}_{2}.log'.format(kind, self.language, os.getpid())
        location = get_conf_path(osp.join('lsp_logs', fname))

        # Create directory that contains the file, in case it doesn't
        # exist
        if not osp.exists(osp.dirname(location)):
            os.makedirs(osp.dirname(location))

        return location

    @property
    def server_log_file(self):
        """
        Filename to redirect the server process stdout/stderr output.
        """
        return self._get_log_filename('server')

    @property
    def transport_log_file(self):
        """
        Filename to redirect the transport process stdout/stderr
        output.
        """
        return self._get_log_filename('transport')

    @property
    def server_args(self):
        """Arguments for the server process."""
        args = []
        if self.language == 'python':
            args += [sys.executable, '-m']
        args += [self._server_cmd]

        # Replace host and port placeholders
        host_and_port = self._server_args.format(
            host=self.server_host,
            port=self.server_port)
        if len(host_and_port) > 0:
            args += host_and_port.split(' ')

        if self.language == 'python' and get_debug_level() > 0:
            args += ['--log-file', self.server_log_file]
            if get_debug_level() == 2:
                args.append('-v')
            elif get_debug_level() == 3:
                args.append('-vv')

        return args

    @property
    def transport_args(self):
        """Arguments for the transport process."""
        args = [
            sys.executable,
            '-u',
            osp.join(LOCATION, 'transport', 'main.py'),
            '--folder', self.folder,
            '--transport-debug', str(get_debug_level())
        ]

        # Replace host and port placeholders
        host_and_port = '--server-host {host} --server-port {port} '.format(
            host=self.server_host,
            port=self.server_port)
        args += host_and_port.split(' ')

        # Add socket ports
        args += ['--zmq-in-port', str(self.zmq_out_port),
                 '--zmq-out-port', str(self.zmq_in_port)]

        # Adjustments for stdio/tcp
        if self.stdio:
            args += ['--stdio-server']
            if get_debug_level() > 0:
                args += ['--server-log-file', self.server_log_file]
            args += self.server_args
        else:
            args += ['--external-server']

        return args

    def create_transport_sockets(self):
        """Create PyZMQ sockets for transport."""
        self.zmq_out_socket = self.context.socket(zmq.PAIR)
        self.zmq_out_port = self.zmq_out_socket.bind_to_random_port(
            'tcp://{}'.format(LOCALHOST))
        self.zmq_in_socket = self.context.socket(zmq.PAIR)
        self.zmq_in_socket.set_hwm(0)
        self.zmq_in_port = self.zmq_in_socket.bind_to_random_port(
            'tcp://{}'.format(LOCALHOST))

    @Slot(QProcess.ProcessError)
    def handle_process_errors(self, error):
        """Handle errors with the transport layer or server processes."""
        self.sig_went_down.emit(self.language)

    def start_server(self):
        """Start server."""
        # This is not necessary if we're trying to connect to an
        # external server
        if self.external_server or self.stdio:
            return

        logger.info('Starting server: {0}'.format(' '.join(self.server_args)))

        # Create server process
        self.server = QProcess(self)
        env = self.server.processEnvironment()

        # Use local PyLS instead of site-packages one.
        if DEV or running_under_pytest():
            running_in_ci = bool(os.environ.get('CI'))
            if os.name != 'nt' or os.name == 'nt' and not running_in_ci:
                env.insert('PYTHONPATH', os.pathsep.join(sys.path)[:])

        # Adjustments for the Python language server.
        if self.language == 'python':
            # Set the PyLS current working to an empty dir inside
            # our config one. This avoids the server to pick up user
            # files such as random.py or string.py instead of the
            # standard library modules named the same.
            cwd = osp.join(get_conf_path(), 'lsp_paths', 'cwd')
            if not osp.exists(cwd):
                os.makedirs(cwd)

            # On Windows, some modules (notably Matplotlib)
            # cause exceptions if they cannot get the user home.
            # So, we need to pass the USERPROFILE env variable to
            # the PyLS.
            if os.name == "nt" and "USERPROFILE" in os.environ:
                env.insert("USERPROFILE", os.environ["USERPROFILE"])
        else:
            # There's no need to define a cwd for other servers.
            cwd = None

            # Most LSP servers spawn other processes, which may require
            # some environment variables.
            for var in os.environ:
                env.insert(var, os.environ[var])
            logger.info('Server process env variables: {0}'.format(env.keys()))

        # Setup server
        self.server.setProcessEnvironment(env)
        self.server.errorOccurred.connect(self.handle_process_errors)
        self.server.setWorkingDirectory(cwd)
        self.server.setProcessChannelMode(QProcess.MergedChannels)
        if self.server_log_file is not None:
            self.server.setStandardOutputFile(self.server_log_file)

        # Start server
        self.server.start(self.server_args[0], self.server_args[1:])

    def start_transport(self):
        """Start transport layer."""
        logger.info('Starting transport for {1}: {0}'
                    .format(' '.join(self.transport_args), self.language))

        # Create transport process
        self.transport = QProcess(self)
        env = self.transport.processEnvironment()

        # Most LSP servers spawn other processes other than Python, which may
        # require some environment variables
        if self.language != 'python' and self.stdio:
            for var in os.environ:
                env.insert(var, os.environ[var])
            logger.info('Transport process env variables: {0}'.format(
                env.keys()))

        self.transport.setProcessEnvironment(env)

        # Modifying PYTHONPATH to run transport in development mode or
        # tests
        if DEV or running_under_pytest():
            if running_under_pytest():
                env.insert('PYTHONPATH', os.pathsep.join(sys.path)[:])
            else:
                env.insert('PYTHONPATH', os.pathsep.join(sys.path)[1:])
            self.transport.setProcessEnvironment(env)

        # Set up transport
        self.transport.errorOccurred.connect(self.handle_process_errors)
        if self.stdio:
            self.transport.setProcessChannelMode(QProcess.SeparateChannels)
            if self.transport_log_file is not None:
                self.transport.setStandardErrorFile(self.transport_log_file)
        else:
            self.transport.setProcessChannelMode(QProcess.MergedChannels)
            if self.transport_log_file is not None:
                self.transport.setStandardOutputFile(self.transport_log_file)

        # Start transport
        self.transport.start(self.transport_args[0], self.transport_args[1:])

    def start(self):
        """Start client."""
        # NOTE: DO NOT change the order in which these methods are called.
        self.create_transport_sockets()
        self.start_server()
        self.start_transport()

        # Create notifier
        fid = self.zmq_in_socket.getsockopt(zmq.FD)
        self.notifier = QSocketNotifier(fid, QSocketNotifier.Read, self)
        self.notifier.activated.connect(self.on_msg_received)

        # This is necessary for tests to pass locally!
        logger.debug('LSP {} client started!'.format(self.language))

    def stop(self):
        """Stop transport and server."""
        logger.info('Stopping {} client...'.format(self.language))
        if self.notifier is not None:
            self.notifier.activated.disconnect(self.on_msg_received)
            self.notifier.setEnabled(False)
            self.notifier = None
        if self.transport is not None:
            self.transport.kill()
        self.context.destroy()
        if self.server is not None:
            self.server.kill()

    def is_transport_alive(self):
        """Detect if transport layer is alive."""
        state = self.transport.state()
        return state != QProcess.NotRunning

    def is_stdio_alive(self):
        """Check if an stdio server is alive."""
        alive = True
        if not psutil.pid_exists(self.stdio_pid):
            alive = False
        else:
            try:
                pid_status = psutil.Process(self.stdio_pid).status()
            except psutil.NoSuchProcess:
                pid_status = ''
            if pid_status == psutil.STATUS_ZOMBIE:
                alive = False
        return alive

    def is_server_alive(self):
        """Detect if a tcp server is alive."""
        state = self.server.state()
        return state != QProcess.NotRunning

    def is_down(self):
        """
        Detect if the transport layer or server are down to inform our
        users about it.
        """
        is_down = False
        if self.transport and not self.is_transport_alive():
            logger.debug(
                "Transport layer for {} is down!!".format(self.language))
            if not self.transport_unresponsive:
                self.transport_unresponsive = True
                self.sig_went_down.emit(self.language)
            is_down = True

        if self.server and not self.is_server_alive():
            logger.debug("LSP server for {} is down!!".format(self.language))
            if not self.server_unresponsive:
                self.server_unresponsive = True
                self.sig_went_down.emit(self.language)
            is_down = True

        if self.stdio_pid and not self.is_stdio_alive():
            logger.debug("LSP server for {} is down!!".format(self.language))
            if not self.server_unresponsive:
                self.server_unresponsive = True
                self.sig_went_down.emit(self.language)
            is_down = True

        return is_down

    def send(self, method, params, kind):
        """Send message to transport."""
        if self.is_down():
            return

        if ClientConstants.CANCEL in params:
            return
        _id = self.request_seq
        if kind == MessageKind.REQUEST:
            msg = {
                'id': self.request_seq,
                'method': method,
                'params': params
            }
            self.req_status[self.request_seq] = method
        elif kind == MessageKind.RESPONSE:
            msg = {
                'id': self.request_seq,
                'result': params
            }
        elif kind == MessageKind.NOTIFICATION:
            msg = {
                'method': method,
                'params': params
            }

        logger.debug('Perform request {0} with id {1}'.format(method, _id))

        # Save requests to check their ordering.
        if running_under_pytest():
            self._requests.append((_id, method))

        # Try sending a message. If the send queue is full, keep trying for a
        # a second before giving up.
        timeout = 1
        start_time = time.time()
        timeout_time = start_time + timeout
        while True:
            try:
                self.zmq_out_socket.send_pyobj(msg, flags=zmq.NOBLOCK)
                self.request_seq += 1
                return int(_id)
            except zmq.error.Again:
                if time.time() > timeout_time:
                    self.sig_went_down.emit(self.language)
                    return
                # The send queue is full! wait 0.1 seconds before retrying.
                if self.initialized:
                    logger.warning("The send queue is full! Retrying...")
                time.sleep(.1)

    @Slot()
    def on_msg_received(self):
        """Process received messages."""
        self.notifier.setEnabled(False)
        while True:
            try:
                # events = self.zmq_in_socket.poll(1500)
                resp = self.zmq_in_socket.recv_pyobj(flags=zmq.NOBLOCK)

                try:
                    method = resp['method']
                    logger.debug(
                        '{} response: {}'.format(self.language, method))
                except KeyError:
                    pass

                if 'error' in resp:
                    logger.debug('{} Response error: {}'
                                 .format(self.language, repr(resp['error'])))
                    if self.language == 'python':
                        # Show PyLS errors in our error report dialog only in
                        # debug or development modes
                        if get_debug_level() > 0 or DEV:
                            message = resp['error'].get('message', '')
                            traceback = (resp['error'].get('data', {}).
                                         get('traceback'))
                            if traceback is not None:
                                traceback = ''.join(traceback)
                                traceback = traceback + '\n' + message
                                self.sig_server_error.emit(traceback)
                        req_id = resp['id']
                        if req_id in self.req_reply:
                            self.req_reply[req_id](None, {'params': []})
                elif 'method' in resp:
                    if resp['method'][0] != '$':
                        if 'id' in resp:
                            self.request_seq = int(resp['id'])
                        if resp['method'] in self.handler_registry:
                            handler_name = (
                                self.handler_registry[resp['method']])
                            handler = getattr(self, handler_name)
                            handler(resp['params'])
                elif 'result' in resp:
                    if resp['result'] is not None:
                        req_id = resp['id']
                        if req_id in self.req_status:
                            req_type = self.req_status[req_id]
                            if req_type in self.handler_registry:
                                handler_name = self.handler_registry[req_type]
                                handler = getattr(self, handler_name)
                                handler(resp['result'], req_id)
                                self.req_status.pop(req_id)
                                if req_id in self.req_reply:
                                    self.req_reply.pop(req_id)
            except RuntimeError:
                # This is triggered when a codeeditor instance has been
                # removed before the response can be processed.
                pass
            except zmq.ZMQError:
                self.notifier.setEnabled(True)
                return

    def perform_request(self, method, params):
        if method in self.sender_registry:
            handler_name = self.sender_registry[method]
            handler = getattr(self, handler_name)
            _id = handler(params)
            if 'response_callback' in params:
                if params['requires_response']:
                    self.req_reply[_id] = params['response_callback']
            return _id

    # ------ LSP initialization methods --------------------------------
    @handles(SERVER_READY)
    @send_request(method=LSPRequestTypes.INITIALIZE)
    def initialize(self, params, *args, **kwargs):
        self.stdio_pid = params['pid']
        pid = self.transport.processId() if not self.external_server else None
        params = {
            'processId': pid,
            'rootUri': pathlib.Path(osp.abspath(self.folder)).as_uri(),
            'capabilities': self.client_capabilites,
            'trace': TRACE
        }
        return params

    @send_request(method=LSPRequestTypes.SHUTDOWN)
    def shutdown(self):
        params = {}
        return params

    @handles(LSPRequestTypes.SHUTDOWN)
    def handle_shutdown(self, response, *args):
        self.ready_to_close = True

    @send_notification(method=LSPRequestTypes.EXIT)
    def exit(self):
        params = {}
        return params

    @handles(LSPRequestTypes.INITIALIZE)
    def process_server_capabilities(self, server_capabilites, *args):
        """
        Register server capabilities and inform other plugins that it's
        available.
        """
        # Update server capabilities with the info sent by the server.
        server_capabilites = server_capabilites['capabilities']

        if isinstance(server_capabilites['textDocumentSync'], int):
            kind = server_capabilites['textDocumentSync']
            server_capabilites['textDocumentSync'] = TEXT_DOCUMENT_SYNC_OPTIONS
            server_capabilites['textDocumentSync']['change'] = kind
        if server_capabilites['textDocumentSync'] is None:
            server_capabilites.pop('textDocumentSync')

        self.server_capabilites.update(server_capabilites)

        # The initialized notification needs to be the first request sent by
        # the client according to the protocol.
        self.initialized = True
        self.initialized_call()

        # This sends a DidChangeConfiguration request to pass to the server
        # the configurations set by the user in our config system.
        self.send_configurations(self.configurations)

        # Inform other plugins that the server is up.
        self.sig_initialize.emit(self.server_capabilites, self.language)

    @send_notification(method=LSPRequestTypes.INITIALIZED)
    def initialized_call(self):
        params = {}
        return params

    # ------ Settings queries --------------------------------
    @property
    def support_multiple_workspaces(self):
        workspace_settings = self.server_capabilites['workspace']
        return workspace_settings['workspaceFolders']['supported']

    @property
    def support_workspace_update(self):
        workspace_settings = self.server_capabilites['workspace']
        return workspace_settings['workspaceFolders']['changeNotifications']
Exemplo n.º 2
0
class TerminalMainWidget(PluginMainWidget):
    """
    Terminal plugin main widget.
    """
    MAX_SERVER_CONTACT_RETRIES = 40
    URL_ISSUES = ' https://github.com/spyder-ide/spyder-terminal/issues'

    # --- Signals
    # ------------------------------------------------------------------------
    sig_server_is_ready = Signal()
    """
    This signal is emitted when the server is ready to connect.
    """

    def __init__(self, name, plugin, parent):
        """Widget constructor."""
        self.terms = []
        super().__init__(name, plugin, parent)

        # Attributes
        self.tab_widget = None
        self.menu_actions = None
        self.server_retries = 0
        self.server_ready = False
        self.font = None
        self.port = select_port(default_port=8071)
        self.stdout_file = None
        self.stderr_file = None
        if get_debug_level() > 0:
            self.stdout_file = osp.join(os.getcwd(), 'spyder_terminal_out.log')
            self.stderr_file = osp.join(os.getcwd(), 'spyder_terminal_err.log')
        self.project_path = None
        self.current_file_path = None
        self.current_cwd = os.getcwd()

        # Widgets
        self.main = parent
        self.find_widget = FindTerminal(self)
        self.find_widget.hide()

        layout = QVBoxLayout()

        # Tab Widget
        self.tabwidget = Tabs(self, rename_tabs=True)
        self.tabwidget.currentChanged.connect(self.refresh_plugin)
        self.tabwidget.move_data.connect(self.move_tab)
        self.tabwidget.set_close_function(self.close_term)

        if (hasattr(self.tabwidget, 'setDocumentMode') and
                not sys.platform == 'darwin'):
            # Don't set document mode to true on OSX because it generates
            # a crash when the console is detached from the main window
            # Fixes Issue 561
            self.tabwidget.setDocumentMode(True)
        layout.addWidget(self.tabwidget)
        layout.addWidget(self.find_widget)
        self.setLayout(layout)

        css = qstylizer.style.StyleSheet()
        css.QTabWidget.pane.setValues(border=0)
        self.setStyleSheet(css.toString())

        self.__wait_server_to_start()

    # ---- PluginMainWidget API
    # ------------------------------------------------------------------------
    def get_focus_widget(self):
        """
        Set focus on current selected terminal.

        Return the widget to give focus to when
        this plugin's dockwidget is raised on top-level.
        """
        term = self.tabwidget.currentWidget()
        if term is not None:
            return term.view

    def get_title(self):
        """Define the title of the widget."""
        return _('Terminal')

    def setup(self):
        """Perform the setup of plugin's main menu and signals."""
        self.cmd = find_program(self.get_conf('shell'))
        server_args = [
            sys.executable, '-m', 'spyder_terminal.server',
             '--port', str(self.port), '--shell', self.cmd]
        self.server = QProcess(self)
        env = self.server.processEnvironment()
        for var in os.environ:
            env.insert(var, os.environ[var])
        self.server.setProcessEnvironment(env)
        self.server.errorOccurred.connect(self.handle_process_errors)
        self.server.setProcessChannelMode(QProcess.SeparateChannels)
        if self.stdout_file and self.stderr_file:
            self.server.setStandardOutputFile(self.stdout_file)
            self.server.setStandardErrorFile(self.stderr_file)
        self.server.start(server_args[0], server_args[1:])
        self.color_scheme = self.get_conf('appearance', 'ui_theme')
        self.theme = self.get_conf('appearance', 'selected')

        # Menu
        menu = self.get_options_menu()

        # Actions
        new_terminal_toolbar_action = self.create_toolbutton(
            TerminalMainWidgetToolbarSections.New,
            text=_("Open a new terminal"),
            icon=self.create_icon('expand_selection'),
            triggered=lambda: self.create_new_term(),
        )

        self.add_corner_widget(
            TerminalMainWidgetCornerToolbar.NewTerm,
            new_terminal_toolbar_action)

        new_terminal_cwd = self.create_action(
            TerminalMainWidgetActions.NewTerminalForCWD,
            text=_("New terminal in current working directory"),
            tip=_("Sets the pwd at the current working directory"),
            triggered=lambda: self.create_new_term(),
            shortcut_context='terminal',
            register_shortcut=True)

        self.new_terminal_project = self.create_action(
            TerminalMainWidgetActions.NewTerminalForProject,
            text=_("New terminal in current project"),
            tip=_("Sets the pwd at the current project directory"),
            triggered=lambda: self.create_new_term(path=self.project_path))

        new_terminal_file = self.create_action(
            TerminalMainWidgetActions.NewTerminalForFile,
            text=_("New terminal in current Editor file"),
            tip=_("Sets the pwd at the directory that contains the current "
                  "opened file"),
            triggered=lambda: self.create_new_term(
                path=self.current_file_path))

        rename_tab_action = self.create_action(
            TerminalMainWidgetActions.RenameTab,
            text=_("Rename terminal"),
            triggered=lambda: self.tab_name_editor())

        # Context menu actions
        self.create_action(
            TerminalMainWidgetActions.Copy,
            text=_('Copy text'),
            icon=self.create_icon('editcopy'),
            shortcut_context='terminal',
            triggered=lambda: self.copy(),
            register_shortcut=True)

        self.create_action(
            TerminalMainWidgetActions.Paste,
            text=_('Paste text'),
            icon=self.create_icon('editpaste'),
            shortcut_context='terminal',
            triggered=lambda: self.paste(),
            register_shortcut=True)

        self.create_action(
            TerminalMainWidgetActions.Clear,
            text=_('Clear terminal'),
            shortcut_context='terminal',
            triggered=lambda: self.clear(),
            register_shortcut=True)

        self.create_action(
            TerminalMainWidgetActions.ZoomIn,
            text=_('Zoom in'),
            shortcut_context='terminal',
            triggered=lambda: self.increase_font(),
            register_shortcut=True)

        self.create_action(
            TerminalMainWidgetActions.ZoomOut,
            text=_('Zoom out'),
            shortcut_context='terminal',
            triggered=lambda: self.decrease_font(),
            register_shortcut=True)

        # Create context menu
        self.create_menu(TermViewMenus.Context)

        # Add actions to options menu
        for item in [new_terminal_cwd, self.new_terminal_project,
                     new_terminal_file]:
            self.add_item_to_menu(
                item, menu=menu,
                section=TerminalMainWidgetMenuSections.New)

        self.add_item_to_menu(
            rename_tab_action, menu=menu,
            section=TerminalMainWidgetMenuSections.TabActions)

    def update_actions(self):
        """Setup and update the actions in the options menu."""
        if self.project_path is None:
            self.new_terminal_project.setEnabled(False)

    # ------ Private API ------------------------------------------
    def copy(self):
        if self.get_focus_widget():
            self.get_focus_widget().copy()

    def paste(self):
        if self.get_focus_widget():
            self.get_focus_widget().paste()

    def clear(self):
        if self.get_focus_widget():
            self.get_focus_widget().clear()

    def increase_font(self):
        if self.get_focus_widget():
            self.get_focus_widget().increase_font()

    def decrease_font(self):
        if self.get_focus_widget():
            self.get_focus_widget().decrease_font()

    def __wait_server_to_start(self):
        try:
            code = requests.get('http://127.0.0.1:{0}'.format(
                self.port)).status_code
        except:
            code = 500

        if self.server_retries == self.MAX_SERVER_CONTACT_RETRIES:
            QMessageBox.critical(self, _('Spyder Terminal Error'),
                                 _("Terminal server could not be located at "
                                   '<a href="http://127.0.0.1:{0}">'
                                   'http://127.0.0.1:{0}</a>,'
                                   ' please restart Spyder on debugging mode '
                                   "and open an issue with the contents of "
                                   "<tt>{1}</tt> and <tt>{2}</tt> "
                                   "files at {3}.").format(self.port,
                                                           self.stdout_file,
                                                           self.stderr_file,
                                                           self.URL_ISSUES),
                                 QMessageBox.Ok)
        elif code != 200:
            self.server_retries += 1
            QTimer.singleShot(250, self.__wait_server_to_start)
        elif code == 200:
            self.sig_server_is_ready.emit()
            self.server_ready = True
            self.create_new_term(give_focus=False)

    # ------ Plugin API --------------------------------
    def update_font(self, font):
        """Update font from Preferences."""
        self.font = font
        for term in self.terms:
            term.set_font(font.family())

    def on_close(self, cancelable=False):
        """Perform actions before parent main window is closed."""
        for term in self.terms:
            term.close()
        self.server.kill()
        return True

    def refresh_plugin(self):
        """Refresh tabwidget."""
        term = None
        if self.tabwidget.count():
            term = self.tabwidget.currentWidget()
            term.view.setFocus()
        else:
            term = None

    @on_conf_change
    def apply_plugin_settings(self, options):
        """Apply the config settings."""
        term_options = {}
        for option in options:
            if option == 'color_scheme_name':
                term_options[option] = option
            else:
                term_options[option] = self.get_conf(option)
        for term in self.get_terms():
            term.apply_settings(term_options)

    # ------ Public API (for terminals) -------------------------
    def get_terms(self):
        """Return terminal list."""
        return [cl for cl in self.terms if isinstance(cl, TerminalWidget)]

    def get_current_term(self):
        """Return the currently selected terminal."""
        try:
            terminal = self.tabwidget.currentWidget()
        except AttributeError:
            terminal = None
        if terminal is not None:
            return terminal

    def create_new_term(self, name=None, give_focus=True, path=None):
        """Add a new terminal tab."""
        if path is None:
            path = self.current_cwd
        if self.project_path is not None:
            path = self.project_path
        path = path.replace('\\', '/')
        term = TerminalWidget(
            self, self.port, path=path, font=self.font.family(),
            theme=self.theme, color_scheme=self.color_scheme)
        self.add_tab(term)
        term.terminal_closed.connect(lambda: self.close_term(term=term))

    def close_term(self, index=None, term=None):
        """Close a terminal tab."""
        if not self.tabwidget.count():
            return
        if term is not None:
            index = self.tabwidget.indexOf(term)
        if index is None and term is None:
            index = self.tabwidget.currentIndex()
        if index is not None:
            term = self.tabwidget.widget(index)
        if term:
            term.close()
        self.tabwidget.removeTab(self.tabwidget.indexOf(term))
        if term in self.terms:
            self.terms.remove(term)
        if self.tabwidget.count() == 0:
            self.create_new_term()

    def set_project_path(self, path):
        """Refresh current project path."""
        self.project_path = path
        self.new_terminal_project.setEnabled(True)

    def set_current_opened_file(self, path):
        """Get path of current opened file in editor."""
        self.current_file_path = osp.dirname(path)

    def unset_project_path(self):
        """Refresh current project path."""
        self.project_path = None
        self.new_terminal_project.setEnabled(False)

    @Slot(str)
    def set_current_cwd(self, cwd):
        """Update current working directory."""
        self.current_cwd = cwd

    def server_is_ready(self):
        """Return server status."""
        return self.server_ready

    def search_next(self, text, case=False, regex=False, word=False):