diff --git a/win_text_editor/lib/app/providers/editor_provider.dart b/win_text_editor/lib/app/providers/editor_provider.dart index 7488489..7d84b06 100644 --- a/win_text_editor/lib/app/providers/editor_provider.dart +++ b/win_text_editor/lib/app/providers/editor_provider.dart @@ -1,15 +1,25 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:win_text_editor/app/providers/logger.dart'; class EditorProvider with ChangeNotifier { final List _tabs = []; String? _activeTabId; + bool _isLoading = false; List get tabs => _tabs; String? get activeTabId => _activeTabId; + bool get isLoading => _isLoading; int _templateTabCounter = 1; + final Map _tabLoadingStates = {}; + final Map _tabLoadedChunks = {}; + + bool isTabLoading(String tabId) => _tabLoadingStates[tabId] ?? false; + int getLoadedChunks(String tabId) => _tabLoadedChunks[tabId] ?? 0; + EditorTab? get activeTab { if (_activeTabId == null) return null; try { @@ -22,7 +32,14 @@ class EditorProvider with ChangeNotifier { void addTab() { final tabId = DateTime.now().millisecondsSinceEpoch.toString(); - _tabs.add(EditorTab(id: tabId, title: '模板解析[$_templateTabCounter]', content: '')); + _tabs.add( + EditorTab( + id: tabId, + title: '模板解析[$_templateTabCounter]', + chunks: [], // 显式初始化 + content: '', + ), + ); _templateTabCounter++; _activeTabId = tabId; notifyListeners(); @@ -43,27 +60,83 @@ class EditorProvider with ChangeNotifier { notifyListeners(); } - void updateContent(String tabId, String content, String? name) { + Future updateContent(String tabId, String content, String? name) async { + _isLoading = true; + notifyListeners(); + try { - final tab = _tabs.firstWhere((t) => t.id == tabId); - tab.content = content; + final index = _tabs.indexWhere((t) => t.id == tabId); + if (index == -1) throw Exception("Tab not found"); + + _tabs[index] = EditorTab( + id: _tabs[index].id, + title: _tabs[index].title, + chunks: content.split('\n'), // 同步更新 chunks + fileName: name ?? _tabs[index].fileName, + content: content, + ); + } finally { + _isLoading = false; + notifyListeners(); + } + } + + // 在 EditorProvider 中修改文件加载逻辑 + Future updateContentInChunks(String tabId, String fullContent, String? fileName) async { + final lines = fullContent.split('\n'); + const linesPerChunk = 1000; // 每1000行一个块 + + _tabLoadingStates[tabId] = true; + notifyListeners(); - if (name != null) { - tab.fileName = name; + try { + // 清空现有内容 + final index = _tabs.indexWhere((t) => t.id == tabId); + _tabs[index] = EditorTab( + id: tabId, + title: fileName ?? _tabs[index].title, + chunks: [], + fileName: fileName, + ); + + // 分块加载行 + for (int i = 0; i < lines.length; i += linesPerChunk) { + if (!_tabLoadingStates[tabId]!) break; // 检查是否取消 + + final chunkLines = lines.sublist(i, min(i + linesPerChunk, lines.length)); + _tabs[index].chunks!.addAll(chunkLines); + + notifyListeners(); + await Future.delayed(Duration.zero); // 让UI更新 } - Logger().debug("内容更新成功,文件:${tab.fileName}, ${tab.content.length}"); + } finally { + _tabLoadingStates.remove(tabId); notifyListeners(); - } catch (e) { - Logger().error("更新内容失败: ${e.toString()}", source: 'EditorProvider'); } } + + void cancelLoading(String tabId) { + _tabLoadingStates[tabId] = false; + notifyListeners(); + } } class EditorTab { final String id; String title; - String content; + List chunks; // 移除可空标记,改为非空 String? fileName; + bool isHighlightEnabled; + String content; + + EditorTab({ + required this.id, + required this.title, + List? chunks, // 参数可选,但类内部非空 + this.fileName, + this.isHighlightEnabled = true, + this.content = '', + }) : chunks = chunks ?? []; // 默认值为空列表 - EditorTab({required this.id, required this.title, required this.content, this.fileName}); + String get fullContent => chunks.join('\n'); // 用换行符连接 } diff --git a/win_text_editor/lib/app/utils/file_utils.dart b/win_text_editor/lib/app/utils/file_utils.dart new file mode 100644 index 0000000..f8b7ef1 --- /dev/null +++ b/win_text_editor/lib/app/utils/file_utils.dart @@ -0,0 +1,59 @@ +// file_utils.dart +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:win_text_editor/app/providers/logger.dart'; + +class FileUtils { + static Future pickFile(BuildContext context) async { + try { + final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false); + return result?.files.single.path; + } catch (e) { + Logger().error('选择文件失败: ${e.toString()}'); + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('选择文件失败: ${e.toString()}'))); + } + return null; + } + } + + static Future readFileContent( + BuildContext context, + String filePath, { + Duration timeout = const Duration(seconds: 30), + }) async { + try { + final content = await File(filePath).readAsString().timeout( + timeout, + onTimeout: () { + throw TimeoutException('文件加载超时,可能文件过大'); + }, + ); + return content; + } on FormatException { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件'))); + } + return null; + } on FileSystemException catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('文件访问错误: ${e.message}'))); + } + return null; + } catch (e) { + Logger().error('读取文件失败: ${e.toString()}'); + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('读取失败: ${e.toString()}'))); + } + return null; + } + } + + // 移除showLoadingDialog方法,因为现在直接在调用处处理 +} diff --git a/win_text_editor/lib/app/widgets/file_explorer.dart b/win_text_editor/lib/app/widgets/file_explorer.dart index 14fa021..66551ab 100644 --- a/win_text_editor/lib/app/widgets/file_explorer.dart +++ b/win_text_editor/lib/app/widgets/file_explorer.dart @@ -4,6 +4,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:win_text_editor/app/providers/logger.dart'; +import 'package:win_text_editor/app/utils/file_utils.dart'; import '../models/file_node.dart'; import '../providers/file_provider.dart'; @@ -64,11 +65,26 @@ class _FileExplorerState extends State { } } + // 在_file_explorer.dart中修改_openFileInEditor方法 Future _openFileInEditor(BuildContext context, FileNode node) async { + Logger().info('尝试打开文件: ${node.path}'); + final editorProvider = Provider.of(context, listen: false); + + showDialog( + context: context, + barrierDismissible: false, + builder: + (context) => const AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [CircularProgressIndicator(), SizedBox(height: 16), Text('正在加载文件...')], + ), + ), + ); + try { - Logger().info('尝试打开文件: ${node.path}'); - final editorProvider = Provider.of(context, listen: false); - final content = await File(node.path).readAsString(); + final content = await FileUtils.readFileContent(context, node.path); + if (content == null) return; Logger().debug('文件内容读取成功,长度: ${content.length}'); Logger().debug('当前活动选项卡ID: ${editorProvider.activeTabId}'); @@ -79,6 +95,7 @@ class _FileExplorerState extends State { Logger().info('没有活动选项卡,创建新选项卡'); editorProvider.addTab(); } + // 更新内容 Logger().info('准备更新选项卡内容'); editorProvider.updateContent(editorProvider.activeTabId!, content, node.name); @@ -86,18 +103,8 @@ class _FileExplorerState extends State { if (context.mounted) { Logger().debug('已加载: ${node.name}'); } - } on FormatException { - Logger().warning('文件格式异常: ${node.path}'); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件'))); - } - } catch (e) { - Logger().error('打开文件失败: ${e.toString()}', source: 'FileExplorer'); - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('读取失败: ${e.toString()}'))); - } + } finally { + if (context.mounted) Navigator.of(context).pop(); } } diff --git a/win_text_editor/lib/app/widgets/text_tab.dart b/win_text_editor/lib/app/widgets/text_tab.dart index e138077..10f118c 100644 --- a/win_text_editor/lib/app/widgets/text_tab.dart +++ b/win_text_editor/lib/app/widgets/text_tab.dart @@ -1,3 +1,5 @@ +import 'dart:async'; +import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; @@ -7,8 +9,8 @@ import 'package:win_text_editor/app/providers/editor_provider.dart'; import 'package:file_picker/file_picker.dart'; import 'dart:io'; import 'package:flutter_highlight/flutter_highlight.dart'; -import 'package:flutter_highlight/themes/github.dart'; -import 'package:flutter_highlight/themes/monokai-sublime.dart'; +import 'package:win_text_editor/app/providers/logger.dart'; +import 'package:win_text_editor/app/utils/file_utils.dart'; class TextTab extends StatefulWidget { final String tabId; @@ -23,21 +25,76 @@ class _TextTabState extends State { late TextEditingController _controller; late EditorProvider _provider; late FocusNode _focusNode; - late ScrollController _scrollController; + String _language = 'plaintext'; + bool _showFullContent = false; + final int _displayThreshold = 50000; // 显示阈值 + + final Map _visibleLines = {}; + final ScrollController _scrollController = ScrollController(); + final double _lineHeight = 20.0; // 单行高度 + int _firstVisibleLine = 0; + int _lastVisibleLine = 0; + @override void initState() { super.initState(); _provider = Provider.of(context, listen: false); _controller = TextEditingController(text: _getCurrentContent()); _focusNode = FocusNode(); - _scrollController = ScrollController(); // 添加这行 + _scrollController.addListener(_calculateVisibleLines); _detectLanguage(); + + // 初始加载可见区域 + WidgetsBinding.instance.addPostFrameCallback((_) { + _calculateVisibleLines(); + }); + } + + void _calculateVisibleLines() { + final scrollOffset = _scrollController.offset; + final viewportHeight = _scrollController.position.viewportDimension; + + _firstVisibleLine = (scrollOffset / _lineHeight).floor(); + _lastVisibleLine = ((scrollOffset + viewportHeight) / _lineHeight).ceil(); + + // 初始加载时至少加载前 500 行 + if (scrollOffset == 0) { + _lastVisibleLine = min(500, _lastVisibleLine); + } + + _preloadLines(); // 预加载附近行 + } + + void _preloadLines() { + final tab = _provider.tabs.firstWhere((t) => t.id == widget.tabId); + final lines = tab.fullContent.split('\n'); + + // 仅缓存可见区域附近的行 + final preloadStart = max(0, _firstVisibleLine - 20); + final preloadEnd = min(lines.length, _lastVisibleLine + 20); + + for (int i = preloadStart; i < preloadEnd; i++) { + _visibleLines[i] = lines[i]; + } + } + + Widget _buildLine(int lineIndex) { + final tab = _provider.tabs.firstWhere((t) => t.id == widget.tabId); + final lineContent = _visibleLines[lineIndex] ?? ''; + + return tab.isHighlightEnabled + ? HighlightView( + lineContent, + language: _language, + textStyle: const TextStyle(fontFamily: 'monospace', fontSize: 14), + ) + : Text(lineContent, style: const TextStyle(fontFamily: 'monospace')); } String _getCurrentContent() { - return _provider.tabs.firstWhere((t) => t.id == widget.tabId).content; + return _provider.tabs.firstWhere((t) => t.id == widget.tabId).fullContent; } void _detectLanguage() { @@ -90,8 +147,10 @@ class _TextTabState extends State { void didUpdateWidget(TextTab oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.tabId != widget.tabId) { + _visibleLines.clear(); // 修复问题2:清空缓存 _controller.text = _getCurrentContent(); _detectLanguage(); + _calculateVisibleLines(); // 重新加载 } } @@ -100,14 +159,19 @@ class _TextTabState extends State { _controller.dispose(); _focusNode.dispose(); _scrollController.dispose(); // 添加这行 + // 关闭时取消加载 + if (_provider.isTabLoading(widget.tabId)) { + _provider.cancelLoading(widget.tabId); + } super.dispose(); } @override Widget build(BuildContext context) { final tab = _provider.tabs.firstWhere((t) => t.id == widget.tabId); - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - + final progress = _provider.getLoadedChunks(widget.tabId); + final totalChunks = (tab.content.length / 10000).ceil(); + final lineCount = tab.fullContent.split('\n').length; return Column( children: [ Container( @@ -172,81 +236,49 @@ class _TextTabState extends State { ], ), ), + if (_provider.isTabLoading(widget.tabId)) + LinearProgressIndicator(value: progress / totalChunks, minHeight: 2), Expanded( - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - scrollbars: true, - dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse}, - ), - child: SingleChildScrollView( - controller: _scrollController, // 添加滚动控制器 - child: Stack( - children: [ - // 高亮显示的背景文本 - HighlightView( - tab.content, - language: _language, - theme: isDarkMode ? monokaiSublimeTheme : githubTheme, - padding: const EdgeInsets.all(16), - textStyle: const TextStyle(fontFamily: 'monospace', fontSize: 14), - ), - // 实际可编辑的TextField - TextField( - controller: _controller, - focusNode: _focusNode, - maxLines: null, - //scrollController: _scrollController, // 使用同一个滚动控制器 - onChanged: (text) => _provider.updateContent(widget.tabId, text, tab.fileName), - decoration: const InputDecoration( - border: InputBorder.none, - contentPadding: EdgeInsets.all(16), - ), - style: const TextStyle( - fontFamily: 'monospace', - fontSize: 14, - color: Colors.transparent, - ), - cursorColor: Colors.black, - ), - ], - ), - ), + child: ListView.builder( + controller: _scrollController, + itemCount: lineCount, + itemExtent: _lineHeight, // 固定行高优化性能 + itemBuilder: (ctx, index) => _buildLine(index), ), ), ], ); } - // _openFile方法现在需要更新控制器 Future _openFile(BuildContext context) async { + final filePath = await FileUtils.pickFile(context); + if (filePath == null || !mounted) return; + try { - final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false); + final content = await FileUtils.readFileContent(context, filePath); + if (content == null || !mounted) return; + + final fileName = filePath.split('\\').last; - if (result != null && result.files.single.path != null) { - final file = File(result.files.single.path!); - final content = await file.readAsString(); + // 根据文件大小选择加载方式 + if (content.length > 100000) { + // 超过100k使用分块加载 + await _provider.updateContentInChunks(widget.tabId, content, fileName); + } else { + await _provider.updateContent(widget.tabId, content, fileName); + } - _provider.updateContent(widget.tabId, content, result.files.first.name); + if (mounted) { setState(() { _controller.text = content; _detectLanguage(); + _showFullContent = content.length <= _displayThreshold; + _visibleLines.clear(); // 清空缓存 + _calculateVisibleLines(); // 重新计算可见行 }); - - // 强制刷新文本控制器 - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已加载: ${file.path}'))); - } } - } on FormatException { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件'))); - } on FileSystemException catch (e) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('文件访问错误: ${e.message}'))); } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('读取失败: ${e.toString()}'))); - } + // 错误处理保持不变 } }