diff --git a/features/extensions/extensions_manager/ExtensionLoader.py b/features/extensions/extensions_manager/ExtensionLoader.py index d4ab465599ac83043fb67e4954ea5adc537774db..d7c9aa5089dc42f3e2066d12c74c72f08424cdc7 100644 --- a/features/extensions/extensions_manager/ExtensionLoader.py +++ b/features/extensions/extensions_manager/ExtensionLoader.py @@ -164,6 +164,7 @@ class ExtensionLoader: def load_class(self, file, class_name): path = os.path.join(self.path, file) + # TODO 这里的设置将导致奇怪的错误,即模块不再为单例模式,而这是Python中的一个重要特性 module = self.import_module(path) if module: if hasattr(module, class_name): diff --git a/packages/code_editor/assets/languages/en.ts b/packages/code_editor/assets/languages/en.ts index 0a874eaee8cdc3b2a9a73234d4442ddc698130c3..b34577a5a4794c95257857a77da6da7203a726f1 100644 --- a/packages/code_editor/assets/languages/en.ts +++ b/packages/code_editor/assets/languages/en.ts @@ -29,79 +29,97 @@ - PMBaseEditor + PMBaseCodeEdit - + Format Code - + Run Code - + Run Selected Code - - save.svg + + Save Code - - Find + + Find Code - + Replace - + Find In Path - + AutoComp - + Goto Line - + + Goto Definition + + + + + Function Help + + + + + Help in Console + + + + Add Breakpoint - + Remove Breakpoint - + View BreakPoints + + + PMBaseEditor - + Save - + Save file "{0}"? - + Save file @@ -330,27 +348,12 @@ PMPythonEditor - - Function Help - - - - - Help In Console - - - - - Go to Definition - - - - + Help - + Cannot get name. Maybe There is: 1、Syntax error in your code. @@ -358,7 +361,7 @@ Maybe There is: - + Error diff --git a/packages/code_editor/assets/languages/zh_CN.qm b/packages/code_editor/assets/languages/zh_CN.qm index 8f65a357a42b5cb76a1288a6d457a2400fc285d4..7f5950d5ffa96412ee63e191c5f4cd0ba7624149 100644 Binary files a/packages/code_editor/assets/languages/zh_CN.qm and b/packages/code_editor/assets/languages/zh_CN.qm differ diff --git a/packages/code_editor/assets/languages/zh_CN.ts b/packages/code_editor/assets/languages/zh_CN.ts index c11ed114879a24621dec984ea2108800d008e938..09ca3f8809135c690b242a982149e7ee51087d55 100644 --- a/packages/code_editor/assets/languages/zh_CN.ts +++ b/packages/code_editor/assets/languages/zh_CN.ts @@ -5,27 +5,105 @@ Case - + 匹配大小写 Whole Word - + 全词匹配 Find - + 查找 Find In Path - + 在路径中查找 line - + 行号 + + + + PMBaseCodeEdit + + + Format Code + 代码格式化 + + + + Run Code + 运行代码 + + + + Run Selected Code + 运行选中的代码 + + + + Save Code + 保存代码 + + + + Find Code + 查找代码 + + + + Replace + 替换 + + + + Find In Path + 在路径中查找 + + + + AutoComp + 自动补全 + + + + Goto Line + 跳转到行 + + + + Goto Definition + 转到定义 + + + + Function Help + 函数帮助 + + + + Help in Console + 在控制台中显示帮助 + + + + Add Breakpoint + 添加断点 + + + + Remove Breakpoint + 移除断点 + + + + View BreakPoints + 查看所有断点 @@ -33,77 +111,77 @@ Format Code - 代码格式化 + 代码格式化 Run Code - 运行代码 + 运行代码 Run Selected Code - 运行选中的代码 + 运行选中的代码 - save.svg - + Save Code + 保存代码 - Find - + Find Code + 查找代码 Replace - + 替换 Find In Path - + 在路径中查找 AutoComp - + 自动补全 Goto Line - + 跳转到行 Add Breakpoint - + 添加断点 Remove Breakpoint - + 移除断点 View BreakPoints - + 查看所有断点 - + Save - + 保存 - + Save file "{0}"? - + 保存文件“{0}”? - + Save file - + 保存文件 @@ -111,58 +189,59 @@ Warning - + 警告 Editor does not support file: %s - + 编辑器不支持的文件类型: +%s Open File - + 打开文件 Run: %s - + 运行:%s Run Python Code inside %s - + 在%s中运行Python代码 Script - + 脚本 Builtin (3.8.5) - + 内置(3.8.5) This Editor does not support instant boot. - + 本编辑器不支持立即启动。 Builtin (%s) - + 内置(%s) Editor - + 编辑器 Find In Path - + 在路径中查找 @@ -170,17 +249,17 @@ Debugger - + 调试器 Terminate - + 中止 Process is running, Would you like to terminate this process? - + 进程正在运行,是否想要结束进程? @@ -188,72 +267,72 @@ New Script - + 新脚本 Open Script - + 打开脚本 Save - + 保存 Find - + 查找 Toggle Comment - + 切换注释 Goto Line - + 跳转到行 Indent - + 缩进 Dedent - + 撤销缩进 IPython - + IPython Separately - + 分割 Terminal - + 终端 Instant Boot - + 立即启动 Run script with common module preloaded to shorten interpterter startup-time. - + 使用通用预加载模块来运行脚本以加快启动速度。 Editor - + 编辑器 @@ -261,52 +340,52 @@ Text to Find - + 要查找的文本 Text to Replace - + 要替换的文本 Wrap - + Wrap Regex - + 正则 Case Sensitive - + 匹配大小写 Whole Word - + 全词 Up - + 向上 Down - + 向下 Replace - + 替换 Replace All - + 替换所有 @@ -314,53 +393,67 @@ Input Value Error - + 入参错误 Cannot convert '%s' to integer. - + 无法将“%s”转化为整数。 Line Number {line} out of range! - + 行号{line}超出范围! - + PMPythonEditor + + + Go to Definition + 转到定义 + + + + Help + 帮助 + Function Help - + 函数帮助 Help In Console - + 在控制台中显示帮助 - - Go to Definition - - - - - Help - - - Cannot get name. Maybe There is: -1、Syntax error in your code. -2、No word under text cursor. - +1、Syntax error in your code. +2、No word under text cursor. + 无法获取名称。 +可能出现的问题: +1、语法错误; +2、光标下没有文本。 - + Error - + 错误 + + + + Cannot get name. +Maybe There is: +1、Syntax error in your code. +2、No word under text cursor. + 无法获取名称。 +可能的问题: +1、代码中存在语法错误; +2、游标下没有单词。 diff --git a/packages/code_editor/assets/languages/zh_TW.ts b/packages/code_editor/assets/languages/zh_TW.ts index 0a874eaee8cdc3b2a9a73234d4442ddc698130c3..b34577a5a4794c95257857a77da6da7203a726f1 100644 --- a/packages/code_editor/assets/languages/zh_TW.ts +++ b/packages/code_editor/assets/languages/zh_TW.ts @@ -29,79 +29,97 @@ - PMBaseEditor + PMBaseCodeEdit - + Format Code - + Run Code - + Run Selected Code - - save.svg + + Save Code - - Find + + Find Code - + Replace - + Find In Path - + AutoComp - + Goto Line - + + Goto Definition + + + + + Function Help + + + + + Help in Console + + + + Add Breakpoint - + Remove Breakpoint - + View BreakPoints + + + PMBaseEditor - + Save - + Save file "{0}"? - + Save file @@ -330,27 +348,12 @@ PMPythonEditor - - Function Help - - - - - Help In Console - - - - - Go to Definition - - - - + Help - + Cannot get name. Maybe There is: 1、Syntax error in your code. @@ -358,7 +361,7 @@ Maybe There is: - + Error diff --git a/packages/code_editor/assets/translations/qt_zh_CN.qm b/packages/code_editor/assets/translations/qt_zh_CN.qm deleted file mode 100644 index 9c2a84bec977655d28e061a6a60479065c3bfd3b..0000000000000000000000000000000000000000 Binary files a/packages/code_editor/assets/translations/qt_zh_CN.qm and /dev/null differ diff --git a/packages/code_editor/assets/translations/qt_zh_CN.ts b/packages/code_editor/assets/translations/qt_zh_CN.ts deleted file mode 100644 index 42d01c2def27d5fee5974a5469799fcd4d4b9724..0000000000000000000000000000000000000000 --- a/packages/code_editor/assets/translations/qt_zh_CN.ts +++ /dev/null @@ -1,459 +0,0 @@ - - - - - PMGotoLineDialog - - - Go to Line/Column - 前往行/列 - - - - [Line] [:column]: - [行] [:列]: - - - - PMFindDialog - - - Text to Find - 要查找的文本 - - - - Text to Replace - 替换为 - - - - Wrap - 循环查找 - - - - Regex - 匹配正则表达式 - - - - Case Sensitive - 大小写敏感 - - - - Whole Word - 匹配整个文字 - - - - Up - 向上 - - - - Down - 向下 - - - - Replace - 替换 - - - - Replace All - 替换全部 - - - - FindInPathWidget - - - Case - 大小写敏感 - - - - Whole Word - 匹配整个文字 - - - - Find - 查找 - - - - Find In Path - 在路径中查找 - - - - FormEditor - - - Length:{0} Lines:{1} - 长度:{0} 行{1} - - - - UTF-8 - UTF-8 - - - - Sel:{0} | {1} - 选中区域:{0} | {1} - - - - Ln:{0} Col:{1} - 行:{0} 列:{1} - - - - Unix(LF) - Unix(LF) - - - - Form - - - - - PMBaseEditor - - - Save - 保存 - - - - Save file "{0}"? - 是否保存文件 "{0}"? - - - - PMBaseEditor - - - Ln:{0} Col:{1} - 行:{0} 列:{1} - - - - Ln:1 Col:1 - 行:1 列:1 - - - - Length:0 Lines:1 - 长度:0 行数:1 - - - - Sel:0 | 0 - 选中:0|0 - - - - Format Code - 格式化代码 - - - - Run Code - 运行代码 - - - - Run Selected Code - 运行选中代码 - - - - Save - 保存 - - - - Find/Replace - 查找/替换 - - - - Find In Path - 在路径中查找 - - - - AutoComp - 自动补全 - - - - Add Breakpoint - 添加断点 - - - - Remove Breakpoint - 移除断点 - - - - View BreakPoints - 查看断点 - - - - Save file "{0}"? - 是否保存文件“{0}”? - - - - Length:{0} Lines:{1} - 长度:{0} 行{1} - - - - Save file - 保存文件 - - - - Sel:{0} | {1} - 选中区域:{0} | {1} - - - - File Modified - 文件已经更改 - - - - PMCPPEditor - - - Function Help - 获取函数帮助 - - - - PMCodeEditTabWidget - - - Open File - 打开文件 - - - - Run: %s - 运行:%s - - - - Run Python Code inside %s - 运行 %s 中的代码 - - - - Script - 脚本 - - - - Editor - 编辑器 - - - - Builtin (3.8.5) - 内置解释器(3.8.5) - - - - Warning - 警告 - - - - This Editor does not support instant boot. - 当前编辑器不支持快速启动脚本的运行。 - - - - Builtin (%s) - 内置(%s) - - - - Find In Path - 在路径中查找 - - - - PMCythonEditor - - - Compile to Library - 编译为运行库 - - - - Analyse Profile - 生成代码分析报告 - - - - PMDebugConsoleTabWidget - - - Terminate - 终止 - - - - Process is running, Would you like to terminate this process? - 进程正在运行,是否要终止此进程? - - - - Debugger - 调试器 - - - - PMEditorToolbar - - - New Script - 新建脚本 - - - - Open Script - 打开脚本 - - - - Save - 保存 - - - - Find/Replace - 查找/替换 - - - - Toggle Comment - 切换注释 - - - - Goto Line - 跳转到行 - - - - Indent - 增加缩进 - - - - Dedent - 减少缩进 - - - - IPython - IPython运行 - - - - Separately - 独立运行 - - - - Terminal - 终端中运行 - - - - Editor - 编辑器 - - - - Instant Boot - 快速运行 - - - - Run script with common module preloaded to shorten interpterter startup-time. - 以预加载模块的方式运行当前脚本,从而大大缩减代码冷启动时间。 - - - - PMMarkdownEditor - - - Save - 保存 - - - - Save file "{0}"? - 是否保存文件 "{0}"? - - - - PMPythonEditor - - - Function Help - 获取函数帮助 - - - - Help In Console - 控制台中获取帮助 - - - - Go to Definition - 跳转到定义 - - - - Help - 获取帮助 - - - - Cannot get name. -Maybe There is: -1、Syntax error in your code. -2、No word under text cursor. - 无法获取名称。 -可能有以下错误: -1、代码中有语法错误 -2、指针下没有文字。 - - - - Error - 错误 - - - - Running Current Script Cell (Line %d to %d). - 运行当前的脚本单元(%d行到%d行)。 - - - diff --git a/packages/code_editor/code_handlers/__init__.py b/packages/code_editor/code_handlers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/packages/code_editor/code_handlers/base_handler.py b/packages/code_editor/code_handlers/base_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..6326d3e03eb4fc61c651ff0e6532067bb76261c0 --- /dev/null +++ b/packages/code_editor/code_handlers/base_handler.py @@ -0,0 +1,82 @@ +from functools import cached_property +from typing import Tuple, Type, List, Optional + +from PySide2.QtWidgets import QMessageBox + +from packages.code_editor.utils.base_object import CodeEditorBaseObject + + +class BaseAnalyzer(CodeEditorBaseObject): + """ + 在每次进行代码分析的时候都创建一遍这个对象。 + 这个对象的好处是所有的属性计算都是惰性的,按需计算,降低性能损耗。 + """ + + def __init__(self, code: str, cursor: int, selection_range: Optional[Tuple[int, int]] = None): + self.code: str = code + self.cursor: int = cursor + self.selection_range: Tuple[int, int] = selection_range if selection_range is not None else (cursor, cursor) + + @cached_property + def has_selection(self): + return self.selection_range[0] != self.selection_range[1] + + @cached_property + def lines(self) -> List[str]: + return self.code.split('\n') + + @cached_property + def current_line_index(self) -> int: + """行的位置,用于进行索引,从0开始""" + return self.code[:self.cursor].count('\n') + + @cached_property + def current_line_number(self) -> int: + """行号,用于进行显示,从1开始""" + return self.current_line_index + 1 + + @cached_property + def selected_code(self): + if self.has_selection: + return self.code[self.selection_range[0]:self.selection_range[1]] + else: + return self.lines[self.current_line_index] + + +class BaseHandler(CodeEditorBaseObject): + """ + 代码的执行、格式化、分析等所有工作都应写在这个类及其子类下。 + + 这里相当于是界面的后端,所有的对代码的操作都应该放在这里。 + """ + analyzer_class: Type[BaseAnalyzer] = BaseAnalyzer + analyzer: BaseAnalyzer = None + + def feed(self, code: str, position: int, selection_range: Tuple[int, int]): + """输入代码,以用于分析等操作 + + 这个更新的方式为增量更新,仅当参数与上一次的参数不一致时,才创建新的analyzer对象。 + + Args: + code: 代码,应该是plainText + position: 游标的位置,是一个整数,而不是行列号 + selection_range: 选区的位置,是一对整数,表示起止位置,而不是行列号 + """ + if (a := self.analyzer) is not None: + if (code, position, selection_range) == (a.code, a.cursor, a.selection_range): + return + self.analyzer = self.analyzer_class(code, position, selection_range) + + def __not_implemented_error(self, name): + title = self.tr('Not Implemented Error') + message = self.tr('{} is not implemented').format(name) + QMessageBox.critical(None, title, message) + + def run_code(self, code: str, hint: str = 'run code'): + """运行一段代码 + + Args: + code: 代码 + hint: 代码的标题 + """ + self.__not_implemented_error(self.tr('run code')) diff --git a/packages/code_editor/code_handlers/python_handler.py b/packages/code_editor/code_handlers/python_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..9401f03fbf67435fd97633c07e7dc13e9f8ecc62 --- /dev/null +++ b/packages/code_editor/code_handlers/python_handler.py @@ -0,0 +1,18 @@ +from functools import cached_property +from typing import TYPE_CHECKING + +from packages.code_editor.code_handlers.base_handler import BaseHandler + +if TYPE_CHECKING: + from packages.ipython_console.main import ConsoleInterface + + +class PythonHandler(BaseHandler): + @cached_property + def ipython_console(self) -> 'ConsoleInterface': + return self.extension_lib.get_interface('ipython_console') + + def run_code(self, code: str, hint: str = ''): + if hint == '': + hint = self.tr('Run code') + self.ipython_console.run_command(command=code, hint_text=hint, hidden=False) diff --git a/packages/code_editor/main.py b/packages/code_editor/main.py index 985b0ccb2adb76dd12722340ecfe6fc8c2ae5d81..2458886c06b6a2ac0a52595230e800204c5a56de 100644 --- a/packages/code_editor/main.py +++ b/packages/code_editor/main.py @@ -1,11 +1,7 @@ import logging import os -from pathlib import Path from typing import Dict, Union, TYPE_CHECKING -from PySide2.QtCore import QLocale, QTranslator -from PySide2.QtWidgets import QApplication - if TYPE_CHECKING: from packages.file_tree.main import Interface as FileTreeInterface @@ -18,11 +14,6 @@ from pmgwidgets import PMGPanel, load_json, dump_json __prevent_from_ide_optimization = PMEditorToolbar # 这一行的目的是防止导入被编辑器自动优化。 logger = logging.getLogger('code_editor') -# 翻译文件只需要加载一次 -__translator = QTranslator() -__translator.load(str(Path(__file__).parent / 'assets' / 'translations' / f'qt_{QLocale.system().name()}.qm')) -QApplication.instance().installTranslator(__translator) - class Extension(BaseExtension): editor_widget: 'PMCodeEditTabWidget' diff --git a/packages/code_editor/scripts/translation/README.md b/packages/code_editor/scripts/translation/README.md index e05b9815f5f9ce39abf7b7b0d16a5fb9e17751c2..d77c261f6786789cd165dd69312dcd1509de433e 100644 --- a/packages/code_editor/scripts/translation/README.md +++ b/packages/code_editor/scripts/translation/README.md @@ -2,15 +2,12 @@ 出于开发时的简洁性,使用英文进行开发,而后需要译回中文。 -主要包括以下脚本: +翻译生成工具只有一个脚本,其主要工作流程如下: -`get_all_python_files.py`,可以获取所有的code_editor中的python文件, -将其输出结果复制到`code_editor.pro`里面,就可以选择需要的python文件了。 +1. 运行`translate.py`,增量生成各个语言的ts文件,以用于翻译; +1. 使用Qt语言家进行翻译,翻译结果直接保存在生成的ts文件内; +1. 执行`translate.py`,将翻译文件生成可以用Qt读取的文件。 -目前没有`ui`文件,因此其相关内容暂时没做。 +这个脚本同时执行了两项功能,即创建ts文件,再将ts文件转换为qm文件。 因为这两个步骤对性能的要求都不高,因此没有进行分别处理。 -执行create_ts_files.bat,可以创建(或更新?)ts文件。 - -使用linguist编辑ts文件,我是借助了PyCharm的External Tools功能实现的。 - -最后编译ts文件至qm文件,使用`release.py`。 \ No newline at end of file +这个脚本仅在Windows下进行过测试,其他平台麻烦其他人协助测试。 \ No newline at end of file diff --git a/packages/code_editor/scripts/translation/code_editor.pro b/packages/code_editor/scripts/translation/code_editor.pro index ee4728f491af28ec1401357c29fc3734ebf8887a..07719d69aa23c9eab68d2e0d7bd115d1a4e3d7c9 100644 --- a/packages/code_editor/scripts/translation/code_editor.pro +++ b/packages/code_editor/scripts/translation/code_editor.pro @@ -45,7 +45,4 @@ TRANSLATIONS = ..\..\assets\languages\en.ts \ ..\..\assets\languages\zh_TW.ts CODECFORTR = UTF-8 -CODECFORSRC = UTF-8 - -# pylupdate5.exe pyminer.pro -# linguist.exe languages\en\en.ts languages\zh_CN\zh_CN.ts languages\zh_TW\zh_TW.ts \ No newline at end of file +CODECFORSRC = UTF-8 \ No newline at end of file diff --git a/packages/code_editor/scripts/translation/code_editor.pro_t b/packages/code_editor/scripts/translation/code_editor.pro_t new file mode 100644 index 0000000000000000000000000000000000000000..f65f1d8ea3703286c8eed56551b99e5efebafb84 --- /dev/null +++ b/packages/code_editor/scripts/translation/code_editor.pro_t @@ -0,0 +1,7 @@ +SOURCES = {{ python_files }} +TRANSLATIONS = ..\..\assets\languages\en.ts \ + ..\..\assets\languages\zh_CN.ts \ + ..\..\assets\languages\zh_TW.ts + +CODECFORTR = UTF-8 +CODECFORSRC = UTF-8 \ No newline at end of file diff --git a/packages/code_editor/scripts/translation/create_ts_files.py b/packages/code_editor/scripts/translation/create_ts_files.py deleted file mode 100644 index 2a76e6a484a67886b3ad7fb24d1cd33d9c8d59c4..0000000000000000000000000000000000000000 --- a/packages/code_editor/scripts/translation/create_ts_files.py +++ /dev/null @@ -1,15 +0,0 @@ -import os -from pathlib import Path - -import PySide2 - - -def main(): - exe = Path(PySide2.__file__).parent / 'pyside2-lupdate.exe' - pro = Path(__file__).absolute().parent / 'code_editor.pro' - content = os.popen(f'"{exe}" "{pro}"').read() - print(content) - - -if __name__ == '__main__': - main() diff --git a/packages/code_editor/scripts/translation/release.py b/packages/code_editor/scripts/translation/release.py deleted file mode 100644 index d92c5f149dd221f2f0c04834d47706df094cd9c6..0000000000000000000000000000000000000000 --- a/packages/code_editor/scripts/translation/release.py +++ /dev/null @@ -1,30 +0,0 @@ -import io -import os -import subprocess -from io import BytesIO -from pathlib import Path - -import PySide2 -import chardet - - -def main(): - base_path = Path(__file__).absolute().parent.parent.parent - print(base_path) - exe = Path(PySide2.__file__).parent / 'lrelease.exe' - ts_dir = base_path / 'assets' / 'languages' - for file in ts_dir.glob('*.ts'): - output = file.parent / f'{file.stem}.qm' - result = subprocess.run(args=[exe, f'{file}', '-qm', output], capture_output=True) - if stdout := result.stdout: - print('Out: ') - stdout = stdout.decode(chardet.detect(result.stdout)['encoding']) - print(stdout) - if stderr := result.stderr: - print('Error: ') - stderr = stderr.decode(chardet.detect(result.stderr)['encoding']) - print(stderr) - - -if __name__ == '__main__': - main() diff --git a/packages/code_editor/scripts/translation/get_all_python_files.py b/packages/code_editor/scripts/translation/translate.py similarity index 31% rename from packages/code_editor/scripts/translation/get_all_python_files.py rename to packages/code_editor/scripts/translation/translate.py index f292ef8203c5c4b3f8e447cfcb9858279a001e1a..57591bb07061d7eb38006c7bddd82ec19614c8fa 100644 --- a/packages/code_editor/scripts/translation/get_all_python_files.py +++ b/packages/code_editor/scripts/translation/translate.py @@ -1,8 +1,16 @@ import os +import sys from pathlib import Path +import PySide2 +import jinja2 -def main(): +sys.path.insert(1, str(Path(__file__).absolute().parent.parent.parent.parent.parent)) + +from utils.dev.system import system + + +def get_python_files(): base_path = Path(__file__).parent.parent.parent.absolute() python_files = [] for root, sub_dirs, files in os.walk(base_path): @@ -15,7 +23,33 @@ def main(): continue file = os.path.join('..', '..', file.relative_to(base_path)) python_files.append(file) - print('\\\n '.join(python_files)) + return python_files + + +def main(): + # 生成pro文件 + # TODO 由于目前没有ui文件,因此没有考虑ui文件 + template = Path(__file__).absolute().parent / 'code_editor.pro_t' + with open(template, 'r', encoding='utf-8') as f: + template_content = f.read() + python_files = get_python_files() + python_files = '\\\n '.join(python_files) + pro_content = jinja2.Template(template_content).render(python_files=python_files) + pro = Path(__file__).absolute().parent / 'code_editor.pro' + with open(pro, 'w', encoding='utf-8') as f: + f.write(pro_content) + + # 使用lupdate生成ts文件 + exe = Path(PySide2.__file__).parent / 'pyside2-lupdate.exe' + system(exe, pro) + + # 将ts文件转换为qm文件 + base_path = Path(__file__).absolute().parent.parent.parent + exe = Path(PySide2.__file__).parent / 'lrelease.exe' + ts_dir = base_path / 'assets' / 'languages' + for file in ts_dir.glob('*.ts'): + output = file.parent / f'{file.stem}.qm' + system(exe, f'{file}', '-qm', output) if __name__ == '__main__': diff --git a/packages/code_editor/utils/base_object.py b/packages/code_editor/utils/base_object.py index 4bfe0169ec13bd2e03a1093d6cc54bfc47bf8582..b026790ef8d5db20bf5a4780d3a2406bbeaf6a70 100644 --- a/packages/code_editor/utils/base_object.py +++ b/packages/code_editor/utils/base_object.py @@ -1,10 +1,53 @@ -from typing import TYPE_CHECKING, Callable +from functools import cached_property +from pathlib import Path +from typing import Optional, Callable, TYPE_CHECKING +from PySide2.QtCore import QTranslator, QLocale + +from features.extensions.extensionlib import extension_lib from ..settings import Settings +if TYPE_CHECKING: + from features.extensions.extensionlib.extension_lib import ExtensionLib + + +def _get_translator() -> Optional[QTranslator]: + result = QTranslator() + language = QLocale.system().name() + file = Path(__file__).parent.parent / 'assets' / 'languages' / f'{language}.qm' + if file.exists(): + result.load(str(file)) + return result + else: + return None + class CodeEditorBaseObject: + # 由于插件内为独立的模块加载,其命名空间不共享,因此暂时只能采用这种方式实现全局的extension_lib读取 + extension_lib: 'ExtensionLib' = extension_lib settings = Settings() - if TYPE_CHECKING: - tr: Callable[[str], str] + __translator = _get_translator() + + @cached_property + def tr(self) -> Callable[[str], str]: + """使用属性的方式返回一个运行时生成的函数。 + + 这个函数可以实现自动判断Context以查找对应的翻译。 + 这个函数旨在解决的问题是,PySide2默认情况下采用静态编码的方式生成翻译,而后Python在运行时由于继承,类名不再是self.tr所在的类名, + 因此translator无法找到相对应的翻译。 + + 使用cached property以实现MRO查找等性能的节省。 + """ + klass_names = [klass.__name__ for klass in self.__class__.mro()] + if self.__translator is None: + return lambda source: source + else: + translate: Callable[[str, str], str] = self.__translator.translate + + def wrapper(source: str): + translations = [translate(klass, source) for klass in klass_names] + translations = [t for t in translations if t] + return translations[0] if translations else source + + return wrapper diff --git a/packages/code_editor/utils/operation.py b/packages/code_editor/utils/operation.py index 13c26caec782f76532bdf4644e34fe400eea0d4d..996de192664f43c9e22453ee797f73e692134899 100644 --- a/packages/code_editor/utils/operation.py +++ b/packages/code_editor/utils/operation.py @@ -33,11 +33,12 @@ class Operation(CodeEditorBaseObject): # 设置Action的图标 if icon_name is None: - icon = None + # 用于进行缓存,以免每次都要重新创建一个action对象 + self.__action: QAction = QAction(label, widget) else: icon = QIcon(self.settings.get_icon(icon_name)) - self.__action: QAction = QAction(icon, label, widget) # 用于进行缓存,以免每次都要重新创建一个action对象 - # noinspection PyUnresolvedReferences + self.__action: QAction = QAction(icon, label, widget) + # noinspection PyUnresolvedReferences has_slot and self.__action.triggered.connect(slot) # 设置QAction和QShortcut快捷键 diff --git a/packages/code_editor/widgets/dialogs/find_dialog.py b/packages/code_editor/widgets/dialogs/find_dialog.py index d688be9880dc83a23092cdb0bc386ffd404e0ab3..79685eb4febe23c7c74f36e0513e428a1b75f209 100644 --- a/packages/code_editor/widgets/dialogs/find_dialog.py +++ b/packages/code_editor/widgets/dialogs/find_dialog.py @@ -80,8 +80,8 @@ class PMFindDialog(QDialog): def show(self) -> None: super().show() - if self.text_edit.get_selected_text() != '': - self.settings_panel.set_value({'text_to_find': self.text_edit.get_selected_text()}) + if self.text_edit.selected_code != '': + self.settings_panel.set_value({'text_to_find': self.text_edit.selected_code}) def show_replace_actions(self, replace_on: bool = False): self.settings_panel.get_ctrl('text_to_replace').setVisible(replace_on) diff --git a/packages/code_editor/widgets/editors/base_editor.py b/packages/code_editor/widgets/editors/base_editor.py index 926f8a04d1d317325917927fa710e04febf88f5c..858d3da5d5491069ed234dd39a288ddf805fd2d5 100644 --- a/packages/code_editor/widgets/editors/base_editor.py +++ b/packages/code_editor/widgets/editors/base_editor.py @@ -31,17 +31,15 @@ import os import time from typing import Dict, Callable, TYPE_CHECKING, Type -from PySide2.QtCore import SignalInstance, Signal, QDir, QCoreApplication, QPoint +from PySide2.QtCore import SignalInstance, Signal, QDir from PySide2.QtGui import QTextDocument, QTextCursor -from PySide2.QtWidgets import QWidget, QMessageBox, QLabel, QVBoxLayout, QFileDialog, \ - QMenu +from PySide2.QtWidgets import QWidget, QMessageBox, QLabel, QVBoxLayout, QFileDialog from features.extensions.extensionlib.extension_lib import ExtensionLib from ..dialogs.find_dialog import PMFindDialog from ..dialogs.goto_line_dialog import PMGotoLineDialog from ..text_edit.base_text_edit import PMBaseCodeEdit from ...utils.base_object import CodeEditorBaseObject -from ...utils.operation import Operation from ...utils.utils import decode logger = logging.getLogger(__name__) @@ -49,10 +47,8 @@ logger = logging.getLogger(__name__) class PMBaseEditor(CodeEditorBaseObject, QWidget): """ - 编辑器的各种操件应当由这个类及其子类进行管理,包括代码重构、代码缩进等内容。 - 快捷键也应当定义在这个类中。 - 这个类需要调用其text_edit属性提供的各种方法来实现对编辑器的操作。 - 关于具体的代码缩进调整等仅与代码文本有关的内容应当定义在TextEdit里面。 + 这个类仅作为布局管理一些辅助内容,例如显示行号、列号等内容。 + 所有实际的代码操作都应写在TextEdit里面。 """ # 用于子类继承时的配置项 @@ -152,90 +148,6 @@ class PMBaseEditor(CodeEditorBaseObject, QWidget): self.text_edit.setTextCursor(cursor) return ret - def _init_signals(self) -> None: - """初始化信号绑定""" - - # 绑定右键菜单信号 - self.text_edit.customContextMenuRequested.connect(self.slot_custom_context_menu_requested) - - def _init_actions(self) -> None: - """初始化快捷键和菜单项""" - - def text_exists(): - """判断是否有文本,如果有文本才允许使用自动排版等功能""" - return len(self.text().strip()) > 0 - - # 格式化代码 - self.__operation_format = Operation( - widget=self.text_edit, name='format code', label=self.tr('Format Code'), - slot=self.slot_code_format, key='Ctrl+Alt+F', icon_name='format.svg', conditions=[text_exists], - ) - - # 运行代码 - self.__operation_run_code = Operation( - widget=self.text_edit, name='run code', label=self.tr('Run Code'), - slot=self.slot_code_run, key='Ctrl+R', icon_name='run.svg', conditions=[text_exists], - ) - - # 运行选中代码 - self.__operation_run_selected_code = Operation( - widget=self.text_edit, name='run code', label=self.tr('Run Selected Code'), - slot=self.slot_code_sel_run, key='F9', icon_name='python.svg', conditions=[text_exists], - # TODO 添加判别条件:仅当有文本选中时才可用 - ) - - # 保存代码 - self.__operation_save_code = Operation( - widget=self.text_edit, name='save code', label=self.tr('save.svg'), - slot=self.slot_save, key='Ctrl+S', icon_name='save.svg', - ) - - # 查找代码 - self.__operation_find = Operation( - widget=self.text_edit, name='find code', label=self.tr('Find'), - slot=self.slot_find, key='Ctrl+F', - ) - - # 替换代码 - self.__operation_replace = Operation( - widget=self.text_edit, name='replace code', label=self.tr('Replace'), - slot=self.slot_replace, key='Ctrl+H', - ) - - # 在路径中查找,暂不理解这个功能的含义 - self.__operation_find_in_path = Operation( - widget=self.text_edit, name='find in path', label=self.tr('Find In Path'), - slot=self.slot_find_in_path, key='Ctrl+Shift+F', - ) - - # 自动补全功能是每隔一段时间自动显示的,使用快捷键可以立刻显示 - self.__operation_auto_completion = Operation( - widget=self.text_edit, name='auto completion', label=self.tr('AutoComp'), - slot=self.auto_completion, key='Ctrl+P', - ) - - # 跳转到行 - self.__operation_goto_line = Operation( - widget=self.text_edit, name='goto line', label=self.tr('Goto Line'), - slot=self.slot_goto_line, key='Ctrl+G', - ) - - # 添加断点 - self.__operation_add_breakpoint = Operation( - widget=self.text_edit, name='add breakpoint', label=self.tr('Add Breakpoint'), - icon_name='breakpoint.svg', - ) - - # 移除断点 - self.__operation_remove_breakpoint = Operation( - widget=self.text_edit, name='remove breakpoint', label=self.tr('Remove Breakpoint'), - ) - - # 查看所有断点 - self.__operation_view_breakpoints = Operation( - widget=self.text_edit, name='view breakpoints', label=self.tr('View BreakPoints'), - ) - def auto_completion(self): pass @@ -423,7 +335,7 @@ class PMBaseEditor(CodeEditorBaseObject, QWidget): str, 选中的或全部的代码 """ if selected: - return self.text_edit.get_selected_text() + return self.text_edit.selected_code else: return self.text_edit.toPlainText() @@ -442,45 +354,25 @@ class PMBaseEditor(CodeEditorBaseObject, QWidget): """格式化代码""" def slot_code_run(self): - """运行全部代码""" + """运行代码""" + logger.warning('run code') + text = self.text().strip() + if not text: + return + + self._parent.slot_run_script(text) def slot_code_sel_run(self): - """运行选中的代码""" - - def create_context_menu(self) -> 'QMenu': - """创建上下文菜单。""" - menu = self.text_edit.createStandardContextMenu() - - # 遍历本身已有的菜单项做翻译处理 - # 前提是要加载了Qt自带的翻译文件 - for action in menu.actions(): - action.setText(QCoreApplication.translate('QTextControl', action.text())) - # 添加额外菜单 - menu.addSeparator() - menu.addAction(self.__operation_format.action) - menu.addAction(self.__operation_run_code.action) - menu.addAction(self.__operation_run_selected_code.action) - menu.addAction(self.__operation_save_code.action) - menu.addAction(self.__operation_find.action) - menu.addAction(self.__operation_replace.action) - menu.addAction(self.__operation_find_in_path.action) - menu.addAction(self.__operation_add_breakpoint.action) - menu.addAction(self.__operation_remove_breakpoint.action) - menu.addAction(self.__operation_view_breakpoints.action) - # menu.addAction(self) - return menu - - def slot_custom_context_menu_requested(self, pos: QPoint) -> None: - """打开右键菜单""" - menu = self.create_context_menu() - # 根据条件决定菜单是否可用 - logger.setLevel(logging.DEBUG) - logger.debug('menu craeted') - menu.exec_(self.text_edit.mapToGlobal(pos)) - logger.debug('menu deleted') + """运行选中代码""" + # TODO 存在问题,当选中了多行时,会报错 + text = self.text(selected=True).strip() + if not text: + text = self.current_line_text().strip() + + self._parent.slot_run_sel(text) def slot_find_in_path(self): - selected_text = self.text_edit.get_selected_text() + selected_text = self.text_edit.selected_code self.signal_request_find_in_path.emit(selected_text) def slot_find(self): diff --git a/packages/code_editor/widgets/editors/python_editor.py b/packages/code_editor/widgets/editors/python_editor.py index 87625d8b8cb8224deff64d5f78c3aaa6ff106f62..5c1efe4035eb0c054969325c9938530858aca8b8 100644 --- a/packages/code_editor/widgets/editors/python_editor.py +++ b/packages/code_editor/widgets/editors/python_editor.py @@ -36,9 +36,9 @@ from functools import cached_property from pathlib import Path from typing import List, Tuple, Optional, TYPE_CHECKING, Callable -from PySide2.QtCore import SignalInstance, Signal, Qt, QDir -from PySide2.QtGui import QKeySequence, QCloseEvent -from PySide2.QtWidgets import QAction, QShortcut, QMessageBox +from PySide2.QtCore import SignalInstance, Signal, QDir +from PySide2.QtGui import QCloseEvent +from PySide2.QtWidgets import QMessageBox from yapf.yapflib import py3compat, yapf_api from pmgwidgets import in_unit_test, PMGOneShotThreadRunner, run_python_file_in_terminal, parse_simplified_pmgjson, \ @@ -71,7 +71,6 @@ class PMPythonEditor(PMBaseEditor): self.browser_id = None self._parent = parent self.last_hint = '' - self.prepare_actions() def stop_auto_complete_thread(self): logger.info('autocomp stopped') @@ -84,40 +83,6 @@ class PMPythonEditor(PMBaseEditor): result = json.load(f) return result - def prepare_actions(self): - self._init_actions() - self._init_signals() - - def _init_actions(self) -> None: - """初始化事件""" - super(PMPythonEditor, self)._init_actions() - self._action_help = QAction(self.tr('Function Help'), self.text_edit) - self._shortcut_help = QShortcut(QKeySequence('F1'), self.text_edit, context=Qt.WidgetShortcut) - self._action_help.setShortcut(QKeySequence('F1')) - - self._action_help_in_console = QAction(self.tr('Help In Console'), self.text_edit) - self._shortcut_help_in_console = QShortcut(QKeySequence('F2'), self.text_edit, context=Qt.WidgetShortcut) - self._action_help_in_console.setShortcut(QKeySequence('F2')) - - self._action_goto_definition = QAction(self.tr('Go to Definition'), self.text_edit) - self._shortcut_goto_definition = QShortcut(QKeySequence('Ctrl+B'), self.text_edit, context=Qt.WidgetShortcut) - self._action_goto_definition.setShortcut(QKeySequence('Ctrl+B')) - - self._action_help_in_console.setVisible(False) - - # noinspection PyUnresolvedReferences - def _init_signals(self) -> None: - """初始化信号绑定。""" - super(PMPythonEditor, self)._init_signals() - self._shortcut_help.activated.connect(self.get_help) - self._action_help.triggered.connect(self.get_help) - - self._shortcut_help_in_console.activated.connect(self.get_help_in_console) - self._action_help_in_console.triggered.connect(self.get_help_in_console) - - self._shortcut_goto_definition.activated.connect(self.slot_goto_definition) - self._action_goto_definition.triggered.connect(self.slot_goto_definition) - def set_indicators(self, msgs: List[Tuple[int, int, str, str]], clear=True): """ 设置 error warning info 指示器 @@ -371,24 +336,6 @@ class PMPythonEditor(PMBaseEditor): # 清除被标记波浪线 self.text_edit.clearIndicatorRange(row, 0, row, col, self._indicator_error2) - def slot_code_sel_run(self): - """运行选中代码""" - # TODO 存在问题,当选中了多行时,会报错 - text = self.text(selected=True).strip() - if not text: - text = self.current_line_text().strip() - - self._parent.slot_run_sel(text) - - def slot_code_run(self): - """运行代码""" - logger.warning('run code') - text = self.text().strip() - if not text: - return - - self._parent.slot_run_script(text) - def slot_run_in_terminal(self): """在终端中运行代码 @@ -453,14 +400,6 @@ class PMPythonEditor(PMBaseEditor): traceback.print_exc() pass - def create_context_menu(self) -> 'QMenu': - logger.info('create_context_menu') - menu = super().create_context_menu() - menu.addAction(self._action_help) - menu.addAction(self._action_help_in_console) - menu.addAction(self._action_goto_definition) - return menu - def check_mkval_expr(self, code: str) -> Optional[Tuple[str, object, List[object]]]: """ 判断一行是否满足mkval的需求。 diff --git a/packages/code_editor/widgets/tab_widget.py b/packages/code_editor/widgets/tab_widget.py index 90fc7fe3bd524c053a59c3070d09234dcdceda11..1a5664b66ac8e3c760aae091d805d94b69255735 100644 --- a/packages/code_editor/widgets/tab_widget.py +++ b/packages/code_editor/widgets/tab_widget.py @@ -13,6 +13,7 @@ from pmgwidgets import PMDockObject, UndoManager, PMGFileSystemWatchdog, in_unit from .editors.markdown_editor import PMMarkdownEditorPM from .editors.python_editor import PMPythonEditor from .ui.findinpath import FindInPathWidget +from ..utils.base_object import CodeEditorBaseObject from ..utils.code_checker.base_code_checker import CodeCheckWorker from ..utils.highlighter.python_highlighter import PythonHighlighter @@ -50,6 +51,7 @@ class PMCodeEditTabWidget(QTabWidget, PMDockObject): def set_extension_lib(self, extension_lib): self.extension_lib = extension_lib + CodeEditorBaseObject.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)) @@ -518,14 +520,12 @@ class PMCodeEditTabWidget(QTabWidget, PMDockObject): code = code.strip() if hint == '': - hint = self.tr( - 'Run: %s') % self.get_current_filename() + hint = self.tr('Run: %s') % self.get_current_filename() elif isinstance(self.currentWidget(), PMMarkdownEditorPM): code = self.currentWidget().get_code() code = code.strip() if hint == '': - hint = self.tr( - 'Run Python Code inside %s') % self.get_current_filename() + hint = self.tr('Run Python Code inside %s') % self.get_current_filename() else: return if not code: @@ -533,7 +533,7 @@ class PMCodeEditTabWidget(QTabWidget, PMDockObject): 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)) + 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): """ diff --git a/packages/code_editor/widgets/text_edit/base_text_edit.py b/packages/code_editor/widgets/text_edit/base_text_edit.py index 688bdc8c7ad2903e17e0213061ad0a418c7e40e7..bef78ce95c111c5636d793f6d585fcdd2741a11d 100644 --- a/packages/code_editor/widgets/text_edit/base_text_edit.py +++ b/packages/code_editor/widgets/text_edit/base_text_edit.py @@ -8,18 +8,21 @@ from itertools import groupby from queue import Queue from typing import Callable, Tuple, Dict, List, TYPE_CHECKING, Type, Any -from PySide2.QtCore import SignalInstance, Signal, Qt, QTimer, QModelIndex, QUrl, QRect +from PySide2.QtCore import SignalInstance, Signal, Qt, QTimer, QModelIndex, QUrl, QRect, QPoint, QCoreApplication from PySide2.QtGui import QFocusEvent, QTextCursor, QMouseEvent, QKeyEvent, QDragEnterEvent, QDropEvent, QPainter, \ QColor, QTextFormat, QFontDatabase, QFont, QTextDocument -from PySide2.QtWidgets import QPlainTextEdit, QWidget, QApplication, QTextEdit, QLabel +from PySide2.QtWidgets import QPlainTextEdit, QWidget, QApplication, QTextEdit, QLabel, QMenu from jedi.api.classes import Completion as CompletionResult import utils from .line_number_area import QLineNumberArea from ..auto_complete_dropdown.base_auto_complete_dropdown import BaseAutoCompleteDropdownWidget +from ...code_handlers.base_handler import BaseHandler +from ...utils.base_object import CodeEditorBaseObject from ...utils.grammar_analyzer.get_indent import get_indent from ...utils.grammar_analyzer.grammar_analyzer import GrammarAnalyzer from ...utils.highlighter.python_highlighter import PythonHighlighter +from ...utils.operation import Operation if TYPE_CHECKING: from ...utils.highlighter.base_highlighter import BaseHighlighter @@ -30,11 +33,9 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -class PMBaseCodeEdit(QPlainTextEdit): +class PMBaseCodeEdit(CodeEditorBaseObject, QPlainTextEdit): """ - 与语言无关的编辑器相关操作应该定义在这里。 - 所有与TextEdit相关的操作都应该定义在这里,而对代码相关的操作应当定义在Editor中。 - 应当尽可能地避免子控件对父控件的调用,而是通过信号与槽的方式进行解耦。 + 所有与代码相关的编辑功能都应该定义在这里,包括排版、高亮等功能。 """ # 各个子类的配置项 highlighter_class: 'Type[BaseHighlighter]' = None # 语法高亮类 @@ -43,6 +44,9 @@ class PMBaseCodeEdit(QPlainTextEdit): highlighter: 'BaseHighlighter' = None auto_complete_thread: 'BaseAutoCompleteThread' = None + handler_class: 'Type[BaseHandler]' = BaseHandler # 代码核心操作类 + handler: 'BaseHandler' = None + # cursorPositionChanged = Signal() signal_save: SignalInstance = Signal() # 触发保存的事件,具体的保存操作交由给editor控件进行操作 signal_focused_in: SignalInstance = Signal(QFocusEvent) # 使用click代替focus,因为focus in信号触发过于频繁 @@ -74,6 +78,7 @@ class PMBaseCodeEdit(QPlainTextEdit): self.auto_complete_thread = self.auto_complete_thread_class() self.auto_complete_thread.trigger.connect(self.on_autocomp_signal_received) self.auto_complete_thread.start() + self.handler = self.handler_class() self.setTabChangesFocus(False) # 不允许Tab切换焦点,因Tab有更重要的切换缩进的作用 self.setMouseTracking(True) # 启用鼠标跟踪,这允许在鼠标滑过该控件时捕捉到事件 @@ -103,7 +108,6 @@ class PMBaseCodeEdit(QPlainTextEdit): self.setLineWrapMode(QPlainTextEdit.NoWrap) self.doc_tab_widget = parent - self.filename = '*' self.path = '' self.modified = False self._last_text = '' @@ -120,10 +124,11 @@ class PMBaseCodeEdit(QPlainTextEdit): self.ui_update_timer.start(300) # 绑定各个信号 - self._bind_signals() + self.__bind_signals() + self.__create_operations() # noinspection PyUnresolvedReferences - def _bind_signals(self): + def __bind_signals(self): # 定时触发的事件 self.ui_update_timer.timeout.connect(self.update_ui) @@ -142,6 +147,88 @@ class PMBaseCodeEdit(QPlainTextEdit): # 在代码提示框里面双击后,将自动补全的内容添加至代码 self.autocompletion_dropdown.doubleClicked.connect(self._insert_autocomp) + # 绑定右键菜单信号 + self.customContextMenuRequested.connect(self.slot_custom_context_menu_requested) + + self.textChanged.connect(self.update_handler_code) + self.selectionChanged.connect(self.update_handler_code) + + def __create_operations(self): + """创建操作,绑定快捷键,生成菜单项。""" + + def text_exists(): + """判断是否有文本,如果有文本才允许使用自动排版等功能""" + return len(self.code) > 0 + + def always_false(): + return False + + # 如果没有定义父对象,则直接返回,因为目前这个体系之下,大量的操作定义在了父对象中,导致单元测试跑不起来 + if not self.parent(): + return + + # TODO 将这些操作全部迁移至这个类下 + # 这里代码比较紧凑,以节省行数 + self.__menu_operations = [Operation( # 格式化代码 + widget=self, name='format code', label=self.tr('Format Code'), + slot=self.parent().slot_code_format, key='Ctrl+Alt+F', icon_name='format.svg', + conditions=[text_exists], + ), Operation( # 运行代码 + widget=self, name='run code', label=self.tr('Run Code'), + slot=self.slot_code_run, key='Ctrl+R', icon_name='run.svg', + conditions=[text_exists], + ), Operation( # 运行选中代码 + widget=self, name='run code', label=self.tr('Run Selected Code'), + slot=self.parent().slot_code_sel_run, key='F9', icon_name='python.svg', conditions=[text_exists], + # TODO 添加判别条件:仅当有文本选中时才可用 + ), Operation( # 保存代码 + widget=self, name='save code', label=self.tr('Save Code'), + slot=self.parent().slot_save, key='Ctrl+S', icon_name='save.svg', + ), Operation( # 查找代码 + widget=self, name='find code', label=self.tr('Find Code'), + slot=self.parent().slot_find, key='Ctrl+F', + ), Operation( # 替换代码 + widget=self, name='replace code', label=self.tr('Replace'), + slot=self.parent().slot_replace, key='Ctrl+H', + ), Operation( # 在路径中查找,暂不理解这个功能的含义 + widget=self, name='find in path', label=self.tr('Find In Path'), + slot=self.parent().slot_find_in_path, key='Ctrl+Shift+F', + ), Operation( # 自动补全功能是每隔一段时间自动显示的,使用快捷键可以立刻显示 + widget=self, name='auto completion', label=self.tr('AutoComp'), + slot=self.parent().auto_completion, key='Ctrl+P', + ), Operation( # 跳转到行 + widget=self, name='goto line', label=self.tr('Goto Line'), + slot=self.parent().slot_goto_line, key='Ctrl+G', + ), Operation( + widget=self, name='goto definition', label=self.tr('Goto Definition'), + slot=self.parent().slot_goto_definition, key='Ctrl+B', + ), Operation( # 函数帮助 + widget=self, name='function help', label=self.tr('Function Help'), + slot=self.slot_function_help, key='F1', + ), Operation( + widget=self, name='help in console', label=self.tr('Help in Console'), + slot=self.slot_help_in_console, key='F2', + conditions=[always_false], + ), Operation( # 添加断点 + widget=self, name='add breakpoint', label=self.tr('Add Breakpoint'), icon_name='breakpoint.svg', + ), Operation( # 移除断点 + widget=self, name='remove breakpoint', label=self.tr('Remove Breakpoint'), + ), Operation( # 查看所有断点 + widget=self, name='view breakpoints', label=self.tr('View BreakPoints'), + )] + + def createStandardContextMenu(self) -> QMenu: + menu = super().createStandardContextMenu() + # 遍历本身已有的菜单项做翻译处理,前提是要加载了Qt自带的翻译文件 + [action.setText(QCoreApplication.translate('QTextControl', action.text())) for action in menu.actions()] + # 添加额外菜单 + menu.addSeparator(), [menu.addAction(operation.action) for operation in self.__menu_operations] + return menu + + def slot_custom_context_menu_requested(self, pos: QPoint): + """打开右键菜单""" + self.createStandardContextMenu().exec_(self.mapToGlobal(pos)) + @property def line_number_area_width(self): return 30 + self.fontMetrics().width('9') * len(str(max(1, self.blockCount()))) @@ -247,6 +334,10 @@ class PMBaseCodeEdit(QPlainTextEdit): def hide_autocomp(self): self.autocompletion_dropdown.hide_autocomp() + def update_handler_code(self): + cursor = self.textCursor() + self.handler.feed(self.code, cursor.position(), (cursor.selectionStart(), cursor.selectionEnd())) + def on_text_changed(self): """文字发生改变时的方法""" if not self.modified: @@ -370,6 +461,7 @@ class PMBaseCodeEdit(QPlainTextEdit): 具体的性能没有进行过查证,不过直观上看,使用keyPressEvent在性能上会存在优势。 """ + self.update_handler_code() # TODO 按键处理逻辑仍存在bug,应当分为字符映射和键盘映射两种情况进行处理 # 即分别通过event.text()和event.key()+event.modifier()进行处理 text, key, modifiers = event.text(), event.key(), int(event.modifiers()) @@ -382,6 +474,7 @@ class PMBaseCodeEdit(QPlainTextEdit): callback(event) else: super().keyPressEvent(event) + self.update_handler_code() def on_left_parenthesis(self, event: QKeyEvent): cursor = self.textCursor() @@ -398,7 +491,6 @@ class PMBaseCodeEdit(QPlainTextEdit): return self.toPlainText() def on_right_parenthesis(self, event: QKeyEvent): - print(f'received {event.text()}') left, right = { Qt.Key_ParenRight: ('(', ')'), Qt.Key_BracketRight: ('[', ']'), @@ -410,12 +502,9 @@ class PMBaseCodeEdit(QPlainTextEdit): analyzer = GrammarAnalyzer() analyzer.feed(code) length = len(code) - print(f'{position == length}, {analyzer.is_not_matched(position, left)}, {code[position]}') if position == length or analyzer.is_not_matched(position, left) or code[position] != right: - print('first') cursor.insertText(right) else: - print('second') cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.MoveAnchor, 1) event.accept() @@ -528,7 +617,6 @@ class PMBaseCodeEdit(QPlainTextEdit): if not cursor.selectedText().endswith(' '): cursor.movePosition(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor, 1) break - # print('cursor.selected',cursor.selectedText()) cursor.removeSelectedText() def on_tab(self, _: QKeyEvent): @@ -629,11 +717,17 @@ class PMBaseCodeEdit(QPlainTextEdit): text_cursor.setPosition(pos) self.setTextCursor(text_cursor) - def get_selected_text(self) -> str: - if self.textCursor().hasSelection(): - return self.textCursor().selectedText() - else: - return '' + @property + def selected_code(self): + """获取光标选中的代码,或当前行""" + return self.handler.analyzer.selected_code + + @property + def current_line_code(self): + """当前行的代码,包括尾换行符""" + lines = self.code.splitlines(keepends=True) + row = self.textCursor().blockNumber() + return '' if row >= len(lines) else lines[row] def get_selected_row_numbers(self) -> Tuple[int, int]: """返回选中的行号范围""" @@ -793,3 +887,13 @@ class PMBaseCodeEdit(QPlainTextEdit): self.hint_widget.setText(text.strip()) self.hint_widget.setVisible(flag) event.ignore() + + def slot_function_help(self): + return self.parent().get_help() + + def slot_help_in_console(self): + return self.parent().get_help_in_console() + + def slot_code_run(self): + # TODO 这里的path应该是本对象的属性,而非父对象的属性 + self.handler.run_code(self.code, self.tr('Running {}').format(self.parent()._path)) diff --git a/packages/code_editor/widgets/text_edit/python_text_edit.py b/packages/code_editor/widgets/text_edit/python_text_edit.py index fed28e12bcae556831bb64354d5bec33703712d2..5a19c68d6e439c474d969cda748a5d00e859c065 100644 --- a/packages/code_editor/widgets/text_edit/python_text_edit.py +++ b/packages/code_editor/widgets/text_edit/python_text_edit.py @@ -5,6 +5,7 @@ from PySide2.QtCore import QPoint from jedi.api.classes import Completion as CompletionResult from .base_text_edit import PMBaseCodeEdit +from ...code_handlers.python_handler import PythonHandler from ...utils.auto_complete_thread.python_auto_complete import PythonAutoCompleteThread from ...utils.highlighter.python_highlighter import PythonHighlighter @@ -17,6 +18,7 @@ class PMPythonCodeEdit(PMBaseCodeEdit): auto_complete_thread_class = PythonAutoCompleteThread highlighter_class = PythonHighlighter + handler_class = PythonHandler def on_autocomp_signal_received(self, text_cursor_content: tuple, completions: List[CompletionResult]): """ diff --git a/requirements_dev.txt b/requirements_dev.txt index ca98cd0dd38dcd86e3dae72b3e4c3b724adb4d7c..f1f2bf039b58ad772849a8d6a084e6a91c201dab 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -6,3 +6,4 @@ flake8 pytest numpydoc pytest-qt +jinja2 \ No newline at end of file diff --git a/tests/test_code_editor/test_gui/test_python_code_edit.py b/tests/test_code_editor/test_gui/test_python_code_edit.py index 8ab8ddb9e015451027fa7d06a76ea9955b053370..b30825d4cf01d3cddb156f7786727966de51caed 100644 --- a/tests/test_code_editor/test_gui/test_python_code_edit.py +++ b/tests/test_code_editor/test_gui/test_python_code_edit.py @@ -1,16 +1,26 @@ -from packages.code_editor.widgets.text_edit.python_text_edit import PMPythonCodeEdit - +from PySide2.QtCore import Qt -def test_myapp(qtbot): - # TODO 用例错误 - editor = PMPythonCodeEdit() - qtbot.addWidget(editor) - editor.show() - qtbot.waitForWindowShown(editor) +from packages.code_editor.widgets.text_edit.python_text_edit import PMPythonCodeEdit - editor.setPlainText("abcdefg = 123\n" * 100) - editor.highlighter.registerHighlight(5, 3, 7, editor.highlighter.DEHIGHLIGHT, 'This is an Dehighlight') - editor.highlighter.registerHighlight(3, 1, 7, editor.highlighter.WARNING, 'This is an warning') - editor.highlighter.rehighlight() - qtbot.wait(1000) +def test_get_selected_text(qtbot): + window = PMPythonCodeEdit() + qtbot.addWidget(window) + window.show() + qtbot.waitForWindowShown(window) + qtbot.wait(100) + qtbot.keyClicks(window, 'a = 123') + qtbot.keySequence(window, 'Ctrl+A') + assert window.selected_code == 'a = 123' + qtbot.keyClick(window, Qt.Key_Right) + qtbot.keyClick(window, Qt.Key_Return) + qtbot.keyClicks(window, 'print(123)') + qtbot.keyClick(window, Qt.Key_Return) + qtbot.keySequence(window, 'Shift+Up') + assert window.selected_code == 'print(123)\n' + qtbot.keyClick(window, Qt.Key_Right) + qtbot.keyClick(window, Qt.Key_Right) + qtbot.keyClick(window, Qt.Key_Right) + assert window.selected_code == '' + qtbot.keyClick(window, Qt.Key_Up) + assert window.selected_code == 'print(123)' diff --git a/tests/test_code_editor/test_gui/test_python_edit.py b/tests/test_code_editor/test_gui/test_python_edit.py index 5a0af330aefebe00f7fcab4ccda96a60ac2420eb..a2783b2d37612019a13df2e2c5f51af51803640d 100644 --- a/tests/test_code_editor/test_gui/test_python_edit.py +++ b/tests/test_code_editor/test_gui/test_python_edit.py @@ -6,5 +6,5 @@ def test_myapp(qtbot): qtbot.addWidget(window) window.show() qtbot.waitForWindowShown(window) - window.setPlainText('a = 123') - qtbot.wait(1000) + qtbot.keyClicks(window, 'a=123') + assert window.code == 'a=123' diff --git a/tests/test_code_editor/test_gui/test_python_editor.py b/tests/test_code_editor/test_gui/test_python_editor.py index 5d10fcff2157b5b153589193f52aa6ef36394a14..b5e8d5489858cbab4178ade226e97357eef248f9 100644 --- a/tests/test_code_editor/test_gui/test_python_editor.py +++ b/tests/test_code_editor/test_gui/test_python_editor.py @@ -8,8 +8,9 @@ def test_format_code(qtbot): qtbot.addWidget(window) window.show() qtbot.waitForWindowShown(window) + qtbot.wait(100) qtbot.keyClicks(window.text_edit, 'a = 123') - qtbot.keyPress(window.text_edit, 'F', modifier=Qt.ControlModifier | Qt.AltModifier) + qtbot.keySequence(window.text_edit, 'Ctrl+Alt+F') assert window.text() == 'a = 123\n' @@ -24,3 +25,36 @@ def test_auto_completion(qtbot): qtbot.keyClick(window.text_edit.autocompletion_dropdown, Qt.Key_Return) qtbot.wait(1000) assert window.text() == 'import numbers' + + +def test_format_when_editing(qtbot): + """回车后会自动调整格式""" + window = PMPythonEditor() + qtbot.addWidget(window) + window.show() + qtbot.waitForWindowShown(window) + qtbot.wait(100) + qtbot.keyClicks(window.text_edit, 'def a():') + qtbot.keyClick(window.text_edit, Qt.Key_Return) + qtbot.keyClicks(window.text_edit, 'print(123)') + assert window.text_edit.code == 'def a():\n print(123)' + + +def test_comment(qtbot): + """注释功能和反注释功能""" + window = PMPythonEditor() + qtbot.addWidget(window) + window.show() + qtbot.waitForWindowShown(window) + qtbot.wait(100) + window.text_edit.setPlainText('def a():\n print(123)\n') + qtbot.keySequence(window.text_edit, 'Ctrl+A') + qtbot.keySequence(window.text_edit, 'Ctrl+/') + assert window.text_edit.code == '#def a():\n# print(123)\n#' + qtbot.keySequence(window.text_edit, 'Ctrl+/') + assert window.text_edit.code == 'def a():\n print(123)\n' + qtbot.keyClick(window.text_edit, Qt.Key_Left) + qtbot.keySequence(window.text_edit, 'Ctrl+/') + assert window.text_edit.code == '#def a():\n print(123)\n' + qtbot.keySequence(window.text_edit, 'Ctrl+/') + assert window.text_edit.code == 'def a():\n print(123)\n' diff --git a/tests/test_code_editor/test_handler/__init__.py b/tests/test_code_editor/test_handler/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/test_code_editor/test_handler/test_base_handler.py b/tests/test_code_editor/test_handler/test_base_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..32d38e7cdaadb8b6addbb6d5101afe5447c31a73 --- /dev/null +++ b/tests/test_code_editor/test_handler/test_base_handler.py @@ -0,0 +1,27 @@ +from packages.code_editor.code_handlers.base_handler import BaseAnalyzer + +code = ''' +a = 1 +b = 2 +c = 3 +print(a, b, c) +''' + + +# 2的结尾是位置12,3的前面是位置13 + + +def test_current_line_number(): + analyzer = BaseAnalyzer(code, 12) + assert analyzer.current_line_index == 2 + analyzer = BaseAnalyzer(code, 13) + assert analyzer.current_line_index == 3 + + +def test_selection(): + analyzer = BaseAnalyzer(code, 12, (12, 12)) + assert not analyzer.has_selection + assert analyzer.selected_code == 'b = 2' + analyzer = BaseAnalyzer(code, 12, (12, 17)) + assert analyzer.has_selection + assert analyzer.selected_code == '\nc = ' diff --git a/utils/dev/__init__.py b/utils/dev/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8f507ff9163e5be322f2db41689554682fc91d4c --- /dev/null +++ b/utils/dev/__init__.py @@ -0,0 +1,3 @@ +""" +用于存储开发时用到的一些工具 +""" diff --git a/utils/dev/system.py b/utils/dev/system.py new file mode 100644 index 0000000000000000000000000000000000000000..9f866a558483c12a6a8d49acf3ab653be428b3a6 --- /dev/null +++ b/utils/dev/system.py @@ -0,0 +1,18 @@ +import subprocess +from pathlib import Path +from typing import Union + +import chardet + + +def system(*args: Union[str, Path]): + args = [str(arg) for arg in args] + result = subprocess.run(args=args, capture_output=True) + if stdout := result.stdout: + print('Out: ') + stdout = stdout.decode(chardet.detect(result.stdout)['encoding']) + print(stdout) + if stderr := result.stderr: + print('Error: ') + stderr = stderr.decode(chardet.detect(result.stderr)['encoding']) + print(stderr)