From 045b19e8a10ae4c2ce1a86b2e4a399fd71f15cae Mon Sep 17 00:00:00 2001 From: hejl Date: Sun, 1 Jun 2025 09:41:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=90=9C=E7=B4=A2=E6=80=A7=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/tab_items_controller.dart | 33 +- .../lib/framework/services/file_service.dart | 2 +- win_text_editor/lib/menus/app_menu.dart | 4 + win_text_editor/lib/menus/menu_constants.dart | 1 + .../controllers/call_function_controller.dart | 106 +++++ .../call_function/models/call_function.dart | 40 ++ .../services/call_function_service.dart | 128 ++++++ .../widgets/call_function_left_side.dart | 70 +++ .../widgets/call_function_right_side.dart | 136 ++++++ .../widgets/call_function_view.dart | 76 ++++ .../content_search_controller.dart | 18 +- .../services/base_search_service.dart | 153 +++++++ .../services/content_search_service.dart | 405 ------------------ .../services/count_search_service.dart | 218 ++++++++++ .../services/custom_search_service.dart | 133 ++++++ .../services/locate_search_service.dart | 112 +++++ .../widgets/search_settings.dart | 78 +++- .../lib/modules/module_router.dart | 1 + 18 files changed, 1283 insertions(+), 431 deletions(-) create mode 100644 win_text_editor/lib/modules/call_function/controllers/call_function_controller.dart create mode 100644 win_text_editor/lib/modules/call_function/models/call_function.dart create mode 100644 win_text_editor/lib/modules/call_function/services/call_function_service.dart create mode 100644 win_text_editor/lib/modules/call_function/widgets/call_function_left_side.dart create mode 100644 win_text_editor/lib/modules/call_function/widgets/call_function_right_side.dart create mode 100644 win_text_editor/lib/modules/call_function/widgets/call_function_view.dart create mode 100644 win_text_editor/lib/modules/content_search/services/base_search_service.dart delete mode 100644 win_text_editor/lib/modules/content_search/services/content_search_service.dart create mode 100644 win_text_editor/lib/modules/content_search/services/count_search_service.dart create mode 100644 win_text_editor/lib/modules/content_search/services/custom_search_service.dart create mode 100644 win_text_editor/lib/modules/content_search/services/locate_search_service.dart diff --git a/win_text_editor/lib/framework/controllers/tab_items_controller.dart b/win_text_editor/lib/framework/controllers/tab_items_controller.dart index a740d3d..1630d22 100644 --- a/win_text_editor/lib/framework/controllers/tab_items_controller.dart +++ b/win_text_editor/lib/framework/controllers/tab_items_controller.dart @@ -112,21 +112,28 @@ class TabItemsController with ChangeNotifier { } void handleFileDoubleTap(String filePath) { - if (activeContentController == null) { - final fileName = filePath.split(Platform.pathSeparator).last; - if (fileName == "component.xml") { - openOrActivateTab("标准组件", RouterKey.uftComponent, Icons.extension); - } else { - final fileExtension = _getFileExtension(fileName); - switch (fileExtension) { - case 'uftstructure': - openOrActivateTab("内存表", RouterKey.memoryTable, Icons.list); - break; - default: - Logger().error("没有活动的内容控制器", source: 'TabItemsController'); - } + final fileName = filePath.split(Platform.pathSeparator).last; + if (fileName == "component.xml") { + openOrActivateTab("标准组件", RouterKey.uftComponent, Icons.extension); + } else { + final fileExtension = _getFileExtension(fileName); + switch (fileExtension) { + case 'uftstructure': + openOrActivateTab("内存表", RouterKey.memoryTable, Icons.list); + break; + case 'uftfunction': + case 'uftservice': + case 'uftatomfunction': + case 'uftatomservice': + case 'uftfactorfunction': + case 'uftfactorservice': + openOrActivateTab("函数调用", RouterKey.callFunction, Icons.functions); + break; + default: + Logger().error("没有活动的内容控制器", source: 'TabItemsController'); } } + activeContentController?.onOpenFile(filePath); } diff --git a/win_text_editor/lib/framework/services/file_service.dart b/win_text_editor/lib/framework/services/file_service.dart index 9f126d5..c47bd4f 100644 --- a/win_text_editor/lib/framework/services/file_service.dart +++ b/win_text_editor/lib/framework/services/file_service.dart @@ -16,7 +16,7 @@ class FileService { ]; static const Map _uftFloders = { '.settings': '项目设置', - 'metadat': '元数据', + 'metadata': '元数据', 'tools': '工具资源', 'uftatom': 'UFT原子', 'uftbusiness': 'UFT业务逻辑', diff --git a/win_text_editor/lib/menus/app_menu.dart b/win_text_editor/lib/menus/app_menu.dart index cc5b37d..83f20b7 100644 --- a/win_text_editor/lib/menus/app_menu.dart +++ b/win_text_editor/lib/menus/app_menu.dart @@ -60,6 +60,10 @@ class AppMenu extends StatelessWidget { value: MenuConstants.uftComponent, child: ListTile(leading: Icon(Icons.extension), title: Text('标准组件')), ), + const PopupMenuItem( + value: MenuConstants.callFunction, + child: ListTile(leading: Icon(Icons.functions), title: Text('函数调用')), + ), ]; } diff --git a/win_text_editor/lib/menus/menu_constants.dart b/win_text_editor/lib/menus/menu_constants.dart index b5ebca0..cfc0520 100644 --- a/win_text_editor/lib/menus/menu_constants.dart +++ b/win_text_editor/lib/menus/menu_constants.dart @@ -25,6 +25,7 @@ class MenuConstants { //Uft菜单 static const String memoryTable = 'memory_table'; static const String uftComponent = 'uft_component'; + static const String callFunction = 'call_function'; static const String uftTools = 'uft_tools'; // 编辑菜单项 diff --git a/win_text_editor/lib/modules/call_function/controllers/call_function_controller.dart b/win_text_editor/lib/modules/call_function/controllers/call_function_controller.dart new file mode 100644 index 0000000..33465af --- /dev/null +++ b/win_text_editor/lib/modules/call_function/controllers/call_function_controller.dart @@ -0,0 +1,106 @@ +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; +import 'package:win_text_editor/framework/controllers/logger.dart'; +import 'package:win_text_editor/framework/services/macro_template_service.dart'; +import 'package:win_text_editor/modules/call_function/models/call_function.dart'; +import 'package:win_text_editor/modules/call_function/services/call_function_service.dart'; +import 'package:win_text_editor/shared/models/std_filed.dart'; +import 'package:win_text_editor/shared/uft_std_fields/field_data_source.dart'; +import 'package:win_text_editor/shared/base/base_content_controller.dart'; + +class CallFunctionController extends BaseContentController { + String? _errorMessage; + String tableName = ""; + String objectId = ""; + String chineseName = ""; + + late DataGridSource fieldsSource; + late DataGridSource indexesSource; + final CallFunctionService _service; + final MacroTemplateService templateService = MacroTemplateService(); + + // 新增:维护CallFunction对象 + late CallFunction _memoryTable; + + CallFunctionController() : _service = CallFunctionService(Logger()) { + // 初始化空数据 + final initialFields = [Field('1', '', '', '', false), Field('2', '', '', '', false)]; + + fieldsSource = FieldsDataSource( + initialFields, + onSelectionChanged: (index, isSelected) { + updateFieldSelection(index, isSelected); + }, + ); + + // 初始化CallFunction + _memoryTable = CallFunction(tableName: '', columns: initialFields); + } + + String? get errorMessage => _errorMessage; + + // 新增:获取当前CallFunction + CallFunction get memoryTable => _memoryTable; + + void initTemplateService() { + if (!templateService.inited) { + templateService.init(); + } + } + + String? genCodeString(List macroList) { + initTemplateService(); + return templateService.renderTemplate(macroList, _memoryTable.toMap()); + } + + @override + Future onOpenFile(String filePath) async { + Logger().info("Opening file: $filePath"); + try { + final tableData = await _service.parseStructureFile(filePath); + + // Update controller state + tableName = tableData.tableName; + chineseName = tableData.chineseName; + objectId = tableData.objectId; + + // Update data sources + (fieldsSource as FieldsDataSource).updateData(tableData.fields); + + // 更新CallFunction对象 + _memoryTable = CallFunction(tableName: tableName, columns: tableData.fields); + + // Clear any previous error + _errorMessage = null; + + // Notify UI to update + notifyListeners(); + } catch (e) { + _errorMessage = e.toString(); + notifyListeners(); + Logger().error("Error opening file: $e"); + } + } + + // 新增:更新字段选择状态 + void updateFieldSelection(int index, bool isSelected) { + final fields = (fieldsSource as FieldsDataSource).data; + if (index >= 0 && index < fields.length) { + fields[index].isSelected = isSelected; + fieldsSource.notifyListeners(); + + // 同步更新CallFunction + _memoryTable.columns[index].isSelected = isSelected; + notifyListeners(); + } + } + + @override + void onOpenFolder(String folderPath) { + // 不支持打开文件夹 + } + + @override + void dispose() { + super.dispose(); + } +} diff --git a/win_text_editor/lib/modules/call_function/models/call_function.dart b/win_text_editor/lib/modules/call_function/models/call_function.dart new file mode 100644 index 0000000..010114c --- /dev/null +++ b/win_text_editor/lib/modules/call_function/models/call_function.dart @@ -0,0 +1,40 @@ +import 'package:win_text_editor/shared/models/std_filed.dart'; + +class CallFunction { + final String tableName; + final List columns; + + CallFunction({required this.tableName, required this.columns}); + + List get selectFields => columns.where((field) => field.isSelected).toList(); + + Map toMap() { + return { + 'tableName': tableName, + 'fields': + columns + .map( + (field) => { + 'id': field.id, + 'name': field.name, + 'chineseName': field.chineseName, + 'type': field.type, + 'isLast': columns.indexOf(field) == columns.length - 1, + }, + ) + .toList(), + 'selectedFields': + selectFields + .map( + (field) => { + 'id': field.id, + 'name': field.name, + 'chineseName': field.chineseName, + 'type': field.type, + 'isLast': selectFields.indexOf(field) == selectFields.length - 1, + }, + ) + .toList(), + }; + } +} diff --git a/win_text_editor/lib/modules/call_function/services/call_function_service.dart b/win_text_editor/lib/modules/call_function/services/call_function_service.dart new file mode 100644 index 0000000..1b5480b --- /dev/null +++ b/win_text_editor/lib/modules/call_function/services/call_function_service.dart @@ -0,0 +1,128 @@ +// memory_table_service.dart +import 'dart:io'; + +import 'package:win_text_editor/modules/memory_table/models/memory_table.dart'; +import 'package:win_text_editor/shared/data/std_fields_cache.dart'; +import 'package:win_text_editor/shared/models/std_filed.dart'; +import 'package:win_text_editor/shared/uft_std_fields/field_data_service.dart'; +import 'package:xml/xml.dart' as xml; +import 'package:path/path.dart' as path; +import 'package:win_text_editor/framework/controllers/logger.dart'; + +class CallFunctionService { + final Logger _logger; + + CallFunctionService(this._logger); + + Future parseStructureFile(String filePath) async { + try { + // 1. Check file extension + if (!filePath.toLowerCase().endsWith('.uftstructure')) { + throw const FormatException("文件扩展名必须是.uftstructure"); + } + + // 2. 查找 metadata 目录和 stdfield.stfield 文件 + if (await StdFieldsCache.getLength() == 0) { + _logger.info("加载标准字段缓存"); + final metadataFile = await FieldDataService.findMetadataFile(filePath); + if (metadataFile != null) { + await FieldDataService.processStdFieldFile(metadataFile); + } + } + + // 3. Read and parse structure file content + final file = File(filePath); + final content = await file.readAsString(); + + final document = xml.XmlDocument.parse(content); + final structureNode = document.findAllElements('structure:Structure').firstOrNull; + + if (structureNode == null) { + throw const FormatException("文件格式错误:缺少structure:Structure节点"); + } + + // 4. Get basic info + final fileNameWithoutExt = path.basenameWithoutExtension(filePath); + final chineseName = structureNode.getAttribute('chineseName') ?? ''; + final objectId = structureNode.getAttribute('objectId') ?? ''; + + // 5. Process properties (fields) + final properties = document.findAllElements('properties'); + final fields = []; + int index = 1; + + for (final property in properties) { + final id = property.getAttribute('id') ?? ''; + // 尝试从缓存获取标准字段信息 + final stdField = StdFieldsCache.getData(id); + fields.add( + Field( + (index++).toString(), // 序号 + id, // 名称 + stdField?.chineseName ?? '', // 中文名 + stdField?.dateType ?? '', // 类型 + ), + ); + } + + // 6. Process indexes + final indexes = document.findAllElements('indexs'); + final indexList = []; + + for (final indexNode in indexes) { + final name = indexNode.getAttribute('name') ?? ''; + final containerType = indexNode.getAttribute('containerType'); + final isPrimary = containerType == null; + final rule = containerType ?? ''; + + // Get all index fields + final items = indexNode.findAllElements('items'); + final fieldsList = + items + .map((item) => item.getAttribute('attrname') ?? '') + .where((f) => f.isNotEmpty) + .toList(); + final indexFields = fieldsList.join(','); + + indexList.add( + Index( + name, // 索引名称 + isPrimary, // 是否主键 + indexFields, // 索引字段 + rule, // 规则 + ), + ); + } + + return TableData( + tableName: fileNameWithoutExt, + chineseName: chineseName, + objectId: objectId, + fields: fields.isNotEmpty ? fields : FieldDataService.getDefaultFields(), + indexes: indexList.isNotEmpty ? indexList : FieldDataService.getDefaultIndexes(), + ); + } on xml.XmlParserException catch (e) { + _logger.error("XML解析错误: ${e.message}"); + rethrow; + } catch (e) { + _logger.error("处理文件时发生错误: $e"); + rethrow; + } + } +} + +class TableData { + final String tableName; + final String chineseName; + final String objectId; + final List fields; + final List indexes; + + TableData({ + required this.tableName, + required this.chineseName, + required this.objectId, + required this.fields, + required this.indexes, + }); +} diff --git a/win_text_editor/lib/modules/call_function/widgets/call_function_left_side.dart b/win_text_editor/lib/modules/call_function/widgets/call_function_left_side.dart new file mode 100644 index 0000000..7792fbb --- /dev/null +++ b/win_text_editor/lib/modules/call_function/widgets/call_function_left_side.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:win_text_editor/modules/call_function/controllers/call_function_controller.dart'; +import 'package:win_text_editor/shared/uft_std_fields/field_data_source.dart'; +import 'package:win_text_editor/shared/uft_std_fields/fields_data_grid.dart'; + +class CallFunctionLeftSide extends StatelessWidget { + final CallFunctionController controller; + const CallFunctionLeftSide({super.key, required this.controller}); + + Widget _buildTextFieldRow(String label, String value) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('$label:'), + SizedBox( + width: 200, + child: TextField( + controller: TextEditingController(text: value), + readOnly: true, + decoration: const InputDecoration(isDense: true, contentPadding: EdgeInsets.all(8)), + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: double.infinity, + child: Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Wrap( + spacing: 16, + runSpacing: 8, + children: [ + _buildTextFieldRow('名称', controller.tableName), + _buildTextFieldRow('中文名', controller.chineseName), + _buildTextFieldRow('对象编号', controller.objectId), + ], + ), + ), + ), + ), + const SizedBox(height: 8), + const Padding( + padding: EdgeInsets.all(8.0), + child: Text('字段列表', style: TextStyle(fontWeight: FontWeight.bold)), + ), + Expanded( + flex: 6, + child: FieldsDataGrid( + fieldsSource: controller.fieldsSource as FieldsDataSource, + onSelectionChanged: (index, isSelected) { + controller.updateFieldSelection(index, isSelected); + }, + ), + ), + const Padding( + padding: EdgeInsets.all(8.0), + child: Text('索引列表', style: TextStyle(fontWeight: FontWeight.bold)), + ), + ], + ); + } +} diff --git a/win_text_editor/lib/modules/call_function/widgets/call_function_right_side.dart b/win_text_editor/lib/modules/call_function/widgets/call_function_right_side.dart new file mode 100644 index 0000000..fb06c2f --- /dev/null +++ b/win_text_editor/lib/modules/call_function/widgets/call_function_right_side.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:win_text_editor/modules/call_function/controllers/call_function_controller.dart'; + +class CallFunctionRightSide extends StatefulWidget { + final CallFunctionController controller; + final TextEditingController codeController; + + const CallFunctionRightSide({super.key, required this.controller, required this.codeController}); + + @override + State createState() => _CallFunctionRightSideState(); +} + +class _CallFunctionRightSideState extends State { + final List _selectedOperations = []; + + @override + void initState() { + super.initState(); + widget.controller.initTemplateService(); + widget.controller.addListener(_updateDisplay); + } + + @override + void dispose() { + widget.controller.removeListener(_updateDisplay); + super.dispose(); + } + + void _updateDisplay() { + widget.codeController.text = widget.controller.genCodeString(_selectedOperations)!; + } + + void _toggleOperation(String operation, bool? value) { + setState(() { + if (value == true) { + _selectedOperations.add(operation); + } else { + _selectedOperations.remove(operation); + } + _updateDisplay(); + }); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _buildCheckboxSection(), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('生成代码:', style: TextStyle(fontWeight: FontWeight.bold)), + IconButton( + icon: const Icon(Icons.content_copy, size: 20), + tooltip: '复制代码', + onPressed: () { + if (widget.codeController.text.isNotEmpty) { + Clipboard.setData(ClipboardData(text: widget.codeController.text)); + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('已复制到剪贴板'))); + } + }, + ), + ], + ), + ), + Flexible(child: _buildCodeEditor()), + ], + ); + } + + Widget _buildCheckboxSection() { + final operations = ['获取记录', '获取记录数', '插入记录', '修改记录', '删除记录', '遍历记录']; + + return SizedBox( + width: double.infinity, + child: Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 16, + runSpacing: 8, + children: operations.map((op) => _buildCheckbox(op)).toList(), + ), + ], + ), + ), + ), + ); + } + + Widget _buildCheckbox(String label) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox( + value: _selectedOperations.contains(label), + onChanged: (bool? value) => _toggleOperation(label, value), + ), + Text(label), + ], + ); + } + + Widget _buildCodeEditor() { + return Card( + child: Padding( + padding: const EdgeInsets.all(8), + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(4), + ), + child: TextField( + controller: widget.codeController, + maxLines: null, + expands: true, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.all(8), + ), + style: const TextStyle(fontFamily: 'monospace', color: Colors.blueAccent), + ), + ), + ), + ); + } +} diff --git a/win_text_editor/lib/modules/call_function/widgets/call_function_view.dart b/win_text_editor/lib/modules/call_function/widgets/call_function_view.dart new file mode 100644 index 0000000..a02716b --- /dev/null +++ b/win_text_editor/lib/modules/call_function/widgets/call_function_view.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:win_text_editor/framework/controllers/tab_items_controller.dart'; +import 'package:win_text_editor/modules/call_function/controllers/call_function_controller.dart'; + +import 'call_function_left_side.dart'; +import 'call_function_right_side.dart'; + +class CallFunctionView extends StatefulWidget { + final String tabId; + const CallFunctionView({super.key, required this.tabId}); + + @override + State createState() => _CallFunctionViewState(); +} + +class _CallFunctionViewState extends State { + late final CallFunctionController _controller; + final TextEditingController _codeController = TextEditingController(); + bool _isControllerFromTabManager = false; + + get tabManager => Provider.of(context, listen: false); + + @override + void initState() { + super.initState(); + + final controllerFromManager = tabManager.getController(widget.tabId); + if (controllerFromManager != null) { + _controller = controllerFromManager; + _isControllerFromTabManager = true; + } else { + _controller = CallFunctionController(); + _isControllerFromTabManager = false; + tabManager.registerController(widget.tabId, _controller); + } + } + + @override + void dispose() { + if (!_isControllerFromTabManager) { + _controller.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: _controller, + child: Consumer( + builder: (context, controller, child) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 左侧部分 (50%) + Expanded(flex: 5, child: CallFunctionLeftSide(controller: controller)), + const SizedBox(width: 8), + // 右侧部分 (50%) + Expanded( + flex: 5, + child: CallFunctionRightSide( + codeController: _codeController, + controller: controller, + ), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart b/win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart index dcedae1..963cc48 100644 --- a/win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart +++ b/win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart @@ -7,8 +7,10 @@ import 'package:file_picker/file_picker.dart'; import 'package:win_text_editor/framework/controllers/logger.dart'; import 'package:win_text_editor/modules/content_search/models/search_mode.dart'; import 'package:win_text_editor/modules/content_search/models/search_result.dart'; +import 'package:win_text_editor/modules/content_search/services/count_search_service.dart'; +import 'package:win_text_editor/modules/content_search/services/locate_search_service.dart'; import 'package:win_text_editor/shared/base/base_content_controller.dart'; -import '../services/content_search_service.dart'; +import '../services/custom_search_service.dart'; class ContentSearchController extends BaseContentController { String _searchQuery = ''; @@ -44,7 +46,9 @@ class ContentSearchController extends BaseContentController { // 添加进度更新方法 void _updateProgress(double value) { - _progress = value.clamp(0.0, 100.0); + final newProgress = value.clamp(0.0, 100.0); + if (newProgress <= _progress) return; + _progress = newProgress; notifyListeners(); } @@ -144,7 +148,7 @@ class ContentSearchController extends BaseContentController { return; } String first50Chars = searchQuery.length > 50 ? searchQuery.substring(0, 50) : searchQuery; - Logger().debug("搜索内容: $first50Chars"); + Logger().debug("开始搜索: $first50Chars..."); if (searchDirectory.isEmpty || !Directory(searchDirectory).existsSync()) { Logger().info("搜索目录不能为空"); @@ -153,14 +157,14 @@ class ContentSearchController extends BaseContentController { try { if (customRule) { - final validationResult = ContentSearchService.validateJsRule(searchQuery); + final validationResult = CustomSearchService.validateJsRule(searchQuery); if (validationResult.isError) { Logger().error('JavaScript 语法错误: ${validationResult.rawResult.toString()}'); return; } _allResults.addAll( - await ContentSearchService.performCustomSearch( + await CustomSearchService.performCustomSearch( directory: searchDirectory, fileType: fileType, jsFunction: searchQuery, @@ -172,7 +176,7 @@ class ContentSearchController extends BaseContentController { } else { if (searchMode == SearchMode.locate) { _allResults.addAll( - await ContentSearchService.performLocateSearch( + await LocateSearchService.performSearch( directory: searchDirectory, query: searchQuery, fileType: fileType, @@ -184,7 +188,7 @@ class ContentSearchController extends BaseContentController { ), ); } else { - final counts = await ContentSearchService.performCountSearch( + final counts = await CountSearchService.performSearch( directory: searchDirectory, query: searchQuery, fileType: fileType, diff --git a/win_text_editor/lib/modules/content_search/services/base_search_service.dart b/win_text_editor/lib/modules/content_search/services/base_search_service.dart new file mode 100644 index 0000000..2e7b257 --- /dev/null +++ b/win_text_editor/lib/modules/content_search/services/base_search_service.dart @@ -0,0 +1,153 @@ +// lib/app/modules/content_search/services/base_search_service.dart +import 'dart:io'; +import 'package:path/path.dart' as path; + +typedef ProgressCallback = void Function(double progress); +typedef FileProcessor = Future Function(File file, String content); + +abstract class BaseSearchService { + /// 处理文件遍历和进度回调(核心复用逻辑) + static Future processFiles({ + required String directory, + required String fileType, + required FileProcessor onFile, + ProgressCallback? onProgress, + bool Function()? shouldStop, + }) async { + final dir = Directory(directory); + final totalFiles = await countFilesInDirectory(dir, fileType, shouldStop); + if (totalFiles == 0) return; + + onProgress?.call(1); + + int processedFiles = 0; + int oldProgress = 1; + + await for (final entity in dir.list(recursive: true)) { + if (shouldStop?.call() == true) return; + if (entity is! File || !matchesFileType(entity.path, fileType)) continue; + + processedFiles++; + final progress = (processedFiles / totalFiles) * 99 + 1; + if (progress - oldProgress >= 1) { + oldProgress = progress.floor(); + onProgress?.call(progress); + } + + final content = await entity.readAsString(); + await onFile(entity, content); + } + + onProgress?.call(100); + } + + /// 以下为静态工具方法(被所有服务共享) + static List splitQuery(String query) { + return query.split(',').map((q) => q.trim()).where((q) => q.isNotEmpty).toList(); + } + + static RegExp buildSearchPattern({ + required String query, + required bool caseSensitive, + required bool wholeWord, + required bool useRegex, + }) { + String pattern; + if (useRegex) { + pattern = query; + } else { + pattern = RegExp.escape(query); + if (wholeWord) { + pattern = '\\b$pattern\\b'; + } + } + return RegExp(pattern, caseSensitive: caseSensitive, multiLine: true); + } + + static Future countFilesInDirectory( + Directory dir, + String fileType, + bool Function()? shouldStop, + ) async { + int totalFiles = 0; + await for (final entity in dir.list(recursive: true)) { + if (shouldStop?.call() == true) return totalFiles; + if (entity is File && matchesFileType(entity.path, fileType)) { + totalFiles++; + } + } + return totalFiles; + } + + static bool matchesFileType(String filePath, String fileType) { + // 处理特殊情况 + if (fileType == '*.*') return true; + if (fileType == '*') return true; + + // 分割通配符为文件名和扩展名部分 + final parts = fileType.split('.'); + final patternName = parts.length > 0 ? parts[0] : ''; + final patternExt = parts.length > 1 ? parts.sublist(1).join('.') : ''; + + // 获取文件的实际名称和扩展名 + final fileName = path.basename(filePath); + final fileExt = path.extension(fileName).toLowerCase().replaceFirst('.', ''); + + // 匹配文件名部分(如果有指定) + bool nameMatches = true; + if (patternName.isNotEmpty && patternName != '*') { + nameMatches = matchesWildcard(fileName, patternName); + } + + // 匹配扩展名部分(如果有指定) + bool extMatches = true; + if (patternExt.isNotEmpty) { + if (patternExt == '*') { + extMatches = true; + } else { + extMatches = matchesWildcard(fileExt, patternExt); + } + } + + return nameMatches && extMatches; + } + + // 辅助函数:判断字符串是否匹配通配符模式 + static bool matchesWildcard(String input, String pattern) { + // 处理特殊情况 + if (pattern == '*') return true; + + // 动态规划实现通配符匹配 + final m = input.length; + final n = pattern.length; + final dp = List.generate(m + 1, (_) => List.filled(n + 1, false)); + + // 空字符串匹配空模式 + dp[0][0] = true; + + // 处理模式以 * 开头的情况 + for (int j = 1; j <= n; j++) { + if (pattern[j - 1] == '*') { + dp[0][j] = dp[0][j - 1]; + } + } + + // 填充动态规划表 + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (pattern[j - 1] == '*') { + // * 可以匹配任意字符或空字符 + dp[i][j] = dp[i][j - 1] || dp[i - 1][j]; + } else if (pattern[j - 1] == '?' || input[i - 1] == pattern[j - 1]) { + // ? 匹配单个字符,或者字符直接相等 + dp[i][j] = dp[i - 1][j - 1]; + } else { + // 不匹配 + dp[i][j] = false; + } + } + } + + return dp[m][n]; + } +} diff --git a/win_text_editor/lib/modules/content_search/services/content_search_service.dart b/win_text_editor/lib/modules/content_search/services/content_search_service.dart deleted file mode 100644 index 86eb4ed..0000000 --- a/win_text_editor/lib/modules/content_search/services/content_search_service.dart +++ /dev/null @@ -1,405 +0,0 @@ -// lib/app/modules/content_search/content_search_service.dart - -import 'dart:convert'; -import 'dart:io'; -import 'package:flutter_js/flutter_js.dart'; -import 'package:path/path.dart' as path; -import 'package:win_text_editor/framework/controllers/logger.dart'; -import 'package:win_text_editor/modules/content_search/models/match_result.dart'; -import 'package:win_text_editor/modules/content_search/models/search_mode.dart'; -import 'package:win_text_editor/modules/content_search/models/search_result.dart'; - -typedef ProgressCallback = void Function(double progress); - -class ContentSearchService { - /// 执行定位搜索(返回所有匹配项) - static Future> performLocateSearch({ - required String directory, - required String query, - required String fileType, - required bool caseSensitive, - required bool wholeWord, - required bool useRegex, - ProgressCallback? onProgress, - bool Function()? shouldStop, - }) async { - final results = []; - final dir = Directory(directory); - final queries = _splitQuery(query); - - // 先统计文件总数 - int totalFiles = 0; - await for (final entity in dir.list(recursive: true)) { - if (shouldStop?.call() == true) return results; - if (entity is File && _matchesFileType(entity.path, fileType)) { - totalFiles++; - } - } - - onProgress?.call(1); - - int words = 0; - int processedFiles = 0; - int oldProgress = 1; - for (final q in queries) { - if (shouldStop?.call() == true) return results; - - final pattern = _buildSearchPattern( - query: q, - caseSensitive: caseSensitive, - wholeWord: wholeWord, - useRegex: useRegex, - ); - - Logger().info("搜索词 $q 开始[${++words}/${queries.length}]"); - - await for (final entity in dir.list(recursive: true)) { - if (shouldStop?.call() == true) return results; - - if (entity is File && _matchesFileType(entity.path, fileType)) { - processedFiles++; - final progress = (processedFiles / (totalFiles * queries.length)) * 99 + 1; - if (progress - oldProgress >= 1) { - oldProgress = progress.floor(); - onProgress?.call(progress); - } - - await _searchInFile(entity, pattern, results, q); - } - } - Logger().info("搜索词 $q 结束[$words/${queries.length}],搜索文件 $totalFiles 个,"); - } - - onProgress?.call(100); - return results; - } - - /// 计数搜索(返回每个关键词的匹配数) - static Future> performCountSearch({ - required String directory, - required String query, - required String fileType, - required bool caseSensitive, - required bool wholeWord, - required bool useRegex, - ProgressCallback? onProgress, - bool Function()? shouldStop, - }) async { - final counts = {}; - final dir = Directory(directory); - final queries = _splitQuery(query); // 分割查询字符串 - - int totalFiles = 0; - await for (final entity in dir.list(recursive: true)) { - if (shouldStop?.call() == true) return counts; - if (entity is File && _matchesFileType(entity.path, fileType)) { - totalFiles++; - } - } - - onProgress?.call(1); - - int words = 0; - int processedFiles = 0; - int oldProgress = 1; - for (final q in queries) { - if (shouldStop?.call() == true) return counts; - - final pattern = _buildSearchPattern( - query: q, - caseSensitive: caseSensitive, - wholeWord: wholeWord, - useRegex: useRegex, - ); - - Logger().info("搜索词 $q 开始[${++words}/${queries.length}]"); - counts[q] = 0; - - await for (final entity in dir.list(recursive: true)) { - if (shouldStop?.call() == true) return counts; - - if (entity is File && _matchesFileType(entity.path, fileType)) { - processedFiles++; - final progress = (processedFiles / (totalFiles * queries.length)) * 99 + 1; - if (progress - oldProgress >= 1) { - oldProgress = progress.floor(); - onProgress?.call(progress); - } - - await _countInFile(entity, pattern, counts, q); // 传递当前查询项 - } - } - - Logger().info("搜索词 $q 结束[$words/${queries.length}],搜索文件 $totalFiles 个,"); - } - - return counts; - } - - /// 新增方法:执行自定义 JavaScript 规则搜索 - static Future> performCustomSearch({ - required String directory, - required String fileType, - required String jsFunction, - required SearchMode searchMode, - ProgressCallback? onProgress, - bool Function()? shouldStop, - }) async { - final results = []; - int count = 0; - final dir = Directory(directory); - final jsRuntime = getJavascriptRuntime(); - - try { - // 定义 JavaScript 函数 - final jsCode = 'function match(content){$jsFunction};'; - - jsRuntime.evaluate(jsCode); - - int totalFiles = 0; - await for (final entity in dir.list(recursive: true)) { - if (shouldStop?.call() == true) return results; - if (entity is File && _matchesFileType(entity.path, fileType)) { - totalFiles++; - } - } - - onProgress?.call(1); - - int processedFiles = 0; - int calledProcessedFiles = 1; - - await for (final entity in dir.list(recursive: true)) { - if (shouldStop?.call() == true) return results; - if (entity is File && _matchesFileType(entity.path, fileType)) { - try { - final lines = await entity.readAsLines(); - for (int i = 0; i < lines.length; i++) { - if (shouldStop?.call() == true) return results; - final line = lines[i].trim(); - if (line.length < 3) continue; // 跳过短行 - - final result = jsRuntime.evaluate('match(${jsonEncode(line)});'); - if (result.isError) { - throw Exception('JS Error: ${result.stringResult}'); - } - - if (result.stringResult == 'true') { - if (searchMode == SearchMode.locate) { - results.add( - SearchResult( - filePath: entity.path, - lineNumber: i + 1, - lineContent: line, - matches: [], - queryTerm: "Custom Rule", - ), - ); - } else { - count++; - } - } - } - } catch (e) { - Logger().error('Error in file ${entity.path}: $e'); - } - - processedFiles++; - final progress = (processedFiles / totalFiles) * 99 + 1; - if (processedFiles - calledProcessedFiles >= 1) { - calledProcessedFiles = processedFiles; - onProgress?.call(progress); - } - } - } - - // 处理计数模式结果 - if (searchMode == SearchMode.count) { - results.add( - SearchResult( - filePath: "Custom Rule", - lineNumber: count, - lineContent: '', - matches: [], - queryTerm: "Custom Rule", - ), - ); - } - } finally { - onProgress?.call(100); - jsRuntime.dispose(); - } - - return results; - } - - /// 分割查询字符串(按半角逗号分隔,并去除空格) - static List _splitQuery(String query) { - return query.split(',').map((q) => q.trim()).where((q) => q.isNotEmpty).toList(); - } - - /// 构建正则表达式(原逻辑不变) - static RegExp _buildSearchPattern({ - required String query, - required bool caseSensitive, - required bool wholeWord, - required bool useRegex, - }) { - String pattern; - if (useRegex) { - pattern = query; - } else { - pattern = RegExp.escape(query); - if (wholeWord) { - pattern = '\\b$pattern\\b'; - } - } - return RegExp(pattern, caseSensitive: caseSensitive, multiLine: true); - } - - static bool _matchesFileType(String filePath, String fileType) { - // 处理特殊情况 - if (fileType == '*.*') return true; - if (fileType == '*') return true; - - // 分割通配符为文件名和扩展名部分 - final parts = fileType.split('.'); - final patternName = parts.length > 0 ? parts[0] : ''; - final patternExt = parts.length > 1 ? parts.sublist(1).join('.') : ''; - - // 获取文件的实际名称和扩展名 - final fileName = path.basename(filePath); - final fileExt = path.extension(fileName).toLowerCase().replaceFirst('.', ''); - - // 匹配文件名部分(如果有指定) - bool nameMatches = true; - if (patternName.isNotEmpty && patternName != '*') { - nameMatches = _matchesWildcard(fileName, patternName); - } - - // 匹配扩展名部分(如果有指定) - bool extMatches = true; - if (patternExt.isNotEmpty) { - if (patternExt == '*') { - extMatches = true; - } else { - extMatches = _matchesWildcard(fileExt, patternExt); - } - } - - return nameMatches && extMatches; - } - - // 辅助函数:判断字符串是否匹配通配符模式 - static bool _matchesWildcard(String input, String pattern) { - // 处理特殊情况 - if (pattern == '*') return true; - - // 动态规划实现通配符匹配 - final m = input.length; - final n = pattern.length; - final dp = List.generate(m + 1, (_) => List.filled(n + 1, false)); - - // 空字符串匹配空模式 - dp[0][0] = true; - - // 处理模式以 * 开头的情况 - for (int j = 1; j <= n; j++) { - if (pattern[j - 1] == '*') { - dp[0][j] = dp[0][j - 1]; - } - } - - // 填充动态规划表 - for (int i = 1; i <= m; i++) { - for (int j = 1; j <= n; j++) { - if (pattern[j - 1] == '*') { - // * 可以匹配任意字符或空字符 - dp[i][j] = dp[i][j - 1] || dp[i - 1][j]; - } else if (pattern[j - 1] == '?' || input[i - 1] == pattern[j - 1]) { - // ? 匹配单个字符,或者字符直接相等 - dp[i][j] = dp[i - 1][j - 1]; - } else { - // 不匹配 - dp[i][j] = false; - } - } - } - - return dp[m][n]; - } - - /// 在文件中搜索匹配项(增加 queryTerm 参数) - static Future _searchInFile( - File file, - RegExp pattern, - List results, - String queryTerm, // 当前查询项 - ) async { - try { - final lines = await file.readAsLines(); - for (int i = 0; i < lines.length; i++) { - final line = lines[i].trim(); - final matches = pattern.allMatches(line); - - if (matches.isNotEmpty) { - results.add( - SearchResult( - filePath: file.path, - lineNumber: i + 1, - lineContent: line, - matches: matches.map((m) => MatchResult(start: m.start, end: m.end)).toList(), - queryTerm: queryTerm, // 记录匹配的查询项 - ), - ); - } - } - } catch (e) { - Logger().error('Error reading file ${file.path}: $e'); - } - } - - /// 在文件中计数匹配项(增加 queryTerm 参数) - static Future _countInFile( - File file, - RegExp pattern, - Map counts, - String queryTerm, // 当前查询项 - ) async { - try { - final content = await file.readAsString(); - final matches = pattern.allMatches(content); - - if (matches.isNotEmpty) { - counts[queryTerm] = (counts[queryTerm] ?? 0) + matches.length; - } - } catch (e) { - Logger().error('Error reading file ${file.path}: $e'); - } - } - - // 在 ContentSearchService 类中添加以下方法 - - /// 验证 JavaScript 自定义规则的语法 - static JsEvalResult validateJsRule(String jsFunction) { - final jsRuntime = getJavascriptRuntime(); - try { - final jsCode = 'function match(content) {$jsFunction};'; - // 执行 JS 代码进行语法检查 - final result = jsRuntime.evaluate(jsCode); - if (result.isError) { - return result; - } - - // 测试函数是否返回布尔值 - final test = jsRuntime.evaluate("match('test');"); - if (test.stringResult != 'true' && test.stringResult != 'false') { - return JsEvalResult('Custom rule must return boolean (true/false)', null, isError: true); - } - - return test; - } finally { - jsRuntime.dispose(); - } - } -} diff --git a/win_text_editor/lib/modules/content_search/services/count_search_service.dart b/win_text_editor/lib/modules/content_search/services/count_search_service.dart new file mode 100644 index 0000000..4766c1b --- /dev/null +++ b/win_text_editor/lib/modules/content_search/services/count_search_service.dart @@ -0,0 +1,218 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:math'; +import 'package:win_text_editor/modules/content_search/services/base_search_service.dart'; + +class CountSearchService { + static const _maxConcurrentIsolates = 8; // 根据CPU核心数调整 + static const _queriesPerBatch = 500; // 每批搜索词数量 + + static List> _chunkList(List list, int chunkSize) { + return List.generate( + (list.length / chunkSize).ceil(), + (i) => list.sublist( + i * chunkSize, + i * chunkSize + chunkSize > list.length ? list.length : i * chunkSize + chunkSize, + ), + ); + } + + static Future> performSearch({ + required String directory, + required String query, + required String fileType, + required bool caseSensitive, + required bool wholeWord, + required bool useRegex, + ProgressCallback? onProgress, + bool Function()? shouldStop, + }) async { + if (shouldStop?.call() == true) return {}; + + final counts = {}; + final allQueries = BaseSearchService.splitQuery(query); + + // 分割搜索词为批次 + final queryBatches = _chunkList(allQueries, _queriesPerBatch); + + // 获取所有目标文件路径 + final filePaths = await _collectFilePaths(directory, fileType, shouldStop); + if (filePaths.isEmpty) return counts; + + // 启动Isolate池 + final resultPort = ReceivePort(); + final stopPort = ReceivePort(); + final completer = Completer>(); + int activeIsolates = 0; + + // 进度处理 + int processedFiles = 0; + void updateProgress() { + final progress = (processedFiles / filePaths.length) * 100; + onProgress?.call(progress); + } + + final initIsolates = min(2, _maxConcurrentIsolates); // 首批启动2个 + for (int i = 0; i < initIsolates; i++) { + _spawnIsolateWorker( + filePaths.sublist(i, i + 1), // 每个Isolate处理部分文件 + queryBatches, + resultPort.sendPort, + stopPort.sendPort, + caseSensitive, + wholeWord, + useRegex, + ); + activeIsolates++; + } + + // 分发任务到Isolate + Future.delayed(const Duration(milliseconds: 300), () { + for (int i = activeIsolates; i < min(_maxConcurrentIsolates, filePaths.length); i++) { + _spawnIsolateWorker( + filePaths.sublist(i, i + 1), // 每个Isolate处理部分文件 + queryBatches, + resultPort.sendPort, + stopPort.sendPort, + caseSensitive, + wholeWord, + useRegex, + ); + activeIsolates++; + } + }); + + // 监听结果 + resultPort.listen((msg) { + if (msg is Map) { + msg.forEach((query, count) { + counts.update(query, (v) => v + count, ifAbsent: () => count); + }); + processedFiles++; + updateProgress(); + + // 继续分配新任务 + if (activeIsolates < filePaths.length) { + _spawnIsolateWorker( + [filePaths[activeIsolates]], + queryBatches, + resultPort.sendPort, + stopPort.sendPort, + caseSensitive, + wholeWord, + useRegex, + ); + activeIsolates++; + } else if (processedFiles == filePaths.length) { + completer.complete(counts); + } + } + }); + + // 处理停止请求 + stopPort.listen((_) => completer.complete(counts)); + + return completer.future.whenComplete(() { + resultPort.close(); + stopPort.close(); + }); + } + + static Future> _collectFilePaths( + String directory, + String fileType, + bool Function()? shouldStop, + ) async { + final dir = Directory(directory); + final paths = []; + await for (final entity in dir.list(recursive: true)) { + if (shouldStop?.call() == true) break; + if (entity is File && BaseSearchService.matchesFileType(entity.path, fileType)) { + paths.add(entity.path); + } + } + return paths; + } + + static void _spawnIsolateWorker( + List filePaths, + List> queryBatches, + SendPort resultPort, + SendPort stopPort, + bool caseSensitive, + bool wholeWord, + bool useRegex, + ) async { + await Isolate.spawn( + _isolateEntry, + _IsolateData( + filePaths, + queryBatches, + resultPort, + stopPort, + caseSensitive, + wholeWord, + useRegex, + ), + ); + } + + static void _isolateEntry(_IsolateData data) { + final localCounts = {}; + for (final filePath in data.filePaths) { + try { + final content = File(filePath).readAsStringSync(); + for (final batch in data.queryBatches) { + // 并行处理每个搜索词批次 + final batchResults = ParallelTask.runSync(() { + final batchCounts = {}; + for (final query in batch) { + final pattern = BaseSearchService.buildSearchPattern( + query: query, + caseSensitive: data.caseSensitive, + wholeWord: data.wholeWord, + useRegex: data.useRegex, + ); + batchCounts[query] = pattern.allMatches(content).length; + } + return batchCounts; + }); + + batchResults.forEach((query, count) { + localCounts.update(query, (v) => v + count, ifAbsent: () => count); + }); + } + } catch (e) { + print('Isolate error: $e'); + } + } + data.resultPort.send(localCounts); + } +} + +// Isolate通信数据结构 +class _IsolateData { + final List filePaths; + final List> queryBatches; + final SendPort resultPort; + final SendPort stopPort; + final bool caseSensitive; + final bool wholeWord; + final bool useRegex; + + _IsolateData( + this.filePaths, + this.queryBatches, + this.resultPort, + this.stopPort, + this.caseSensitive, + this.wholeWord, + this.useRegex, + ); +} + +// 模拟并行任务处理 +class ParallelTask { + static T runSync(T Function() task) => task(); +} diff --git a/win_text_editor/lib/modules/content_search/services/custom_search_service.dart b/win_text_editor/lib/modules/content_search/services/custom_search_service.dart new file mode 100644 index 0000000..5a5d61f --- /dev/null +++ b/win_text_editor/lib/modules/content_search/services/custom_search_service.dart @@ -0,0 +1,133 @@ +// lib/app/modules/content_search/content_search_service.dart + +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter_js/flutter_js.dart'; +import 'package:win_text_editor/framework/controllers/logger.dart'; +import 'package:win_text_editor/modules/content_search/models/search_mode.dart'; +import 'package:win_text_editor/modules/content_search/models/search_result.dart'; +import 'package:win_text_editor/modules/content_search/services/base_search_service.dart'; + +typedef ProgressCallback = void Function(double progress); + +class CustomSearchService { + /// 新增方法:执行自定义 JavaScript 规则搜索 + static Future> performCustomSearch({ + required String directory, + required String fileType, + required String jsFunction, + required SearchMode searchMode, + ProgressCallback? onProgress, + bool Function()? shouldStop, + }) async { + final results = []; + int count = 0; + final dir = Directory(directory); + final jsRuntime = getJavascriptRuntime(); + + try { + // 定义 JavaScript 函数 + final jsCode = 'function match(content){$jsFunction};'; + + jsRuntime.evaluate(jsCode); + + int totalFiles = 0; + await for (final entity in dir.list(recursive: true)) { + if (shouldStop?.call() == true) return results; + if (entity is File && BaseSearchService.matchesFileType(entity.path, fileType)) { + totalFiles++; + } + } + + onProgress?.call(1); + + int processedFiles = 0; + int calledProcessedFiles = 1; + + await for (final entity in dir.list(recursive: true)) { + if (shouldStop?.call() == true) return results; + if (entity is File && BaseSearchService.matchesFileType(entity.path, fileType)) { + try { + final lines = await entity.readAsLines(); + for (int i = 0; i < lines.length; i++) { + if (shouldStop?.call() == true) return results; + final line = lines[i].trim(); + if (line.length < 3) continue; // 跳过短行 + + final result = jsRuntime.evaluate('match(${jsonEncode(line)});'); + if (result.isError) { + throw Exception('JS Error: ${result.stringResult}'); + } + + if (result.stringResult == 'true') { + if (searchMode == SearchMode.locate) { + results.add( + SearchResult( + filePath: entity.path, + lineNumber: i + 1, + lineContent: line, + matches: [], + queryTerm: "Custom Rule", + ), + ); + } else { + count++; + } + } + } + } catch (e) { + Logger().error('Error in file ${entity.path}: $e'); + } + + processedFiles++; + final progress = (processedFiles / totalFiles) * 99 + 1; + if (processedFiles - calledProcessedFiles >= 1) { + calledProcessedFiles = processedFiles; + onProgress?.call(progress); + } + } + } + + // 处理计数模式结果 + if (searchMode == SearchMode.count) { + results.add( + SearchResult( + filePath: "Custom Rule", + lineNumber: count, + lineContent: '', + matches: [], + queryTerm: "Custom Rule", + ), + ); + } + } finally { + onProgress?.call(100); + jsRuntime.dispose(); + } + + return results; + } + + /// 验证 JavaScript 自定义规则的语法 + static JsEvalResult validateJsRule(String jsFunction) { + final jsRuntime = getJavascriptRuntime(); + try { + final jsCode = 'function match(content) {$jsFunction};'; + // 执行 JS 代码进行语法检查 + final result = jsRuntime.evaluate(jsCode); + if (result.isError) { + return result; + } + + // 测试函数是否返回布尔值 + final test = jsRuntime.evaluate("match('test');"); + if (test.stringResult != 'true' && test.stringResult != 'false') { + return JsEvalResult('Custom rule must return boolean (true/false)', null, isError: true); + } + + return test; + } finally { + jsRuntime.dispose(); + } + } +} diff --git a/win_text_editor/lib/modules/content_search/services/locate_search_service.dart b/win_text_editor/lib/modules/content_search/services/locate_search_service.dart new file mode 100644 index 0000000..826a584 --- /dev/null +++ b/win_text_editor/lib/modules/content_search/services/locate_search_service.dart @@ -0,0 +1,112 @@ +// lib/app/modules/content_search/services/locate_search_service.dart +import 'dart:io'; +import 'package:win_text_editor/modules/content_search/models/match_result.dart'; + +import '../models/search_result.dart'; +import 'base_search_service.dart'; + +class LocateSearchService { + static Future> performSearch({ + required String directory, + required String query, + required String fileType, + required bool caseSensitive, + required bool wholeWord, + required bool useRegex, + ProgressCallback? onProgress, + bool Function()? shouldStop, + }) async { + final results = []; + final queries = BaseSearchService.splitQuery(query); + + await BaseSearchService.processFiles( + directory: directory, + fileType: fileType, + onProgress: onProgress, + shouldStop: shouldStop, + onFile: (file, content) async { + for (final q in queries) { + final pattern = BaseSearchService.buildSearchPattern( + query: q, + caseSensitive: caseSensitive, + wholeWord: wholeWord, + useRegex: useRegex, + ); + + final matches = pattern.allMatches(content); + if (matches.isNotEmpty) { + results.addAll(_convertMatchesToResults(file, content, matches, q)); + } + } + }, + ); + + return results; + } + + /// 将匹配结果转换为 SearchResult 列表(按行拆分) + static List _convertMatchesToResults( + File file, + String content, + Iterable matches, + String queryTerm, + ) { + final results = []; + final lines = content.split('\n'); + + // 创建一个映射表:行号 -> 该行的所有匹配项 + final lineMatches = >{}; + for (final match in matches) { + // 计算匹配所在的行号 + final lineNumber = _getLineNumber(lines, match.start); + lineMatches.putIfAbsent(lineNumber, () => []).add(match); + } + + // 为每行匹配生成结果 + lineMatches.forEach((lineNumber, matchesInLine) { + final lineContent = lines[lineNumber - 1]; // 行号从1开始 + final lineResults = + matchesInLine.map((m) { + // 计算匹配在行内的相对位置 + final lineStartPos = _getLineStartPos(lines, lineNumber - 1); + final startInLine = m.start - lineStartPos; + final endInLine = m.end - lineStartPos; + + return MatchResult(start: startInLine, end: endInLine); + }).toList(); + + results.add( + SearchResult( + filePath: file.path, + lineNumber: lineNumber, + lineContent: lineContent, + matches: lineResults, + queryTerm: queryTerm, + ), + ); + }); + + return results; + } + + /// 辅助方法:根据字符位置计算行号 + static int _getLineNumber(List lines, int charPos) { + int currentPos = 0; + for (int i = 0; i < lines.length; i++) { + currentPos += lines[i].length + 1; // +1 换行符 + if (charPos < currentPos) { + return i + 1; // 行号从1开始 + } + } + return lines.length; // 默认返回最后一行 + } + + /// 辅助方法:获取指定行的起始字符位置 + static int _getLineStartPos(List lines, int lineIndex) { + int pos = 0; + for (int i = 0; i < lineIndex; i++) { + pos += lines[i].length + 1; // +1 换行符 + } + return pos; + } +} diff --git a/win_text_editor/lib/modules/content_search/widgets/search_settings.dart b/win_text_editor/lib/modules/content_search/widgets/search_settings.dart index b8a1b07..6d0b746 100644 --- a/win_text_editor/lib/modules/content_search/widgets/search_settings.dart +++ b/win_text_editor/lib/modules/content_search/widgets/search_settings.dart @@ -1,16 +1,72 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:win_text_editor/shared/components/text_editor.dart'; import 'package:win_text_editor/modules/content_search/controllers/content_search_controller.dart'; import 'package:win_text_editor/modules/content_search/models/search_mode.dart'; -class SearchSettings extends StatelessWidget { +class SearchSettings extends StatefulWidget { const SearchSettings({super.key}); + @override + State createState() => _SearchSettingsState(); +} + +class _SearchSettingsState extends State { + Duration _elapsedTime = Duration.zero; + Stopwatch _stopwatch = Stopwatch(); + Timer? _updateTimer; + + @override + void dispose() { + _updateTimer?.cancel(); + _stopwatch.stop(); + super.dispose(); + } + + void _startTimer() { + _elapsedTime = Duration.zero; + _stopwatch + ..reset() + ..start(); + + // 每100毫秒更新一次UI,保持流畅显示 + _updateTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) { + setState(() { + _elapsedTime = _stopwatch.elapsed; + }); + }); + } + + void _stopTimer() { + _updateTimer?.cancel(); + _updateTimer = null; + _stopwatch.stop(); + // 确保获取最终准确时间 + _elapsedTime = _stopwatch.elapsed; + } + + String _formatDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); + String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); + String twoDigitMillis = twoDigits(duration.inMilliseconds.remainder(1000) ~/ 10); + + return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds.$twoDigitMillis"; + } + @override Widget build(BuildContext context) { final controller = context.watch(); + // 监听搜索状态变化来控制计时器 + if (controller.isSearching && _updateTimer == null) { + _startTimer(); + } else if (!controller.isSearching && _updateTimer != null) { + _stopTimer(); + } + return Card( child: Padding( padding: const EdgeInsets.all(8.0), @@ -227,17 +283,29 @@ class SearchSettings extends StatelessWidget { Expanded( child: ElevatedButton.icon( icon: const Icon(Icons.stop, size: 20), - label: const Text('停止'), + label: const Row( + mainAxisSize: MainAxisSize.min, + children: [Text('停止')], + ), onPressed: - controller.isSearching ? () => controller.stopSearch() : null, + controller.isSearching + ? () { + controller.stopSearch(); + _stopTimer(); + } + : null, style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, + backgroundColor: controller.isSearching ? Colors.red : null, + foregroundColor: controller.isSearching ? Colors.white : null, ), ), ), ], ), + Text( + ' 搜索计时:[${_formatDuration(_elapsedTime)}]', + style: const TextStyle(fontSize: 12), + ), ], ), ), diff --git a/win_text_editor/lib/modules/module_router.dart b/win_text_editor/lib/modules/module_router.dart index 9fd4e7d..bc085ec 100644 --- a/win_text_editor/lib/modules/module_router.dart +++ b/win_text_editor/lib/modules/module_router.dart @@ -25,6 +25,7 @@ class RouterKey { static const String dataCompare = 'data_compare'; static const String memoryTable = 'memory_table'; static const String uftComponent = 'uft_component'; + static const String callFunction = 'call_function'; static const String demo = 'demo'; }