Merge pull request #878 from LucasXu0/logger_refactor

feat: [improvements] integrate logging library in AppFlowyEditor
This commit is contained in:
Nathan.fooo 2022-08-19 21:07:35 +08:00 committed by GitHub
commit 1d2dd15142
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 351 additions and 39 deletions

View File

@ -97,9 +97,13 @@ class _MyHomePageState extends State<MyHomePage> {
builder: (context, snapshot) {
if (snapshot.hasData) {
final data = Map<String, Object>.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(),

View File

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

View File

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

View File

@ -0,0 +1,138 @@
import 'package:logging/logging.dart';
enum LogLevel {
off,
error,
warn,
info,
debug,
all,
}
typedef LogHandler = void Function(String message);
/// Manages log service for [AppFlowyEditor]
///
/// Set the log level and config the handler depending on your need.
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();
}
}
/// For logging message in AppFlowyEditor
class Log {
Log._({
required this.name,
}) : _logger = Logger(name);
final String name;
late final Logger _logger;
/// For logging message related to [AppFlowyEditor].
///
/// For example, uses the logger when registering plugins
/// or handling something related to [EditorState].
static Log editor = Log._(name: 'editor');
/// For logging message related to [AppFlowySelectionService].
///
/// For example, uses the logger when updating or clearing selection.
static Log selection = Log._(name: 'selection');
/// For logging message related to [AppFlowyKeyboardService].
///
/// For example, uses the logger when processing shortcut events.
static Log keyboard = Log._(name: 'keyboard');
/// For logging message related to [AppFlowyInputService].
///
/// For example, uses the logger when processing text inputs.
static Log input = Log._(name: 'input');
/// For logging message related to [AppFlowyScrollService].
///
/// For example, uses the logger when processing scroll events.
static Log scroll = Log._(name: 'scroll');
/// For logging message related to UI.
///
/// For example, uses the logger when building the widget.
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',
),
onTap: () {
debugPrint('[Checkbox] onTap...');
TransactionBuilder(widget.editorState)
..updateNode(widget.textNode, {
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/services.dart';
@ -243,7 +244,8 @@ class _AppFlowyInputState extends State<AppFlowyInput>
@override
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
debugPrint(textEditingDeltas.map((delta) => delta.toString()).toString());
Log.input
.debug(textEditingDeltas.map((delta) => delta.toString()).toString());
apply(textEditingDeltas);
}

View File

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

View File

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

View File

@ -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<PopupListItem> _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<PopupListWidget> {
}
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;
}

View File

@ -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<AppFlowyKeyboard>
@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<AppFlowyKeyboard>
}
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) {

View File

@ -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<TextNode>(
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<Node>(
builder: ((_, value, child) {
debugPrint('Node is rebuilding...');
Log.ui.debug('Node is rebuilding...');
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/material.dart';
import 'package:appflowy_editor/src/extensions/object_extensions.dart';
@ -113,13 +114,13 @@ class _AppFlowyScrollState extends State<AppFlowyScroll>
@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) {

View File

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

View File

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

View File

@ -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:

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