diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index e1192580c5..e17088c954 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -97,9 +97,13 @@ class _MyHomePageState extends State { builder: (context, snapshot) { if (snapshot.hasData) { final data = Map.from(json.decode(snapshot.data!)); - return _buildAppFlowyEditor(EditorState( - document: StateTree.fromJson(data), - )); + final editorState = EditorState(document: StateTree.fromJson(data)); + editorState.logConfiguration + ..level = LogLevel.all + ..handler = (message) { + debugPrint(message); + }; + return _buildAppFlowyEditor(editorState); } else { return const Center( child: CircularProgressIndicator(), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart index d44f1b3241..14826ff713 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart @@ -1,6 +1,7 @@ /// AppFlowyEditor library library appflowy_editor; +export 'src/infra/log.dart'; export 'src/document/node.dart'; export 'src/document/path.dart'; export 'src/document/position.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart index 659e612645..3f9a984090 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:appflowy_editor/src/infra/log.dart'; import 'package:appflowy_editor/src/service/service.dart'; import 'package:flutter/material.dart'; @@ -48,10 +49,15 @@ class EditorState { // Service reference. final service = FlowyService(); + /// Configures log output parameters, + /// such as log level and log output callbacks, + /// with this variable. + LogConfiguration get logConfiguration => LogConfiguration(); + final UndoManager undoManager = UndoManager(); Selection? _cursorSelection; - /// TODO: only for testing. + // TODO: only for testing. bool disableSealTimer = false; Selection? get cursorSelection { @@ -120,7 +126,7 @@ class EditorState { _debouncedSealHistoryItemTimer = Timer(const Duration(milliseconds: 1000), () { if (undoManager.undoStack.isNonEmpty) { - debugPrint('Seal history item'); + Log.editor.debug('Seal history item'); final last = undoManager.undoStack.last; last.seal(); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/log.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/log.dart new file mode 100644 index 0000000000..cf04eefb0f --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/log.dart @@ -0,0 +1,109 @@ +import 'package:logging/logging.dart'; + +enum LogLevel { + off, + error, + warn, + info, + debug, + all, +} + +typedef LogHandler = void Function(String message); + +class LogConfiguration { + LogConfiguration._() { + Logger.root.onRecord.listen((record) { + if (handler != null) { + handler!( + '[${record.level.toLogLevel().name}][${record.loggerName}]: ${record.time}: ${record.message}'); + } + }); + } + + factory LogConfiguration() => _logConfiguration; + + static final LogConfiguration _logConfiguration = LogConfiguration._(); + + LogHandler? handler; + + LogLevel _level = LogLevel.off; + + LogLevel get level => _level; + set level(LogLevel level) { + _level = level; + Logger.root.level = level.toLevel(); + } +} + +class Log { + Log._({ + required this.name, + }) : _logger = Logger(name); + + final String name; + late final Logger _logger; + + static Log editor = Log._(name: 'editor'); + static Log selection = Log._(name: 'selection'); + static Log keyboard = Log._(name: 'keyboard'); + static Log input = Log._(name: 'input'); + static Log scroll = Log._(name: 'scroll'); + static Log ui = Log._(name: 'ui'); + + void error(String message) => _logger.severe(message); + void warn(String message) => _logger.warning(message); + void info(String message) => _logger.info(message); + void debug(String message) => _logger.fine(message); +} + +extension on LogLevel { + Level toLevel() { + switch (this) { + case LogLevel.off: + return Level.OFF; + case LogLevel.error: + return Level.SEVERE; + case LogLevel.warn: + return Level.WARNING; + case LogLevel.info: + return Level.INFO; + case LogLevel.debug: + return Level.FINE; + case LogLevel.all: + return Level.ALL; + } + } + + String get name { + switch (this) { + case LogLevel.off: + return 'OFF'; + case LogLevel.error: + return 'ERROR'; + case LogLevel.warn: + return 'WARN'; + case LogLevel.info: + return 'INFO'; + case LogLevel.debug: + return 'DEBUG'; + case LogLevel.all: + return 'ALL'; + } + } +} + +extension on Level { + LogLevel toLogLevel() { + if (this == Level.SEVERE) { + return LogLevel.error; + } else if (this == Level.WARNING) { + return LogLevel.warn; + } else if (this == Level.INFO) { + return LogLevel.info; + } else if (this == Level.FINE) { + return LogLevel.debug; + } + return LogLevel.off; + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart index 1607d6f649..9b7d3a730f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart @@ -83,7 +83,6 @@ class _CheckboxNodeWidgetState extends State name: check ? 'check' : 'uncheck', ), onTap: () { - debugPrint('[Checkbox] onTap...'); TransactionBuilder(widget.editorState) ..updateNode(widget.textNode, { StyleKey.checkbox: !check, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart index 95ed3546b8..9aae2b5fcb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_editor/src/infra/log.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -243,7 +244,8 @@ class _AppFlowyInputState extends State @override void updateEditingValueWithDeltas(List textEditingDeltas) { - debugPrint(textEditingDeltas.map((delta) => delta.toString()).toString()); + Log.input + .debug(textEditingDeltas.map((delta) => delta.toString()).toString()); apply(textEditingDeltas); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index 61f07fcf9f..3f49a4b566 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -1,6 +1,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/infra/html_converter.dart'; import 'package:appflowy_editor/src/document/node_iterator.dart'; +import 'package:appflowy_editor/src/infra/log.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; @@ -19,10 +20,10 @@ _handleCopy(EditorState editorState) async { startOffset: selection.start.offset, endOffset: selection.end.offset) .toHTMLString(); - debugPrint('copy html: $htmlString'); + Log.keyboard.debug('copy html: $htmlString'); RichClipboard.setData(RichClipboardData(html: htmlString)); } else { - debugPrint("unimplemented: copy non-text"); + Log.keyboard.debug('unimplemented: copy non-text'); } return; } @@ -37,7 +38,7 @@ _handleCopy(EditorState editorState) async { startOffset: selection.start.offset, endOffset: selection.end.offset) .toHTMLString(); - debugPrint('copy html: $copyString'); + Log.keyboard.debug('copy html: $copyString'); RichClipboard.setData(RichClipboardData(html: copyString)); } @@ -54,7 +55,7 @@ _pasteHTML(EditorState editorState, String html) { return; } - debugPrint('paste html: $html'); + Log.keyboard.debug('paste html: $html'); final nodes = HTMLToNodesConverter(html).toNodes(); if (nodes.isEmpty) { @@ -250,7 +251,6 @@ _handlePastePlainText(EditorState editorState, String plainText) { /// 1. copy the selected content /// 2. delete selected content _handleCut(EditorState editorState) { - debugPrint('cut'); _handleCopy(editorState); _deleteSelectedContent(editorState); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart index 3accae0186..b931ee3d61 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart @@ -97,7 +97,6 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) { if (textNodes.length == 1) { final textNode = textNodes.first; if (selection.start.offset >= textNode.delta.length) { - debugPrint("merge next line"); final nextNode = textNode.next; if (nextNode == null) { return KeyEventResult.ignored; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart index cbb345a370..8b3b8e1ebd 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; +import 'package:appflowy_editor/src/infra/log.dart'; import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; @@ -50,12 +51,6 @@ final List _popupListItems = [ icon: _popupListIcon('bullets'), handler: (editorState) => insertBulletedListAfterSelection(editorState), ), - // PopupListItem( - // text: 'Numbered list', - // keywords: ['numbered list'], - // icon: _popupListIcon('number'), - // handler: (editorState) => debugPrint('Not implement yet!'), - // ), PopupListItem( text: 'To-do List', keywords: ['checkbox', 'todo'], @@ -293,7 +288,7 @@ class _PopupListWidgetState extends State { } KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { - debugPrint('slash on key $event'); + Log.keyboard.debug('slash command, on key $event'); if (event is! RawKeyDownEvent) { return KeyEventResult.ignored; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart index 267cd782d8..3d41cdc1b5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/infra/log.dart'; import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; @@ -94,15 +95,13 @@ class _AppFlowyKeyboardState extends State @override KeyEventResult onKey(RawKeyEvent event) { - debugPrint('on keyboard event $event'); + Log.keyboard.debug('on keyboard event $event'); if (event is! RawKeyDownEvent) { return KeyEventResult.ignored; } for (final handler in widget.handlers) { - // debugPrint('handle keyboard event $event by $handler'); - KeyEventResult result = handler(widget.editorState, event); switch (result) { @@ -119,7 +118,7 @@ class _AppFlowyKeyboardState extends State } void _onFocusChange(bool value) { - debugPrint('[KeyBoard Service] focus change $value'); + Log.keyboard.debug('on keyboard event focus change $value'); } KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/render_plugin_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/render_plugin_service.dart index 64b1a9c634..2ad2989207 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/render_plugin_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/render_plugin_service.dart @@ -1,5 +1,6 @@ import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/editor_state.dart'; +import 'package:appflowy_editor/src/infra/log.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -86,7 +87,7 @@ class AppFlowyRenderPlugin extends AppFlowyRenderPluginService { @override void register(String name, NodeWidgetBuilder builder) { - debugPrint('[Plugins] registering $name...'); + Log.editor.info('registers plugin($name)...'); _validatePlugin(name); _builders[name] = builder; } @@ -111,7 +112,7 @@ class AppFlowyRenderPlugin extends AppFlowyRenderPluginService { builder: (_, child) { return Consumer( builder: ((_, value, child) { - debugPrint('Text Node is rebuilding...'); + Log.ui.debug('TextNode is rebuilding...'); return builder.build(context); }), ); @@ -122,7 +123,7 @@ class AppFlowyRenderPlugin extends AppFlowyRenderPluginService { builder: (_, child) { return Consumer( builder: ((_, value, child) { - debugPrint('Node is rebuilding...'); + Log.ui.debug('Node is rebuilding...'); return builder.build(context); }), ); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/scroll_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/scroll_service.dart index 9fba0ec293..d68e686d61 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/scroll_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/scroll_service.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_editor/src/infra/log.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:appflowy_editor/src/extensions/object_extensions.dart'; @@ -113,13 +114,13 @@ class _AppFlowyScrollState extends State @override void disable() { _scrollEnabled = false; - debugPrint('[scroll] $_scrollEnabled'); + Log.scroll.debug('disable scroll service'); } @override void enable() { _scrollEnabled = true; - debugPrint('[scroll] $_scrollEnabled'); + Log.scroll.debug('enable scroll service'); } void _onPointerSignal(PointerSignalEvent event) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart index bcce83a0c6..c5e351059c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_editor/src/infra/log.dart'; import 'package:flutter/material.dart'; import 'package:appflowy_editor/src/document/node.dart'; @@ -185,12 +186,12 @@ class _AppFlowySelectionState extends State if (selection != null) { if (selection.isCollapsed) { - /// updates cursor area. - debugPrint('updating cursor, $selection'); + // updates cursor area. + Log.selection.debug('update cursor area, $selection'); _updateCursorAreas(selection.start); } else { // updates selection area. - debugPrint('updating selection, $selection'); + Log.selection.debug('update cursor area, $selection'); _updateSelectionAreas(selection); } } @@ -312,14 +313,10 @@ class _AppFlowySelectionState extends State // compute the selection in range. if (first != null && last != null) { - bool isDownward = (identical(first, last)) - ? panStartOffset.dx < panEndOffset.dx - : panStartOffset.dy < panEndOffset.dy; final start = first.getSelectionInRange(panStartOffset, panEndOffset).start; final end = last.getSelectionInRange(panStartOffset, panEndOffset).end; final selection = Selection(start: start, end: end); - debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection'); updateSelection(selection); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart index 43d0eeaa35..cfa3f75688 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart @@ -1,11 +1,11 @@ import 'dart:collection'; import 'package:appflowy_editor/src/document/selection.dart'; +import 'package:appflowy_editor/src/infra/log.dart'; import 'package:appflowy_editor/src/operation/operation.dart'; import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import 'package:appflowy_editor/src/operation/transaction.dart'; import 'package:appflowy_editor/src/editor_state.dart'; -import 'package:flutter/foundation.dart'; /// A [HistoryItem] contains list of operations committed by users. /// If a [HistoryItem] is not sealed, operations can be added sequentially. @@ -112,7 +112,7 @@ class UndoManager { } undo() { - debugPrint('undo'); + Log.editor.debug('undo'); final s = state; if (s == null) { return; @@ -131,7 +131,7 @@ class UndoManager { } redo() { - debugPrint('redo'); + Log.editor.debug('redo'); final s = state; if (s == null) { return; diff --git a/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml index 9a7318659f..6d85b431c9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: flutter_svg: ^1.1.1+1 provider: ^6.0.3 url_launcher: ^6.1.5 + logging: ^1.0.2 dev_dependencies: flutter_test: diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/log_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/log_test.dart new file mode 100644 index 0000000000..f49a32c130 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/log_test.dart @@ -0,0 +1,169 @@ +import 'package:appflowy_editor/src/infra/log.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'test_editor.dart'; + +void main() async { + group('log.dart', () { + testWidgets('test LogConfiguration in EditorState', (tester) async { + TestWidgetsFlutterBinding.ensureInitialized(); + + const text = 'Welcome to Appflowy 😁'; + + final List logs = []; + + final editor = tester.editor; + editor.editorState.logConfiguration + ..level = LogLevel.all + ..handler = (message) { + logs.add(message); + }; + + Log.editor.debug(text); + expect(logs.last.contains('DEBUG'), true); + expect(logs.length, 1); + }); + + test('test LogLevel.all', () { + const text = 'Welcome to Appflowy 😁'; + + final List logs = []; + LogConfiguration() + ..level = LogLevel.all + ..handler = (message) { + logs.add(message); + }; + + Log.editor.debug(text); + expect(logs.last.contains('DEBUG'), true); + Log.editor.info(text); + expect(logs.last.contains('INFO'), true); + Log.editor.warn(text); + expect(logs.last.contains('WARN'), true); + Log.editor.error(text); + expect(logs.last.contains('ERROR'), true); + + expect(logs.length, 4); + }); + + test('test LogLevel.off', () { + const text = 'Welcome to Appflowy 😁'; + + final List logs = []; + LogConfiguration() + ..level = LogLevel.off + ..handler = (message) { + logs.add(message); + }; + + Log.editor.debug(text); + Log.editor.info(text); + Log.editor.warn(text); + Log.editor.error(text); + + expect(logs.length, 0); + }); + + test('test LogLevel.error', () { + const text = 'Welcome to Appflowy 😁'; + + final List logs = []; + LogConfiguration() + ..level = LogLevel.error + ..handler = (message) { + logs.add(message); + }; + + Log.editor.debug(text); + Log.editor.info(text); + Log.editor.warn(text); + Log.editor.error(text); + + expect(logs.length, 1); + }); + + test('test LogLevel.warn', () { + const text = 'Welcome to Appflowy 😁'; + + final List logs = []; + LogConfiguration() + ..level = LogLevel.warn + ..handler = (message) { + logs.add(message); + }; + + Log.editor.debug(text); + Log.editor.info(text); + Log.editor.warn(text); + Log.editor.error(text); + + expect(logs.length, 2); + }); + + test('test LogLevel.info', () { + const text = 'Welcome to Appflowy 😁'; + + final List logs = []; + LogConfiguration() + ..level = LogLevel.info + ..handler = (message) { + logs.add(message); + }; + + Log.editor.debug(text); + Log.editor.info(text); + Log.editor.warn(text); + Log.editor.error(text); + + expect(logs.length, 3); + }); + + test('test LogLevel.debug', () { + const text = 'Welcome to Appflowy 😁'; + + final List logs = []; + LogConfiguration() + ..level = LogLevel.debug + ..handler = (message) { + logs.add(message); + }; + + Log.editor.debug(text); + Log.editor.info(text); + Log.editor.warn(text); + Log.editor.error(text); + + expect(logs.length, 4); + }); + + test('test logger', () { + const text = 'Welcome to Appflowy 😁'; + + final List logs = []; + LogConfiguration() + ..level = LogLevel.all + ..handler = (message) { + logs.add(message); + }; + + Log.editor.debug(text); + expect(logs.last.contains('editor'), true); + + Log.selection.debug(text); + expect(logs.last.contains('selection'), true); + + Log.keyboard.debug(text); + expect(logs.last.contains('keyboard'), true); + + Log.input.debug(text); + expect(logs.last.contains('input'), true); + + Log.scroll.debug(text); + expect(logs.last.contains('scroll'), true); + + Log.ui.debug(text); + expect(logs.last.contains('ui'), true); + + expect(logs.length, 6); + }); + }); +}