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)