feat: [improvements] integrate logging library in AppFlowyEditor

This commit is contained in:
Lucas.Xu 2022-08-19 14:02:34 +08:00
parent a0753cea2d
commit c36ccc39ce
16 changed files with 322 additions and 39 deletions

View File

@ -97,9 +97,13 @@ class _MyHomePageState extends State<MyHomePage> {
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
final data = Map<String, Object>.from(json.decode(snapshot.data!)); final data = Map<String, Object>.from(json.decode(snapshot.data!));
return _buildAppFlowyEditor(EditorState( final editorState = EditorState(document: StateTree.fromJson(data));
document: StateTree.fromJson(data), editorState.logConfiguration
)); ..level = LogLevel.all
..handler = (message) {
debugPrint(message);
};
return _buildAppFlowyEditor(editorState);
} else { } else {
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),

View File

@ -1,6 +1,7 @@
/// AppFlowyEditor library /// AppFlowyEditor library
library appflowy_editor; library appflowy_editor;
export 'src/infra/log.dart';
export 'src/document/node.dart'; export 'src/document/node.dart';
export 'src/document/path.dart'; export 'src/document/path.dart';
export 'src/document/position.dart'; export 'src/document/position.dart';

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:appflowy_editor/src/infra/log.dart';
import 'package:appflowy_editor/src/service/service.dart'; import 'package:appflowy_editor/src/service/service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -48,10 +49,15 @@ class EditorState {
// Service reference. // Service reference.
final service = FlowyService(); 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(); final UndoManager undoManager = UndoManager();
Selection? _cursorSelection; Selection? _cursorSelection;
/// TODO: only for testing. // TODO: only for testing.
bool disableSealTimer = false; bool disableSealTimer = false;
Selection? get cursorSelection { Selection? get cursorSelection {
@ -120,7 +126,7 @@ class EditorState {
_debouncedSealHistoryItemTimer = _debouncedSealHistoryItemTimer =
Timer(const Duration(milliseconds: 1000), () { Timer(const Duration(milliseconds: 1000), () {
if (undoManager.undoStack.isNonEmpty) { if (undoManager.undoStack.isNonEmpty) {
debugPrint('Seal history item'); Log.editor.debug('Seal history item');
final last = undoManager.undoStack.last; final last = undoManager.undoStack.last;
last.seal(); last.seal();
} }

View File

@ -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;
}
}

View File

@ -83,7 +83,6 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
name: check ? 'check' : 'uncheck', name: check ? 'check' : 'uncheck',
), ),
onTap: () { onTap: () {
debugPrint('[Checkbox] onTap...');
TransactionBuilder(widget.editorState) TransactionBuilder(widget.editorState)
..updateNode(widget.textNode, { ..updateNode(widget.textNode, {
StyleKey.checkbox: !check, StyleKey.checkbox: !check,

View File

@ -1,3 +1,4 @@
import 'package:appflowy_editor/src/infra/log.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -243,7 +244,8 @@ class _AppFlowyInputState extends State<AppFlowyInput>
@override @override
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) { void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
debugPrint(textEditingDeltas.map((delta) => delta.toString()).toString()); Log.input
.debug(textEditingDeltas.map((delta) => delta.toString()).toString());
apply(textEditingDeltas); apply(textEditingDeltas);
} }

View File

@ -1,6 +1,7 @@
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/infra/html_converter.dart'; import 'package:appflowy_editor/src/infra/html_converter.dart';
import 'package:appflowy_editor/src/document/node_iterator.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/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:rich_clipboard/rich_clipboard.dart'; import 'package:rich_clipboard/rich_clipboard.dart';
@ -19,10 +20,10 @@ _handleCopy(EditorState editorState) async {
startOffset: selection.start.offset, startOffset: selection.start.offset,
endOffset: selection.end.offset) endOffset: selection.end.offset)
.toHTMLString(); .toHTMLString();
debugPrint('copy html: $htmlString'); Log.keyboard.debug('copy html: $htmlString');
RichClipboard.setData(RichClipboardData(html: htmlString)); RichClipboard.setData(RichClipboardData(html: htmlString));
} else { } else {
debugPrint("unimplemented: copy non-text"); Log.keyboard.debug('unimplemented: copy non-text');
} }
return; return;
} }
@ -37,7 +38,7 @@ _handleCopy(EditorState editorState) async {
startOffset: selection.start.offset, startOffset: selection.start.offset,
endOffset: selection.end.offset) endOffset: selection.end.offset)
.toHTMLString(); .toHTMLString();
debugPrint('copy html: $copyString'); Log.keyboard.debug('copy html: $copyString');
RichClipboard.setData(RichClipboardData(html: copyString)); RichClipboard.setData(RichClipboardData(html: copyString));
} }
@ -54,7 +55,7 @@ _pasteHTML(EditorState editorState, String html) {
return; return;
} }
debugPrint('paste html: $html'); Log.keyboard.debug('paste html: $html');
final nodes = HTMLToNodesConverter(html).toNodes(); final nodes = HTMLToNodesConverter(html).toNodes();
if (nodes.isEmpty) { if (nodes.isEmpty) {
@ -250,7 +251,6 @@ _handlePastePlainText(EditorState editorState, String plainText) {
/// 1. copy the selected content /// 1. copy the selected content
/// 2. delete selected content /// 2. delete selected content
_handleCut(EditorState editorState) { _handleCut(EditorState editorState) {
debugPrint('cut');
_handleCopy(editorState); _handleCopy(editorState);
_deleteSelectedContent(editorState); _deleteSelectedContent(editorState);
} }

View File

@ -97,7 +97,6 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
if (textNodes.length == 1) { if (textNodes.length == 1) {
final textNode = textNodes.first; final textNode = textNodes.first;
if (selection.start.offset >= textNode.delta.length) { if (selection.start.offset >= textNode.delta.length) {
debugPrint("merge next line");
final nextNode = textNode.next; final nextNode = textNode.next;
if (nextNode == null) { if (nextNode == null) {
return KeyEventResult.ignored; return KeyEventResult.ignored;

View File

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.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/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.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'; import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
@ -50,12 +51,6 @@ final List<PopupListItem> _popupListItems = [
icon: _popupListIcon('bullets'), icon: _popupListIcon('bullets'),
handler: (editorState) => insertBulletedListAfterSelection(editorState), handler: (editorState) => insertBulletedListAfterSelection(editorState),
), ),
// PopupListItem(
// text: 'Numbered list',
// keywords: ['numbered list'],
// icon: _popupListIcon('number'),
// handler: (editorState) => debugPrint('Not implement yet!'),
// ),
PopupListItem( PopupListItem(
text: 'To-do List', text: 'To-do List',
keywords: ['checkbox', 'todo'], keywords: ['checkbox', 'todo'],
@ -293,7 +288,7 @@ class _PopupListWidgetState extends State<PopupListWidget> {
} }
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
debugPrint('slash on key $event'); Log.keyboard.debug('slash command, on key $event');
if (event is! RawKeyDownEvent) { if (event is! RawKeyDownEvent) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }

View File

@ -1,4 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/infra/log.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -94,15 +95,13 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
@override @override
KeyEventResult onKey(RawKeyEvent event) { KeyEventResult onKey(RawKeyEvent event) {
debugPrint('on keyboard event $event'); Log.keyboard.debug('on keyboard event $event');
if (event is! RawKeyDownEvent) { if (event is! RawKeyDownEvent) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
for (final handler in widget.handlers) { for (final handler in widget.handlers) {
// debugPrint('handle keyboard event $event by $handler');
KeyEventResult result = handler(widget.editorState, event); KeyEventResult result = handler(widget.editorState, event);
switch (result) { switch (result) {
@ -119,7 +118,7 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
} }
void _onFocusChange(bool value) { 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) { KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {

View File

@ -1,5 +1,6 @@
import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/infra/log.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -86,7 +87,7 @@ class AppFlowyRenderPlugin extends AppFlowyRenderPluginService {
@override @override
void register(String name, NodeWidgetBuilder builder) { void register(String name, NodeWidgetBuilder builder) {
debugPrint('[Plugins] registering $name...'); Log.editor.info('registers plugin($name)...');
_validatePlugin(name); _validatePlugin(name);
_builders[name] = builder; _builders[name] = builder;
} }
@ -111,7 +112,7 @@ class AppFlowyRenderPlugin extends AppFlowyRenderPluginService {
builder: (_, child) { builder: (_, child) {
return Consumer<TextNode>( return Consumer<TextNode>(
builder: ((_, value, child) { builder: ((_, value, child) {
debugPrint('Text Node is rebuilding...'); Log.ui.debug('TextNode is rebuilding...');
return builder.build(context); return builder.build(context);
}), }),
); );
@ -122,7 +123,7 @@ class AppFlowyRenderPlugin extends AppFlowyRenderPluginService {
builder: (_, child) { builder: (_, child) {
return Consumer<Node>( return Consumer<Node>(
builder: ((_, value, child) { builder: ((_, value, child) {
debugPrint('Node is rebuilding...'); Log.ui.debug('Node is rebuilding...');
return builder.build(context); return builder.build(context);
}), }),
); );

View File

@ -1,3 +1,4 @@
import 'package:appflowy_editor/src/infra/log.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/extensions/object_extensions.dart'; import 'package:appflowy_editor/src/extensions/object_extensions.dart';
@ -113,13 +114,13 @@ class _AppFlowyScrollState extends State<AppFlowyScroll>
@override @override
void disable() { void disable() {
_scrollEnabled = false; _scrollEnabled = false;
debugPrint('[scroll] $_scrollEnabled'); Log.scroll.debug('disable scroll service');
} }
@override @override
void enable() { void enable() {
_scrollEnabled = true; _scrollEnabled = true;
debugPrint('[scroll] $_scrollEnabled'); Log.scroll.debug('enable scroll service');
} }
void _onPointerSignal(PointerSignalEvent event) { void _onPointerSignal(PointerSignalEvent event) {

View File

@ -1,3 +1,4 @@
import 'package:appflowy_editor/src/infra/log.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/document/node.dart';
@ -185,12 +186,12 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
if (selection != null) { if (selection != null) {
if (selection.isCollapsed) { if (selection.isCollapsed) {
/// updates cursor area. // updates cursor area.
debugPrint('updating cursor, $selection'); Log.selection.debug('update cursor area, $selection');
_updateCursorAreas(selection.start); _updateCursorAreas(selection.start);
} else { } else {
// updates selection area. // updates selection area.
debugPrint('updating selection, $selection'); Log.selection.debug('update cursor area, $selection');
_updateSelectionAreas(selection); _updateSelectionAreas(selection);
} }
} }
@ -312,14 +313,10 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
// compute the selection in range. // compute the selection in range.
if (first != null && last != null) { if (first != null && last != null) {
bool isDownward = (identical(first, last))
? panStartOffset.dx < panEndOffset.dx
: panStartOffset.dy < panEndOffset.dy;
final start = final start =
first.getSelectionInRange(panStartOffset, panEndOffset).start; first.getSelectionInRange(panStartOffset, panEndOffset).start;
final end = last.getSelectionInRange(panStartOffset, panEndOffset).end; final end = last.getSelectionInRange(panStartOffset, panEndOffset).end;
final selection = Selection(start: start, end: end); final selection = Selection(start: start, end: end);
debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection');
updateSelection(selection); updateSelection(selection);
} }

View File

@ -1,11 +1,11 @@
import 'dart:collection'; import 'dart:collection';
import 'package:appflowy_editor/src/document/selection.dart'; 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/operation.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import 'package:appflowy_editor/src/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/operation/transaction.dart'; import 'package:appflowy_editor/src/operation/transaction.dart';
import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/editor_state.dart';
import 'package:flutter/foundation.dart';
/// A [HistoryItem] contains list of operations committed by users. /// A [HistoryItem] contains list of operations committed by users.
/// If a [HistoryItem] is not sealed, operations can be added sequentially. /// If a [HistoryItem] is not sealed, operations can be added sequentially.
@ -112,7 +112,7 @@ class UndoManager {
} }
undo() { undo() {
debugPrint('undo'); Log.editor.debug('undo');
final s = state; final s = state;
if (s == null) { if (s == null) {
return; return;
@ -131,7 +131,7 @@ class UndoManager {
} }
redo() { redo() {
debugPrint('redo'); Log.editor.debug('redo');
final s = state; final s = state;
if (s == null) { if (s == null) {
return; return;

View File

@ -16,6 +16,7 @@ dependencies:
flutter_svg: ^1.1.1+1 flutter_svg: ^1.1.1+1
provider: ^6.0.3 provider: ^6.0.3
url_launcher: ^6.1.5 url_launcher: ^6.1.5
logging: ^1.0.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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);
});
});
}