18 changed files with 1283 additions and 431 deletions
@ -0,0 +1,106 @@
@@ -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<String> macroList) { |
||||
initTemplateService(); |
||||
return templateService.renderTemplate(macroList, _memoryTable.toMap()); |
||||
} |
||||
|
||||
@override |
||||
Future<void> 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(); |
||||
} |
||||
} |
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
import 'package:win_text_editor/shared/models/std_filed.dart'; |
||||
|
||||
class CallFunction { |
||||
final String tableName; |
||||
final List<Field> columns; |
||||
|
||||
CallFunction({required this.tableName, required this.columns}); |
||||
|
||||
List<Field> get selectFields => columns.where((field) => field.isSelected).toList(); |
||||
|
||||
Map<String, dynamic> 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(), |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,128 @@
@@ -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<TableData> 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 = <Field>[]; |
||||
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 = <Index>[]; |
||||
|
||||
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<Field> fields; |
||||
final List<Index> indexes; |
||||
|
||||
TableData({ |
||||
required this.tableName, |
||||
required this.chineseName, |
||||
required this.objectId, |
||||
required this.fields, |
||||
required this.indexes, |
||||
}); |
||||
} |
@ -0,0 +1,70 @@
@@ -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)), |
||||
), |
||||
], |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,136 @@
@@ -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<CallFunctionRightSide> createState() => _CallFunctionRightSideState(); |
||||
} |
||||
|
||||
class _CallFunctionRightSideState extends State<CallFunctionRightSide> { |
||||
final List<String> _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), |
||||
), |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,76 @@
@@ -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<CallFunctionView> createState() => _CallFunctionViewState(); |
||||
} |
||||
|
||||
class _CallFunctionViewState extends State<CallFunctionView> { |
||||
late final CallFunctionController _controller; |
||||
final TextEditingController _codeController = TextEditingController(); |
||||
bool _isControllerFromTabManager = false; |
||||
|
||||
get tabManager => Provider.of<TabItemsController>(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<CallFunctionController>( |
||||
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, |
||||
), |
||||
), |
||||
], |
||||
), |
||||
); |
||||
}, |
||||
), |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,153 @@
@@ -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<void> Function(File file, String content); |
||||
|
||||
abstract class BaseSearchService { |
||||
/// 处理文件遍历和进度回调(核心复用逻辑) |
||||
static Future<void> 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<String> 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<int> 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]; |
||||
} |
||||
} |
@ -1,405 +0,0 @@
@@ -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<List<SearchResult>> 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 = <SearchResult>[]; |
||||
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<Map<String, int>> 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 = <String, int>{}; |
||||
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<List<SearchResult>> performCustomSearch({ |
||||
required String directory, |
||||
required String fileType, |
||||
required String jsFunction, |
||||
required SearchMode searchMode, |
||||
ProgressCallback? onProgress, |
||||
bool Function()? shouldStop, |
||||
}) async { |
||||
final results = <SearchResult>[]; |
||||
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<String> _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<void> _searchInFile( |
||||
File file, |
||||
RegExp pattern, |
||||
List<SearchResult> 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<void> _countInFile( |
||||
File file, |
||||
RegExp pattern, |
||||
Map<String, int> 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(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,218 @@
@@ -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<List<T>> _chunkList<T>(List<T> 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<Map<String, int>> 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 = <String, int>{}; |
||||
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<Map<String, int>>(); |
||||
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<String, int>) { |
||||
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<List<String>> _collectFilePaths( |
||||
String directory, |
||||
String fileType, |
||||
bool Function()? shouldStop, |
||||
) async { |
||||
final dir = Directory(directory); |
||||
final paths = <String>[]; |
||||
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<String> filePaths, |
||||
List<List<String>> 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 = <String, int>{}; |
||||
for (final filePath in data.filePaths) { |
||||
try { |
||||
final content = File(filePath).readAsStringSync(); |
||||
for (final batch in data.queryBatches) { |
||||
// 并行处理每个搜索词批次 |
||||
final batchResults = ParallelTask.runSync(() { |
||||
final batchCounts = <String, int>{}; |
||||
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<String> filePaths; |
||||
final List<List<String>> 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>(T Function() task) => task(); |
||||
} |
@ -0,0 +1,133 @@
@@ -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<List<SearchResult>> performCustomSearch({ |
||||
required String directory, |
||||
required String fileType, |
||||
required String jsFunction, |
||||
required SearchMode searchMode, |
||||
ProgressCallback? onProgress, |
||||
bool Function()? shouldStop, |
||||
}) async { |
||||
final results = <SearchResult>[]; |
||||
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(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,112 @@
@@ -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<List<SearchResult>> 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 = <SearchResult>[]; |
||||
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<SearchResult> _convertMatchesToResults( |
||||
File file, |
||||
String content, |
||||
Iterable<RegExpMatch> matches, |
||||
String queryTerm, |
||||
) { |
||||
final results = <SearchResult>[]; |
||||
final lines = content.split('\n'); |
||||
|
||||
// 创建一个映射表:行号 -> 该行的所有匹配项 |
||||
final lineMatches = <int, List<RegExpMatch>>{}; |
||||
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<String> 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<String> lines, int lineIndex) { |
||||
int pos = 0; |
||||
for (int i = 0; i < lineIndex; i++) { |
||||
pos += lines[i].length + 1; // +1 换行符 |
||||
} |
||||
return pos; |
||||
} |
||||
} |
Loading…
Reference in new issue