#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Created on 2020/9/7
@author: Irony
@email: 892768447@qq.com
@file: widget
@description: Code Editor TabWidget

协作者&维护者(Co-Author and supporter):
Zhanyi Hou
1295752786@qq.com

注释为中文。若需要英文版翻译可将注释复制到翻译软件中。我们已经确认所有的注释都可以被翻译软件正确译为英文。
All Comments are in Simplified Chinese.If you need English version, you can get the translation
via translation websites such as youdao or google. We had made sure that all comments can be correctly
translated into English by those websites.

"""

__version__ = '0.1'

import logging
import os
import sys
import re
import time
from contextlib import redirect_stdout
from io import StringIO
from queue import Queue
from typing import TYPE_CHECKING, List
import pmgwidgets
from qtpy.QtCore import QDir, QLocale, QObject, Signal, QThread, QTemporaryFile, QTimer
from qtpy.QtGui import QCloseEvent
from qtpy.QtWidgets import QTabWidget, QFileDialog, QMessageBox, QApplication, QSizePolicy, QWidget, QComboBox

from qtpyeditor import PythonHighlighter

# TODO to remove (use extensionlib)
from flake8.main.application import Application
from typing import TYPE_CHECKING, Dict, Union, Tuple, Optional, Any

from pmgwidgets import PMDockObject, in_unit_test, PMGFileSystemWatchdog, UndoManager

if TYPE_CHECKING or in_unit_test():
    from pyminer2.extensions.packages.code_editor.codeeditor.pythoneditor import PMPythonEditor
    # from pyminer2.extensions.packages.code_editor.codeeditor.baseeditor import PMBaseEditor
    # from pyminer2.extensions.packages.code_editor.codeeditor.cppeditor import PMCPPEditor
    # from pyminer2.extensions.packages.code_editor.codeeditor.cythoneditor import PMCythonEditor
    from pyminer2.extensions.packages.code_editor.codeeditor.markdowneditor import PMMarkdownEditor

    from pyminer2.extensions.packages.code_editor.debugger import PMDebugConsoleTabWidget
    # from pyminer2.extensions.packages.code_editor.codeeditor.ui.findinpath import FindInPathWidget
else:
    # from codeeditor.baseeditor import PMBaseEditor
    from codeeditor.pythoneditor import PMPythonEditor
    # from codeeditor.cppeditor import PMCPPEditor
    # from codeeditor.cythoneditor import PMCythonEditor
    from codeeditor.markdowneditor import PMMarkdownEditor

EDITOR_TYPE = Optional[Union['PMBaseEditor', 'PMCythonEditor', 'PMPythonEditor', 'PMCPPEditor', 'PMMarkdownEditor']]
logger = logging.getLogger(__name__)


class CodeCheckWorker(QObject):
    """
    代码检查
    """
    checked = Signal(object, list)

    def __init__(self, *args, **kwargs):
        super(CodeCheckWorker, self).__init__(*args, **kwargs)
        self._queue = Queue()
        self._running = True
        self.background_checking = True

    def add(self, widget: 'QsciScintilla', code: str):
        """
        添加需要检测的对象
        Args:
            widget: 目标编辑器
            code: 目标编辑器代码

        Returns:

        """
        self._queue.put_nowait((widget, code))
        while self._queue.qsize() > 3:
            self._queue.get(False, 0)

    def stop(self):
        """
        停止线程标志
        """
        self._running = False

    def run(self):
        """
        代码检测工作函数
        Returns:

        """
        while 1:
            if not self._running:
                logger.info('code checker quit')
                break
            if not self.background_checking:
                QThread.msleep(500)
                continue
            if self._queue.qsize() == 0:
                QThread.msleep(500)
                continue
            try:
                widget, code = self._queue.get(False, 0.5)
                # 创建临时文件
                file = QTemporaryFile(self)
                file.setAutoRemove(True)
                if file.open():
                    with open(file.fileName(), 'wb') as fp:
                        fp.write(code.encode())
                    file.close()
                    # 使用flake8检测代码
                    results = []
                    with StringIO() as out, redirect_stdout(out):
                        app = Application()
                        app.initialize(
                            ['flake8', '--exit-zero', '--config',
                             os.path.join(os.path.dirname(__file__), 'config', '.flake8')])
                        app.run_checks([file.fileName()])
                        app.report()
                        results = out.getvalue().split('\n')
                    print(results)
                    new_results: List[Tuple[int, int, str, str]] = []
                    for ret in results:
                        if re.search(r'\d+:\d+:[EFW]\d+:.*?', ret):
                            split_list = ret.split(':')
                            line_no = int(split_list[0])
                            column = int(split_list[1])
                            error_code = split_list[2]
                            error_type = split_list[3]
                            new_results.append((line_no, column, error_code, error_type))
                    # results = [ret for ret in results if re.search(r'\d+:\d+:[EFW]\d+:.*?', ret)]

                    self.checked.emit(widget, new_results)  # 如果为空，也应该这样做。将空列表传入可以清空所有的标记。
                file.deleteLater()
                del file
            except Exception as e:
                logger.warning(str(e))


class PMCodeEditTabWidget(QTabWidget, PMDockObject):
    """
    多标签页编辑器控件
    """
    extension_lib = None

    def __init__(self, *args, **kwargs):
        super(PMCodeEditTabWidget, self).__init__(*args, **kwargs)
        # 设置其尺寸政策为x,y轴均膨胀。
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.setMinimumWidth(200)
        self._color_scheme = 'light'
        self._index = 0
        self._current_executable = sys.executable  # 设置当前可执行文件的路径。
        self._keywords = []
        self._old_code = ''
        self._thread_check = None
        self._worker_check = None
        self._timer_check = None
        self._last_cursorpos_requested_time = 0
        self._find_in_path_widget: "FindInPathWidget" = None
        self.settings: Dict[str, object] = {}

        self.debug_widget: Optional['PMDebugConsoleTabWidget'] = None
        self.watchdog: Optional['PMGFileSystemWatchdog'] = None

        self.cursor_pos_manager = UndoManager(stack_size=30)

    def set_extension_lib(self, extension_lib):
        self.extension_lib = extension_lib
        self.extension_lib.Data.add_data_changed_callback(lambda name, var, source: self.slot_check_code(True))
        self.extension_lib.Data.add_data_deleted_callback(lambda name, provider: self.slot_check_code(True))

    def setup_ui(self) -> None:
        """
        `setup_ui`方法继承于PMDockObject，将被插件管理器直接调用。
        Returns:

        """
        # 文档模式
        self.setDocumentMode(True)
        # 标签页可关闭
        self.setTabsClosable(True)
        # 标签页可移动
        self.setMovable(True)
        self._init_signals()
        # 创建默认空白页
        self.slot_new_script()

        # 初始化后台检测代码线程
        self._timer_check = QTimer(self)
        self._timer_check.timeout.connect(self.slot_check_code)
        self._thread_check = QThread(self)
        self._worker_check = CodeCheckWorker()
        self._worker_check.moveToThread(self._thread_check)
        self._worker_check.checked.connect(self.slot_checked_code)
        self._thread_check.finished.connect(self._worker_check.deleteLater)
        self._thread_check.finished.connect(self._thread_check.deleteLater)
        self._thread_check.started.connect(self._worker_check.run)
        self._thread_check.start()
        self._timer_check.start(2000)

    def on_work_dir_changed(self, path: str) -> None:
        """
        处理工作路径改变时候的信号
        Deal with signal when work dir changed

        Args:
            path: new work directory

        Returns:None

        """
        last_path = ''
        if self.watchdog is not None:
            last_path = self.watchdog.path
        if os.path.normcase(last_path) != os.path.normcase(path):
            if self.watchdog is not None:
                self.watchdog.observer.stop()
                self.watchdog.deleteLater()
            self.watchdog = PMGFileSystemWatchdog(path)
            self.watchdog.signal_file_modified.connect(self.on_file_modified)
            self.watchdog.signal_file_moved.connect(self.signal_file_moved)

    def signal_file_moved(self, path1, path2):
        """
        文件被移动（或者重命名）时触发的事件。
        Args:
            path1:
            path2:

        Returns:

        """
        for i in range(self.count()):
            editor = self.widget(i)
            if os.path.normcase(path1) == os.path.normcase(editor._path):
                if os.path.splitext(path1)[1] == os.path.splitext(path2)[1]:  # 如果扩展名相同，则直接更改文件名即可
                    editor.set_path(path2)
                else:
                    self.slot_tab_close_request(i)
                    self.slot_new_script(path2)
                break

    def on_file_modified(self, path: str) -> None:
        """
        处理文件在编辑器外部被修改的事件。
        编辑器内部保存的时候，会将被编辑的内容写入磁盘。写入事件发生时，编辑器设置`last_save_time`为当前时间戳。
        当`on_file_modified`方法调用时，此方法将获取调用时的时间戳，并与编辑器内部的`last_save_time`时间戳进行对比，
        如果`on_file_modified`方法在编辑器内部时间戳`last_save_time`以后的1秒以内进行调用，那么将忽略此事件（因为此事件极大概率是由编辑器发出的。）
        由于看门狗可能对同一次文件更改操作发送多个信号，所以这个函数可能在短时间（毫秒级别）内对同一文件调用多次，所以每次调用这个事件的时候，都会刷新一下last_save_time 以防
        短时间内调用多次的问题发生。

        Handles events where the file has been modified outside the editor.

        When saved internally, the editor writes the edited content to disk.
        When a write event occurs, the editor sets last_save_time to the current timestamp.
        When the `on_file_modified` method is called, this method gets the timestamp at the time of the call and compares it to the last_save_time timestamp inside the editor,
        If the `on_file_Modified` method is called within 1 second of the internal timestamp `last_save_time` of the editor, this event is ignored (because it is highly likely to have been issued by the editor).
        Since the watchdog can send multiple signals to the same file change operation, this function can be called to the same file multiple times in a short period of time (at the millisecond level), so each time the event is called, last_save_time is refreshed to prevent it
        Problems with multiple calls in a short period of time occur.

        Args:
            path:

        Returns:

        """
        logger.warning('file modified:' + path + ' at time:' + str(time.time()))
        editor = self.get_editor_tab_by_path(path)
        if editor is not None:
            if time.time() - editor.last_save_time > 1:
                editor.slot_file_modified_externally()

    def get_editor_tab_by_path(self, path) -> EDITOR_TYPE:
        """
        通过路径获取编辑器。如果没有打开,则返回None
        Args:
            path:

        Returns:

        """
        for i in range(self.count()):
            w = self.widget(i)
            if os.path.normcase(w.path()) == os.path.normcase(path):
                return w
        return None

    def keywords(self) -> list:
        """
        返回自定义的关键词
        Returns:

        """
        return self._keywords

    def set_keywords(self, keywords: list):
        """
        增加额外的关键词

        :param keywords: 关键词列表
        :type: list
        :return:
        """
        if not isinstance(keywords, (tuple, list)):
            return
        self._keywords = list(keywords)

    def get_current_editor(self) -> Optional['PMPythonEditor']:
        """
        get current editor
        Returns:

        """
        try:
            return self.currentWidget()
        except Exception as e:
            logger.warning(str(e))
        return None

    def get_current_edit(self) -> Optional['QsciScintilla']:
        """
        返回当前qscintilla texteditor对象

        Returns: Current Editor(QScintilla)

        """
        try:
            return self.currentWidget().textEdit
        except Exception as e:
            logger.warning(str(e))
        return None

    def get_current_text(self, selected: bool = False) -> str:
        """
        返回当前编辑器选中或者全部内容

        :param selected: 是否获取选中的内容 True or False
        :type: bool
        :return: 返回当前编辑器选中或者全部内容
        """
        try:
            return self.currentWidget().text(selected)
        except Exception as e:
            logger.warning(str(e))
            return ''

    def get_current_filename(self) -> str:
        """
        返回当前编辑器文件名

        :rtype: str
        :return: 返回当前编辑器文件名
        """
        try:
            return self.currentWidget().filename()
        except Exception as e:
            logger.warning(str(e))
            return ''

    def get_current_path(self) -> str:
        """
        返回当前编辑器文件路径

        :rtype: str
        :return: 返回当前编辑器文件路径
        """
        try:
            return self.currentWidget().path()
        except Exception as e:
            logger.warning(str(e))
            return ''

    def slot_set_tab_text(self, title: str) -> None:
        """
        设置标签页标题
        Args:
            title: 标题文字，str

        Returns:

        """
        widget = self.sender()  # 获取来自哪个编辑器
        self.setTabText(self.indexOf(widget), title)

    def slot_new_script(self, path: str = ''):
        """
        创建新文件或者打开已有文件
        当文件已经打开时，跳转到该文件
        Args:
            path:

        Returns:

        """
        if not path:
            # 创建临时文件
            while True:
                self._index += 1
                path = os.path.join(QDir.tempPath(), 'Untitled-%d' % self._index).replace(os.sep, '/')
                try:
                    open(path, 'w', encoding='utf-8', errors='ignore').write('')
                    break
                except IOError as e:
                    logger.warning(str(e))
        w = self.get_editor_tab_by_path(path)
        if w is not None:
            self.setCurrentWidget(w)
            return
        widget: 'PMBaseEditor' = None
        if path.endswith('.py') or path == os.path.join(QDir.tempPath(),
                                                        'Untitled-%d' % self._index).replace(os.sep, '/'):
            widget = PMPythonEditor(parent=self)
        # elif path.endswith(('.c', '.cpp', '.h')):
        #     widget = PMCPPEditor(parent=self)
        # elif path.endswith('.pyx'):
        #     widget = PMCythonEditor(parent=self)
        elif path.endswith('.md'):
            widget = PMMarkdownEditor(parent=self)
        else:
            QMessageBox.warning(self, self.tr('Warning'),
                                self.tr('Editor does not support file:\n%s') % path)
            logger.warning('Editor Cannot open path:%s!!' % path)
        if self.settings is not None:
            widget.update_settings(self.settings)
        widget.set_lib(self.extension_lib)
        widget.load_file(path)
        widget.windowTitleChanged.connect(self.slot_set_tab_text)
        if isinstance(widget, PMPythonEditor):
            try:
                widget.signal_focused_in.connect(self.slot_focused_in)
                widget.signal_new_requested.connect(lambda path, mode: self.slot_new_script(path))
            except:
                import traceback
                traceback.print_exc()
            # widget.textEdit.cursorPositionChanged.connect(self.slot_cursor_position_changed)
            # widget.signal_cursor_next_pos.connect(self.slot_goto_next_cursor_pos)
            # widget.signal_cursor_last_pos.connect(self.slot_goto_last_cursor_pos)
            # widget.signal_goto_definition.connect(self.slot_goto_file)
            if hasattr(widget, 'signal_request_find_in_path'):
                widget.signal_request_find_in_path.connect(self.slot_find_in_path)
            pass
        self.addTab(widget, widget.filename())
        self.setCurrentWidget(widget)
        if not pmgwidgets.in_unit_test():  # 如果不在单元测试，则切换工具条。
            self.extension_lib.UI.raise_dock_into_view('code_editor')
            self.extension_lib.UI.switch_toolbar('code_editor_toolbar', switch_only=True)

    def slot_cursor_position_changed(self, line, col):
        logger.warning('changed:' + str(time.time() - self._last_cursorpos_requested_time))
        logger.warning(self.cursor_pos_manager.last_value())
        if self.cursor_pos_manager.last_value() is not None:
            if abs(self.cursor_pos_manager.last_value()[
                       1] - line) > 5 and time.time() - self._last_cursorpos_requested_time > 0.1:
                current_path = self.currentWidget().path()
                self.cursor_pos_manager.push((current_path, line, col))
        else:
            current_path = self.currentWidget().path()
            self.cursor_pos_manager.push((current_path, line, col))

    def slot_goto_last_cursor_pos(self):
        logger.warning(self.cursor_pos_manager.content + self.cursor_pos_manager.pointer)
        last_pos_result: Tuple[str, int, int] = self.cursor_pos_manager.undo()
        self._last_cursorpos_requested_time = time.time()
        if last_pos_result is not None:
            if last_pos_result[1] == self.currentWidget().textEdit.getCursorPosition()[0]:
                last_pos_result = self.cursor_pos_manager.undo()
                if last_pos_result is None:
                    return
            self.on_gotoline_requested(*last_pos_result)

    def slot_goto_next_cursor_pos(self):
        """
        前往下一个指针位置。
        Returns:

        """
        next_pos_result: Tuple[str, int, int] = self.cursor_pos_manager.redo()
        self._last_cursorpos_requested_time = time.time()
        if next_pos_result is not None:
            if next_pos_result[1] == self.currentWidget().textEdit.getCursorPosition()[0]:
                next_pos_result = self.cursor_pos_manager.redo()
                if next_pos_result is None:
                    return
            self.on_gotoline_requested(*next_pos_result)

    def on_gotoline_requested(self, file_path: str, line_no: int, col_no: int = 0, count_from_zero=True):
        """
        前往某个文件的某个位置。
        Args:
            file_path:
            line_no:
            col_no:
            count_from_zero:

        Returns:

        """
        self.slot_new_script(file_path)
        current_widget = self.currentWidget()
        if count_from_zero:
            current_widget.goto_line(line_no + 1)
        else:
            current_widget.goto_line(line_no)

    def slot_open_script(self):
        """
        弹出对话框选择文件

        :return:
        """
        path, _ = QFileDialog.getOpenFileName(self, self.tr('Open File'), self.extension_lib.Program.get_work_dir(),
                                              filter='*.py')
        if not path or not os.path.exists(path):
            return

        self.slot_new_script(path)

    def slot_search_for_file(self):
        """
        搜索文件内容

        :return:
        """

    def slot_clipboard(self):
        """
        剪贴板操作

        :return:
        """

    def slot_print(self):
        """
        打印预览以及打印

        :return:
        """

    def slot_search(self):
        """
        文本查找

        :return:
        """
        self.currentWidget().slot_find()

    def slot_replace(self):
        """
        文本替换

        :return:
        """

    def slot_goto_file(self, path: str, row: int, col: int):
        """
        goto file
        :return
        """
        t0 = time.time()
        self.slot_new_script(path)
        current_widget = self.currentWidget()
        current_widget.goto_line(row)
        logger.warning('goto file %s ,line %d' % (path, row))
        t1 = time.time()
        print('time elapsed:', t1 - t0)

    def slot_goto(self):
        """
        跳转到指定行

        :return:
        """
        self.currentWidget().slot_goto_line()

    def slot_indent(self):
        """
        批量缩进
        (实际上连接的是同一个函数)
        :return:
        """
        self.get_current_editor().text_edit.on_tab()

    def slot_unindent(self):
        """
        取消缩进

        :return:
        """
        self.get_current_editor().text_edit.on_back_tab()

    def slot_check_code(self, force_update=False):
        """
        代码检查

        :return:
        """

        if not self._thread_check:
            return
        widget = self.currentWidget()
        if not isinstance(widget, PMPythonEditor):
            # 目前暂时支持python代码检测
            return
        code = self.get_current_text()  # .strip() is not needed because there should be a empty line at the end of file
        if (not code or code == self._old_code) and (not force_update):
            return
        self._old_code = code
        self._worker_check.add(widget, code)

    @staticmethod
    def slot_checked_code(widget, msgs):
        """
        代码检测更新

        :param widget: 目标编辑器
        :param msgs: 提示信息
        :return:
        """
        widget.set_indicators(msgs, True)

    def slot_toggle_comment(self):
        self.get_current_editor().text_edit.comment()

    def slot_run_script(self, code: str = '', hint: str = ''):
        """
        执行文件

        :return:
        """
        if isinstance(self.currentWidget(), PMPythonEditor):
            if not code:
                code = self.get_current_text(True)
                if not code:
                    code = self.get_current_text()
            code = code.strip()

            if hint == '':
                hint = self.tr(
                    'Run: %s') % self.get_current_filename()
        elif isinstance(self.currentWidget(), PMMarkdownEditor):
            code = self.currentWidget().get_code()
            code = code.strip()
            if hint == '':
                hint = self.tr(
                    'Run Python Code inside %s') % self.get_current_filename()
        else:
            return
        if not code:
            return
        if not in_unit_test():
            self.extension_lib.get_interface('ipython_console').run_command(command=code, hint_text=hint, hidden=False)
        else:
            logger.info('In Unit test at method \'slot_run_script\'.code is :\n%s,\nhint is :%s' % (code, hint))

    def slot_run_sel(self, sel_text):
        """
        运行选中代码片段或光标所在行
        :param sel_text:
        :return:
        """
        self.extension_lib.get_interface('ipython_console').run_command(command=sel_text, hint_text=sel_text,
                                                                        hidden=False)

    def slot_tab_close_request(self, index: int):
        """
        关闭标签页

        :param index: 标签当前索引
        :type index: int
        :return:
        """
        widget = self.widget(index)
        if not widget:
            return
        if self.count() == 1 and not widget._modified() and not widget.text():
            # 不关闭
            return
        if widget.slot_about_close() == QMessageBox.Cancel:
            return
        self.removeTab(index)
        widget.close()
        widget.deleteLater()
        if self.count() == 0:
            self._index = 0
            self.slot_new_script()

    def slot_run_in_terminal(self):
        """
        在终端中运行
        :return:
        """
        editor: 'PMPythonEditor' = self.currentWidget()
        editor.slot_run_in_terminal()

    def slot_run_isolated(self):
        editor: 'PMPythonEditor' = self.currentWidget()
        path = editor.path()
        self.extension_lib.get_interface('applications_toolbar').create_python_file_process(path,
                                                                                            self._current_executable)

    def run_sys_command(self):
        pass

    def slot_save_current_script(self):
        """
        保存当前脚本
        Returns:

        """
        self.currentWidget().slot_save()

    def _init_signals(self):
        # 标签页关闭信号
        self.tabCloseRequested.connect(self.slot_tab_close_request)
        self.currentChanged.connect(self.on_tab_switched)
        if not in_unit_test():
            try:
                self.extension_lib.UI.get_toolbar_widget('toolbar_home', 'button_new_script').clicked.connect(
                    self.slot_new_script)
                # self.extension_lib.UI.get_toolbar('toolbar_home').append_menu(
                #     'button_new', self.tr('Script'), self.slot_new_script)
                self.extension_lib.UI.get_toolbar('toolbar_home').append_menu('button_open', self.tr('Script'),
                                                                              self.slot_open_script)
                # 创建新文档

                self.extension_lib.UI.get_toolbar_widget('code_editor_toolbar', 'button_new_script').clicked.connect(
                    self.slot_new_script)
                # 打开文件
                self.extension_lib.UI.get_toolbar_widget('code_editor_toolbar', 'button_open_script').clicked.connect(
                    self.slot_open_script)
                interpreters = self.extension_lib.Program.get_settings()['external_interpreters']
                interpreter_names = [self.tr('Builtin (3.8.5)')] + [d['name'] for d in interpreters]
                combo_box: QComboBox = self.extension_lib.UI.get_toolbar_widget('code_editor_toolbar',
                                                                                'combobox_interpreter')
                combo_box.addItems(interpreter_names)
                mouse_pressed_evt = combo_box.mousePressEvent

                def mouse_pressed(e):
                    current_selection_text = combo_box.currentText()
                    self.update_interpreter_selections(combo_box)
                    for i in range(combo_box.count()):
                        if combo_box.itemText(i) == current_selection_text:
                            combo_box.setCurrentIndex(i)
                    mouse_pressed_evt(e)

                combo_box.mousePressEvent = mouse_pressed
                combo_box.currentIndexChanged.connect(lambda: self.change_current_interpreter(combo_box.currentIndex()))
                self.update_interpreter_selections(combo_box)
                self.extension_lib.UI.get_toolbar_widget('code_editor_toolbar', 'combobox_interpreter')

                # # 保存脚本
                self.extension_lib.UI.get_toolbar_widget('code_editor_toolbar', 'button_save').clicked.connect(
                    self.slot_save_current_script)
                # 查找内容&替换
                self.extension_lib.UI.get_toolbar_widget('code_editor_toolbar', 'button_search').clicked.connect(
                    self.slot_search)
                # 跳转到行
                self.extension_lib.UI.get_toolbar_widget('code_editor_toolbar', 'button_goto').clicked.connect(
                    self.slot_goto)
                # 批量注释
                self.extension_lib.UI.get_toolbar_widget('code_editor_toolbar', 'button_comment').clicked.connect(
                    self.slot_toggle_comment)
                # 取消注释
                # self.extension_lib.UI.get_toolbar_widget('code_editor_toolbar', 'button_uncomment').clicked.connect(
                #     self.slot_toggle_comment)
                # 增加缩进
                self.extension_lib.UI.get_toolbar_widget('code_editor_toolbar', 'button_indent').clicked.connect(
                    self.slot_indent)
                # 减少缩进
                self.extension_lib.UI.get_toolbar_widget('code_editor_toolbar', 'button_unindent').clicked.connect(
                    self.slot_unindent)
                # 运行代码
                self.extension_lib.UI.get_toolbar_widget('code_editor_toolbar', 'button_run_script').clicked.connect(
                    self.slot_run_script)

                self.extension_lib.UI.get_toolbar_widget('code_editor_toolbar', 'button_run_isolated').clicked.connect(
                    self.slot_run_isolated)

                self.extension_lib.UI.get_toolbar_widget('code_editor_toolbar',
                                                         'button_run_in_terminal').clicked.connect(
                    self.slot_run_in_terminal)

                self.extension_lib.UI.get_toolbar_widget('code_editor_toolbar', 'button_instant_boot').clicked.connect(
                    self.slot_instant_boot)
                # self.extension_lib.UI.get_toolbar_widget('code_editor_toolbar', 'button_debug').clicked.connect(
                #     self.slot_debug)
            except Exception as e:
                import traceback
                traceback.print_exc()
                logger.warning(str(e))

    def slot_instant_boot(self):
        """
        快速启动
        Returns:

        """
        if isinstance(self.get_current_editor(), PMPythonEditor):
            path = self.get_current_editor().path()
            self.extension_lib.get_interface('applications_toolbar').create_instant_boot_python_file_process(path)
        else:
            QMessageBox.warning(self, self.tr('Warning'), self.tr('This Editor does not support instant boot.'))

    def update_interpreter_selections(self, combo: QComboBox):
        """
        刷新所有解释器状态
        Returns:

        """
        interpreters = self.extension_lib.Program.get_settings()['external_interpreters']
        combo.clear()
        combo.addItem(self.tr('Builtin (%s)' % sys.version.split()[0]))
        for interpreter in interpreters:
            combo.addItem(interpreter['name'] + ' (%s)' % interpreter['version'])

    def change_current_interpreter(self, interpreter_index: int):
        """
        切换当前解释器
        Returns:

        """
        if interpreter_index == -1:
            return
        elif interpreter_index == 0:
            self._current_executable = sys.executable
        else:
            print(self.extension_lib.Program.get_settings()['external_interpreters'], interpreter_index)
            self._current_executable = \
                self.extension_lib.Program.get_settings()['external_interpreters'][interpreter_index - 1]['path']

    def on_tab_switched(self, index: int) -> None:
        for i in range(self.count()):
            if i != index:
                w: 'PMBaseEditor' = self.widget(i)
                if hasattr(w, 'find_dialog') and w.find_dialog is not None:
                    w.find_dialog.hide()

    def set_background_syntax_checking(self, checking: bool):
        self._worker_check.background_checking = checking

    def set_smart_autocomp_stat(self, smart_autocomp_on: bool):
        for i in range(self.count()):
            # if i != index:
            w: 'PMBaseEditor' = self.widget(i)
            w.set_smart_autocomp_stat(smart_autocomp_on)

    def update_settings(self, settings: Dict[str, Any]):
        self.settings = settings
        self.set_background_syntax_checking(settings['check_syntax_background'])
        for i in range(self.count()):
            w: 'PMBaseEditor' = self.widget(i)
            w.update_settings(settings)

    def closeEvent(self, event: QCloseEvent) -> None:
        if self._thread_check and self._thread_check.isRunning():
            self._worker_check.stop()
            self._thread_check.quit()
            self._thread_check.wait(500)
        for i in range(self.count()):
            self.widget(i).close()  # TODO:这里结构不行！
        widgets = [self.widget(i) for i in range(self.count()) if self.widget(i).modified()]
        print(widgets)
        if not widgets:
            return
        save_all = False
        for widget in widgets:
            if save_all:
                # 保存全部则直接进入保存文件流程
                widget.slot_save()
                continue

            ret = widget.slot_about_close(True)

            save_all = ret == QMessageBox.SaveAll

    def get_all_breakpoints(self, language='python') -> str:
        if language == 'python':
            breakpoints_str = ''
            for i in range(self.count()):
                editor: PMBaseEditor = self.widget(i)
                if editor.path().endswith('.py'):
                    path = editor.path()
                    break_points = editor.get_all_breakpoints()
                    for bp_line in break_points:
                        breakpoints_str += 'b %s:%d' % (path, bp_line + 1) + '\n'
                    logger.warning('break_points are:' + str(break_points))
            return breakpoints_str

    def currentWidget(self) -> Union[EDITOR_TYPE, QWidget]:
        return super(PMCodeEditTabWidget, self).currentWidget()

    def widget(self, index) -> Union[EDITOR_TYPE, QWidget]:
        return super(PMCodeEditTabWidget, self).widget(index)

    def set_debug_widget(self, debug_widget):
        self.debug_widget = debug_widget

    def slot_debug(self):
        w = self.currentWidget()
        path = w.path()
        if self.debug_widget is not None:
            self.debug_widget.new_debug(os.path.basename(path), path, self)
            self.extension_lib.UI.raise_dock_into_view('debugger')

    def get_widget_text(self) -> str:
        return self.tr('Editor')

    def slot_find_in_path(self, word: str):
        from pmgwidgets.widgets.basic.texts.simpleeditor.ui.findinpath import FindInPathWidget
        path = self.extension_lib.Program.get_work_dir()
        if not self.extension_lib.UI.widget_exists('find_in_path'):
            w: FindInPathWidget = self.extension_lib.insert_widget(
                FindInPathWidget, 'new_dock_window',
                {
                    "name": "find_in_path",
                    "side": "bottom",
                    "text": self.tr("Find In Path")
                }
            )
            w.set_word(word)
            w.signal_open_file_line.connect(lambda path, row: self.slot_goto_file(path, row + 1, 0))
            self._find_in_path_widget = w
        self._find_in_path_widget.set_path(path)
        self.extension_lib.UI.raise_dock_into_view('find_in_path')

    def slot_focused_in(self, e):
        if not in_unit_test():
            self.extension_lib.UI.switch_toolbar('code_editor_toolbar', switch_only=True)

    def set_color_scheme(self, scheme: str):
        self._color_scheme = scheme
        if scheme == 'dark':
            PythonHighlighter.font_cfg.load_color_scheme(
                {'keyword': '#b7602f', 'normal': '#a8b4c2'}
            )
        else:
            PythonHighlighter.font_cfg.load_color_scheme({'keyword': '#101e96', 'normal': '#000000'})

        # for editor_index in range(self.count()):
        #     self.widget(editor_index).change_color_scheme(scheme)


if __name__ == '__main__':
    import sys
    import cgitb
    import logging

    cgitb.enable(format='text')
    logging.basicConfig(level=logging.INFO)

    app = QApplication(sys.argv)
    # app.setStyleSheet("""
    # PMBaseEditor {
    #     qproperty-theme: "Material-Dark";
    # }
    # """)

    w = PMCodeEditTabWidget()
    w.show()
    w.set_color_scheme('dark')
    w.setMinimumWidth(800)
    w.setMinimumHeight(600)
    w.setup_ui()
    code_editor_root_directory = os.path.dirname(__file__)
    # w.slot_new_script(
    #     '/home/hzy/Desktop/create_dataset.py')  # os.path.join(code_editor_root_directory, 'tests', 'test_file.py'))
    w.slot_new_script(r'C:/Users/12957/documents/developing/Python/pyminer_workdir/app_designer.py')
    w.currentWidget().goto_line(5)
    # w.on_work_dir_changed(r'C:\Users\12957\AppData')
    # w.slot_new_script(r'c:/users/12957/desktop/test.md')
    # w.on_work_dir_changed('c:/users/12957/desktop')
    sys.exit(app.exec_())
