diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/OFL.txt b/frontend/app_flowy/assets/google_fonts/Poppins/OFL.txt new file mode 100644 index 0000000000..246c977c9f --- /dev/null +++ b/frontend/app_flowy/assets/google_fonts/Poppins/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Black.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Black.ttf new file mode 100644 index 0000000000..71c0f995ee Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Black.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-BlackItalic.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-BlackItalic.ttf new file mode 100644 index 0000000000..7aeb58bd1b Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-BlackItalic.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Bold.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Bold.ttf new file mode 100644 index 0000000000..00559eeb29 Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Bold.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-BoldItalic.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-BoldItalic.ttf new file mode 100644 index 0000000000..e61e8e88bd Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-BoldItalic.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraBold.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraBold.ttf new file mode 100644 index 0000000000..df7093608a Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraBold.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraBoldItalic.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraBoldItalic.ttf new file mode 100644 index 0000000000..14d2b375dc Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraBoldItalic.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraLight.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraLight.ttf new file mode 100644 index 0000000000..e76ec69a65 Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraLight.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraLightItalic.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraLightItalic.ttf new file mode 100644 index 0000000000..89513d9469 Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ExtraLightItalic.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Italic.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Italic.ttf new file mode 100644 index 0000000000..12b7b3c40b Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Italic.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Light.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Light.ttf new file mode 100644 index 0000000000..bc36bcc242 Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Light.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-LightItalic.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-LightItalic.ttf new file mode 100644 index 0000000000..9e70be6a9e Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-LightItalic.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Medium.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Medium.ttf new file mode 100644 index 0000000000..6bcdcc27f2 Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Medium.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-MediumItalic.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-MediumItalic.ttf new file mode 100644 index 0000000000..be67410fd0 Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-MediumItalic.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Regular.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Regular.ttf new file mode 100644 index 0000000000..9f0c71b70a Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Regular.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-SemiBold.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-SemiBold.ttf new file mode 100644 index 0000000000..74c726e327 Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-SemiBold.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-SemiBoldItalic.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-SemiBoldItalic.ttf new file mode 100644 index 0000000000..3e6c942233 Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-SemiBoldItalic.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Thin.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Thin.ttf new file mode 100644 index 0000000000..03e736613a Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Thin.ttf differ diff --git a/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ThinItalic.ttf b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ThinItalic.ttf new file mode 100644 index 0000000000..e26db5dd3d Binary files /dev/null and b/frontend/app_flowy/assets/google_fonts/Poppins/Poppins-ThinItalic.ttf differ diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index e22677b9eb..564dffd7c7 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -24,7 +24,7 @@ class BoardPluginBuilder implements PluginBuilder { PluginType get pluginType => PluginType.board; @override - ViewDataTypePB get dataType => ViewDataTypePB.Database; + ViewDataFormatPB get dataFormatType => ViewDataFormatPB.DatabaseFormat; @override ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Board; diff --git a/frontend/app_flowy/lib/plugins/doc/application/doc_bloc.dart b/frontend/app_flowy/lib/plugins/doc/application/doc_bloc.dart index 0f4b355624..4e542cb433 100644 --- a/frontend/app_flowy/lib/plugins/doc/application/doc_bloc.dart +++ b/frontend/app_flowy/lib/plugins/doc/application/doc_bloc.dart @@ -2,10 +2,11 @@ import 'dart:convert'; import 'package:app_flowy/plugins/trash/application/trash_service.dart'; import 'package:app_flowy/workspace/application/view/view_listener.dart'; import 'package:app_flowy/plugins/doc/application/doc_service.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' + show EditorState, Document, Transaction; import 'package:flowy_sdk/protobuf/flowy-folder/trash.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; -import 'package:flutter_quill/flutter_quill.dart' show Document, Delta; import 'package:flowy_sdk/log.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -14,15 +15,13 @@ import 'dart:async'; part 'doc_bloc.freezed.dart'; -typedef FlutterQuillDocument = Document; - class DocumentBloc extends Bloc { final ViewPB view; final DocumentService service; final ViewListener listener; final TrashService trashService; - late FlutterQuillDocument document; + late EditorState editorState; StreamSubscription? _subscription; DocumentBloc({ @@ -35,6 +34,7 @@ class DocumentBloc extends Bloc { await event.map( initial: (Initial value) async { await _initial(value, emit); + _listenOnViewChange(); }, deleted: (Deleted value) async { emit(state.copyWith(isDeleted: true)); @@ -73,6 +73,29 @@ class DocumentBloc extends Bloc { } Future _initial(Initial value, Emitter emit) async { + final result = await service.openDocument(view: view); + result.fold( + (block) { + final document = Document.fromJson(jsonDecode(block.snapshot)); + editorState = EditorState(document: document); + _listenOnDocumentChange(); + emit( + state.copyWith( + loadingState: DocumentLoadingState.finish(left(unit)), + ), + ); + }, + (err) { + emit( + state.copyWith( + loadingState: DocumentLoadingState.finish(right(err)), + ), + ); + }, + ); + } + + void _listenOnViewChange() { listener.start( onViewDeleted: (result) { result.fold( @@ -87,46 +110,18 @@ class DocumentBloc extends Bloc { ); }, ); - final result = await service.openDocument(docId: view.id); - result.fold( - (block) { - document = _decodeJsonToDocument(block.snapshot); - _subscription = document.changes.listen((event) { - final delta = event.item2; - final documentDelta = document.toDelta(); - _composeDelta(delta, documentDelta); - }); - emit(state.copyWith( - loadingState: DocumentLoadingState.finish(left(unit)))); - }, - (err) { - emit(state.copyWith( - loadingState: DocumentLoadingState.finish(right(err)))); - }, - ); } - // Document _decodeListToDocument(Uint8List data) { - // final json = jsonDecode(utf8.decode(data)); - // final document = Document.fromJson(json); - // return document; - // } - - void _composeDelta(Delta composedDelta, Delta documentDelta) async { - final json = jsonEncode(composedDelta.toJson()); - Log.debug("doc_id: $view.id - Send json: $json"); - final result = await service.applyEdit(docId: view.id, operations: json); - - result.fold( - (_) {}, - (r) => Log.error(r), - ); - } - - Document _decodeJsonToDocument(String data) { - final json = jsonDecode(data); - final document = Document.fromJson(json); - return document; + void _listenOnDocumentChange() { + _subscription = editorState.transactionStream.listen((transaction) { + final json = jsonEncode(TransactionAdaptor(transaction).toJson()); + service.applyEdit(docId: view.id, operations: json).then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + }); } } @@ -160,3 +155,44 @@ class DocumentLoadingState with _$DocumentLoadingState { const factory DocumentLoadingState.finish( Either successOrFail) = _Finish; } + +/// Uses to erase the different between appflowy editor and the backend +class TransactionAdaptor { + final Transaction transaction; + TransactionAdaptor(this.transaction); + + Map toJson() { + final json = {}; + if (transaction.operations.isNotEmpty) { + // The backend uses [0,0] as the beginning path, but the editor uses [0]. + // So it needs to extend the path by inserting `0` at the head for all + // operations before passing to the backend. + json['operations'] = transaction.operations + .map((e) => e.copyWith(path: [0, ...e.path]).toJson()) + .toList(); + } + if (transaction.afterSelection != null) { + final selection = transaction.afterSelection!; + final start = selection.start; + final end = selection.end; + json['after_selection'] = selection + .copyWith( + start: start.copyWith(path: [0, ...start.path]), + end: end.copyWith(path: [0, ...end.path]), + ) + .toJson(); + } + if (transaction.beforeSelection != null) { + final selection = transaction.beforeSelection!; + final start = selection.start; + final end = selection.end; + json['before_selection'] = selection + .copyWith( + start: start.copyWith(path: [0, ...start.path]), + end: end.copyWith(path: [0, ...end.path]), + ) + .toJson(); + } + return json; + } +} diff --git a/frontend/app_flowy/lib/plugins/doc/application/doc_service.dart b/frontend/app_flowy/lib/plugins/doc/application/doc_service.dart index bcc920a2cc..c967221c9c 100644 --- a/frontend/app_flowy/lib/plugins/doc/application/doc_service.dart +++ b/frontend/app_flowy/lib/plugins/doc/application/doc_service.dart @@ -3,16 +3,25 @@ import 'package:flowy_sdk/dispatch/dispatch.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-sync/document.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart'; class DocumentService { Future> openDocument({ - required String docId, + required ViewPB view, }) async { - await FolderEventSetLatestView(ViewIdPB(value: docId)).send(); + await FolderEventSetLatestView(ViewIdPB(value: view.id)).send(); + + final payload = OpenDocumentContextPB() + ..documentId = view.id + ..documentVersion = DocumentVersionPB.V1; + // switch (view.dataFormat) { + // case ViewDataFormatPB.DeltaFormat: + // payload.documentVersion = DocumentVersionPB.V0; + // break; + // default: + // break; + // } - final payload = DocumentIdPB(value: docId); return DocumentEventGetDocument(payload).send(); } diff --git a/frontend/app_flowy/lib/plugins/doc/application/share_bloc.dart b/frontend/app_flowy/lib/plugins/doc/application/share_bloc.dart index fa7dbb761a..a37d3eafeb 100644 --- a/frontend/app_flowy/lib/plugins/doc/application/share_bloc.dart +++ b/frontend/app_flowy/lib/plugins/doc/application/share_bloc.dart @@ -1,14 +1,16 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:app_flowy/startup/tasks/rust_sdk.dart'; -import 'package:app_flowy/workspace/application/markdown/delta_markdown.dart'; import 'package:app_flowy/plugins/doc/application/share_service.dart'; +import 'package:app_flowy/workspace/application/markdown/document_markdown.dart'; import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:dartz/dartz.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' show Document; part 'share_bloc.freezed.dart'; class DocShareBloc extends Bloc { @@ -19,10 +21,10 @@ class DocShareBloc extends Bloc { on((event, emit) async { await event.map( shareMarkdown: (ShareMarkdown value) async { - await service.exportMarkdown(view.id).then((result) { + await service.exportMarkdown(view).then((result) { result.fold( - (value) => emit( - DocShareState.finish(left(_convertDeltaToMarkdown(value)))), + (value) => emit(DocShareState.finish( + left(_convertDocumentToMarkdown(value)))), (error) => emit(DocShareState.finish(right(error))), ); }); @@ -35,8 +37,10 @@ class DocShareBloc extends Bloc { }); } - ExportDataPB _convertDeltaToMarkdown(ExportDataPB value) { - final result = deltaToMarkdown(value.data); + ExportDataPB _convertDocumentToMarkdown(ExportDataPB value) { + final json = jsonDecode(value.data); + final document = Document.fromJson(json); + final result = documentToMarkdown(document); value.data = result; writeFile(result); return value; diff --git a/frontend/app_flowy/lib/plugins/doc/application/share_service.dart b/frontend/app_flowy/lib/plugins/doc/application/share_service.dart index 75d045199b..5bb0ef0d4d 100644 --- a/frontend/app_flowy/lib/plugins/doc/application/share_service.dart +++ b/frontend/app_flowy/lib/plugins/doc/application/share_service.dart @@ -3,26 +3,28 @@ import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/dispatch/dispatch.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-document/protobuf.dart'; +import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; class ShareService { Future> export( - String docId, ExportType type) { - final request = ExportPayloadPB.create() - ..viewId = docId - ..exportType = type; + ViewPB view, ExportType type) { + var payload = ExportPayloadPB.create() + ..viewId = view.id + ..exportType = type + ..documentVersion = DocumentVersionPB.V1; - return DocumentEventExportDocument(request).send(); + return DocumentEventExportDocument(payload).send(); } - Future> exportText(String docId) { - return export(docId, ExportType.Text); + Future> exportText(ViewPB view) { + return export(view, ExportType.Text); } - Future> exportMarkdown(String docId) { - return export(docId, ExportType.Markdown); + Future> exportMarkdown(ViewPB view) { + return export(view, ExportType.Markdown); } - Future> exportURL(String docId) { - return export(docId, ExportType.Link); + Future> exportURL(ViewPB view) { + return export(view, ExportType.Link); } } diff --git a/frontend/app_flowy/lib/plugins/doc/document.dart b/frontend/app_flowy/lib/plugins/doc/document.dart index 55296f7831..73456993be 100644 --- a/frontend/app_flowy/lib/plugins/doc/document.dart +++ b/frontend/app_flowy/lib/plugins/doc/document.dart @@ -44,7 +44,7 @@ class DocumentPluginBuilder extends PluginBuilder { PluginType get pluginType => PluginType.editor; @override - ViewDataTypePB get dataType => ViewDataTypePB.Text; + ViewDataFormatPB get dataFormatType => ViewDataFormatPB.TreeFormat; } class DocumentPlugin extends Plugin { diff --git a/frontend/app_flowy/lib/plugins/doc/document_page.dart b/frontend/app_flowy/lib/plugins/doc/document_page.dart index 8593766087..fec32ca76f 100644 --- a/frontend/app_flowy/lib/plugins/doc/document_page.dart +++ b/frontend/app_flowy/lib/plugins/doc/document_page.dart @@ -1,17 +1,14 @@ +import 'package:app_flowy/plugins/doc/editor_styles.dart'; +import 'package:app_flowy/plugins/doc/presentation/plugins/horizontal_rule_node_widget.dart'; import 'package:app_flowy/startup/startup.dart'; -import 'package:app_flowy/workspace/application/appearance.dart'; import 'package:app_flowy/plugins/doc/presentation/banner.dart'; -import 'package:app_flowy/plugins/doc/presentation/toolbar/tool_bar.dart'; -import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter_quill/flutter_quill.dart' as quill; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:provider/provider.dart'; +import 'package:intl/intl.dart'; import 'application/doc_bloc.dart'; -import 'styles.dart'; class DocumentPage extends StatefulWidget { final VoidCallback onDeleted; @@ -29,11 +26,12 @@ class DocumentPage extends StatefulWidget { class _DocumentPageState extends State { late DocumentBloc documentBloc; - final scrollController = ScrollController(); final FocusNode _focusNode = FocusNode(); @override void initState() { + // The appflowy editor use Intl as locatization, set the default language as fallback. + Intl.defaultLocale = 'en_US'; documentBloc = getIt(param1: super.widget.view) ..add(const DocumentEvent.initial()); super.initState(); @@ -48,9 +46,9 @@ class _DocumentPageState extends State { child: BlocBuilder(builder: (context, state) { return state.loadingState.map( - // loading: (_) => const FlowyProgressIndicator(), - loading: (_) => - SizedBox.expand(child: Container(color: Colors.transparent)), + loading: (_) => SizedBox.expand( + child: Container(color: Colors.transparent), + ), finish: (result) => result.successOrFail.fold( (_) { if (state.forceClose) { @@ -75,24 +73,11 @@ class _DocumentPageState extends State { } Widget _renderDocument(BuildContext context, DocumentState state) { - quill.QuillController controller = quill.QuillController( - document: context.read().document, - selection: const TextSelection.collapsed(offset: 0), - ); return Column( children: [ if (state.isDeleted) _renderBanner(context), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _renderEditor(controller), - const VSpace(10), - _renderToolbar(controller), - const VSpace(10), - ], - ), - ), + // AppFlowy Editor + _renderAppFlowyEditor(context.read().editorState), ], ); } @@ -107,36 +92,20 @@ class _DocumentPageState extends State { ); } - Widget _renderEditor(quill.QuillController controller) { - final editor = quill.QuillEditor( - controller: controller, - focusNode: _focusNode, - scrollable: true, - paintCursorAboveText: true, - autoFocus: controller.document.isEmpty(), - expands: false, - padding: const EdgeInsets.symmetric(horizontal: 8.0), - readOnly: false, - scrollBottomInset: 0, - scrollController: scrollController, - customStyles: customStyles(context), + Widget _renderAppFlowyEditor(EditorState editorState) { + final editor = AppFlowyEditor( + editorState: editorState, + editorStyle: customEditorStyle(context), + customBuilders: { + 'horizontal_rule': HorizontalRuleWidgetBuilder(), + }, + shortcutEvents: [ + insertHorizontalRule, + ], ); - return Expanded( - child: ScrollbarListStack( - axis: Axis.vertical, - controller: scrollController, - barSize: 6.0, - child: SizedBox.expand(child: editor), - ), - ); - } - - Widget _renderToolbar(quill.QuillController controller) { - return ChangeNotifierProvider.value( - value: Provider.of(context, listen: true), - child: EditorToolbar.basic( - controller: controller, + child: SizedBox.expand( + child: editor, ), ); } diff --git a/frontend/app_flowy/lib/plugins/doc/editor_styles.dart b/frontend/app_flowy/lib/plugins/doc/editor_styles.dart new file mode 100644 index 0000000000..dc472aeaf1 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/doc/editor_styles.dart @@ -0,0 +1,61 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +EditorStyle customEditorStyle(BuildContext context) { + final theme = context.watch(); + const baseFontSize = 14.0; + const basePadding = 12.0; + var textStyle = theme.isDark + ? BuiltInTextStyle.builtInDarkMode() + : BuiltInTextStyle.builtIn(); + textStyle = textStyle.copyWith( + defaultTextStyle: textStyle.defaultTextStyle.copyWith( + fontFamily: 'poppins', + fontSize: baseFontSize, + ), + bold: textStyle.bold.copyWith( + fontWeight: FontWeight.w500, + ), + ); + return EditorStyle.defaultStyle().copyWith( + padding: const EdgeInsets.symmetric(horizontal: 80), + textStyle: textStyle, + pluginStyles: { + 'text/heading': builtInPluginStyle + ..update( + 'textStyle', + (_) => (EditorState editorState, Node node) { + final headingToFontSize = { + 'h1': baseFontSize + 12, + 'h2': baseFontSize + 8, + 'h3': baseFontSize + 4, + 'h4': baseFontSize, + 'h5': baseFontSize, + 'h6': baseFontSize, + }; + final fontSize = + headingToFontSize[node.attributes.heading] ?? baseFontSize; + return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600); + }, + ) + ..update( + 'padding', + (_) => (EditorState editorState, Node node) { + final headingToPadding = { + 'h1': basePadding + 6, + 'h2': basePadding + 4, + 'h3': basePadding + 2, + 'h4': basePadding, + 'h5': basePadding, + 'h6': basePadding, + }; + final padding = + headingToPadding[node.attributes.heading] ?? basePadding; + return EdgeInsets.only(bottom: padding); + }, + ) + }, + ); +} diff --git a/frontend/app_flowy/lib/plugins/doc/presentation/plugins/horizontal_rule_node_widget.dart b/frontend/app_flowy/lib/plugins/doc/presentation/plugins/horizontal_rule_node_widget.dart new file mode 100644 index 0000000000..c38cc0846c --- /dev/null +++ b/frontend/app_flowy/lib/plugins/doc/presentation/plugins/horizontal_rule_node_widget.dart @@ -0,0 +1,166 @@ +import 'dart:collection'; + +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +ShortcutEvent insertHorizontalRule = ShortcutEvent( + key: 'Horizontal rule', + command: 'Minus', + handler: _insertHorzaontalRule, +); + +ShortcutEventHandler _insertHorzaontalRule = (editorState, event) { + final selection = editorState.service.selectionService.currentSelection.value; + final textNodes = editorState.service.selectionService.currentSelectedNodes + .whereType(); + if (textNodes.length != 1 || selection == null) { + return KeyEventResult.ignored; + } + final textNode = textNodes.first; + if (textNode.toPlainText() == '--') { + final transaction = editorState.transaction + ..deleteText(textNode, 0, 2) + ..insertNode( + textNode.path, + Node( + type: 'horizontal_rule', + children: LinkedList(), + attributes: {}, + ), + ) + ..afterSelection = + Selection.single(path: textNode.path.next, startOffset: 0); + editorState.apply(transaction); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; +}; + +SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem( + name: () => 'Horizontal rule', + icon: const Icon( + Icons.horizontal_rule, + color: Colors.black, + size: 18.0, + ), + keywords: ['horizontal rule'], + handler: (editorState, _, __) { + final selection = + editorState.service.selectionService.currentSelection.value; + final textNodes = editorState.service.selectionService.currentSelectedNodes + .whereType(); + if (selection == null || textNodes.isEmpty) { + return; + } + final textNode = textNodes.first; + if (textNode.toPlainText().isEmpty) { + final transaction = editorState.transaction + ..insertNode( + textNode.path, + Node( + type: 'horizontal_rule', + children: LinkedList(), + attributes: {}, + ), + ) + ..afterSelection = + Selection.single(path: textNode.path.next, startOffset: 0); + editorState.apply(transaction); + } else { + final transaction = editorState.transaction + ..insertNode( + selection.end.path.next, + TextNode( + children: LinkedList(), + attributes: { + 'subtype': 'horizontal_rule', + }, + delta: Delta()..insert('---'), + ), + ) + ..afterSelection = selection; + editorState.apply(transaction); + } + }, +); + +class HorizontalRuleWidgetBuilder extends NodeWidgetBuilder { + @override + Widget build(NodeWidgetContext context) { + return _HorizontalRuleWidget( + key: context.node.key, + node: context.node, + editorState: context.editorState, + ); + } + + @override + NodeValidator get nodeValidator => (node) { + return true; + }; +} + +class _HorizontalRuleWidget extends StatefulWidget { + const _HorizontalRuleWidget({ + Key? key, + required this.node, + required this.editorState, + }) : super(key: key); + + final Node node; + final EditorState editorState; + + @override + State<_HorizontalRuleWidget> createState() => __HorizontalRuleWidgetState(); +} + +class __HorizontalRuleWidgetState extends State<_HorizontalRuleWidget> + with SelectableMixin { + RenderBox get _renderBox => context.findRenderObject() as RenderBox; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Container( + height: 1, + color: Colors.grey, + ), + ); + } + + @override + Position start() => Position(path: widget.node.path, offset: 0); + + @override + Position end() => Position(path: widget.node.path, offset: 1); + + @override + Position getPositionInOffset(Offset start) => end(); + + @override + bool get shouldCursorBlink => false; + + @override + CursorStyle get cursorStyle => CursorStyle.borderLine; + + @override + Rect? getCursorRectInPosition(Position position) { + final size = _renderBox.size; + return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height); + } + + @override + List getRectsInSelection(Selection selection) => + [Offset.zero & _renderBox.size]; + + @override + Selection getSelectionInRange(Offset start, Offset end) => Selection.single( + path: widget.node.path, + startOffset: 0, + endOffset: 1, + ); + + @override + Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset); +} diff --git a/frontend/app_flowy/lib/plugins/grid/grid.dart b/frontend/app_flowy/lib/plugins/grid/grid.dart index 91f6ca5063..2532b5bc57 100644 --- a/frontend/app_flowy/lib/plugins/grid/grid.dart +++ b/frontend/app_flowy/lib/plugins/grid/grid.dart @@ -26,7 +26,7 @@ class GridPluginBuilder implements PluginBuilder { PluginType get pluginType => PluginType.grid; @override - ViewDataTypePB get dataType => ViewDataTypePB.Database; + ViewDataFormatPB get dataFormatType => ViewDataFormatPB.DatabaseFormat; @override ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Grid; diff --git a/frontend/app_flowy/lib/startup/plugin/plugin.dart b/frontend/app_flowy/lib/startup/plugin/plugin.dart index 8e7f88768f..6f33c85374 100644 --- a/frontend/app_flowy/lib/startup/plugin/plugin.dart +++ b/frontend/app_flowy/lib/startup/plugin/plugin.dart @@ -49,7 +49,7 @@ abstract class PluginBuilder { PluginType get pluginType; - ViewDataTypePB get dataType => ViewDataTypePB.Text; + ViewDataFormatPB get dataFormatType => ViewDataFormatPB.TreeFormat; ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Document; } diff --git a/frontend/app_flowy/lib/startup/tasks/app_widget.dart b/frontend/app_flowy/lib/startup/tasks/app_widget.dart index 807e308d7a..83d787d078 100644 --- a/frontend/app_flowy/lib/startup/tasks/app_widget.dart +++ b/frontend/app_flowy/lib/startup/tasks/app_widget.dart @@ -1,14 +1,15 @@ import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/user/application/user_settings_service.dart'; import 'package:app_flowy/workspace/application/appearance.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_sdk/log.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:window_size/window_size.dart'; import 'package:bloc/bloc.dart'; -import 'package:flowy_sdk/log.dart'; class InitAppWidgetTask extends LaunchTask { @override @@ -93,7 +94,8 @@ class ApplicationWidget extends StatelessWidget { builder: overlayManagerBuilder(), debugShowCheckedModeBanner: false, theme: theme.themeData, - localizationsDelegates: context.localizationDelegates, + localizationsDelegates: context.localizationDelegates + + [AppFlowyEditorLocalizations.delegate], supportedLocales: context.supportedLocales, locale: locale, navigatorKey: AppGlobals.rootNavKey, diff --git a/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart b/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart index fa2a9555bc..654d267796 100644 --- a/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart @@ -98,7 +98,7 @@ class AppBloc extends Bloc { appId: app.id, name: value.name, desc: value.desc ?? "", - dataType: value.pluginBuilder.dataType, + dataFormatType: value.pluginBuilder.dataFormatType, pluginType: value.pluginBuilder.pluginType, layoutType: value.pluginBuilder.layoutType!, ); diff --git a/frontend/app_flowy/lib/workspace/application/app/app_service.dart b/frontend/app_flowy/lib/workspace/application/app/app_service.dart index ab35c3338d..fd096a4e0c 100644 --- a/frontend/app_flowy/lib/workspace/application/app/app_service.dart +++ b/frontend/app_flowy/lib/workspace/application/app/app_service.dart @@ -19,7 +19,7 @@ class AppService { required String appId, required String name, String? desc, - required ViewDataTypePB dataType, + required ViewDataFormatPB dataFormatType, required PluginType pluginType, required ViewLayoutTypePB layoutType, }) { @@ -27,7 +27,7 @@ class AppService { ..belongToId = appId ..name = name ..desc = desc ?? "" - ..dataType = dataType + ..dataFormat = dataFormatType ..layout = layoutType; return FolderEventCreateView(payload).send(); diff --git a/frontend/app_flowy/lib/workspace/application/markdown/document_markdown.dart b/frontend/app_flowy/lib/workspace/application/markdown/document_markdown.dart new file mode 100644 index 0000000000..71d0137280 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/markdown/document_markdown.dart @@ -0,0 +1,29 @@ +library delta_markdown; + +import 'dart:convert'; + +import 'package:appflowy_editor/appflowy_editor.dart' show Document; +import 'package:app_flowy/workspace/application/markdown/src/parser/markdown_encoder.dart'; + +/// Codec used to convert between Markdown and AppFlowy Editor Document. +const AppFlowyEditorMarkdownCodec _kCodec = AppFlowyEditorMarkdownCodec(); + +Document markdownToDocument(String markdown) { + return _kCodec.decode(markdown); +} + +String documentToMarkdown(Document document) { + return _kCodec.encode(document); +} + +class AppFlowyEditorMarkdownCodec extends Codec { + const AppFlowyEditorMarkdownCodec(); + + @override + Converter get decoder => throw UnimplementedError(); + + @override + Converter get encoder { + return AppFlowyEditorMarkdownEncoder(); + } +} diff --git a/frontend/app_flowy/lib/workspace/application/markdown/src/parser/image_node_parser.dart b/frontend/app_flowy/lib/workspace/application/markdown/src/parser/image_node_parser.dart new file mode 100644 index 0000000000..575cf13216 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/markdown/src/parser/image_node_parser.dart @@ -0,0 +1,14 @@ +import 'package:app_flowy/workspace/application/markdown/src/parser/node_parser.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +class ImageNodeParser extends NodeParser { + const ImageNodeParser(); + + @override + String get id => 'image'; + + @override + String transform(Node node) { + return '![](${node.attributes['image_src']})'; + } +} diff --git a/frontend/app_flowy/lib/workspace/application/markdown/src/parser/markdown_encoder.dart b/frontend/app_flowy/lib/workspace/application/markdown/src/parser/markdown_encoder.dart new file mode 100644 index 0000000000..f6c1e6ea02 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/markdown/src/parser/markdown_encoder.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; + +import 'package:app_flowy/workspace/application/markdown/src/parser/image_node_parser.dart'; +import 'package:app_flowy/workspace/application/markdown/src/parser/node_parser.dart'; +import 'package:app_flowy/workspace/application/markdown/src/parser/text_node_parser.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +class AppFlowyEditorMarkdownEncoder extends Converter { + AppFlowyEditorMarkdownEncoder({ + this.parsers = const [ + TextNodeParser(), + ImageNodeParser(), + ], + }); + + final List parsers; + + @override + String convert(Document input) { + final buffer = StringBuffer(); + for (final node in input.root.children) { + NodeParser? parser = + parsers.firstWhereOrNull((element) => element.id == node.type); + if (parser != null) { + buffer.write(parser.transform(node)); + } + } + return buffer.toString(); + } +} + +extension IterableExtension on Iterable { + T? firstWhereOrNull(bool Function(T element) test) { + for (var element in this) { + if (test(element)) return element; + } + return null; + } +} diff --git a/frontend/app_flowy/lib/workspace/application/markdown/src/parser/node_parser.dart b/frontend/app_flowy/lib/workspace/application/markdown/src/parser/node_parser.dart new file mode 100644 index 0000000000..649ca7eae7 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/markdown/src/parser/node_parser.dart @@ -0,0 +1,8 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; + +abstract class NodeParser { + const NodeParser(); + + String get id; + String transform(Node node); +} diff --git a/frontend/app_flowy/lib/workspace/application/markdown/src/parser/text_node_parser.dart b/frontend/app_flowy/lib/workspace/application/markdown/src/parser/text_node_parser.dart new file mode 100644 index 0000000000..0dbf6418aa --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/markdown/src/parser/text_node_parser.dart @@ -0,0 +1,68 @@ +import 'dart:convert'; + +import 'package:app_flowy/workspace/application/markdown/delta_markdown.dart'; +import 'package:app_flowy/workspace/application/markdown/src/parser/node_parser.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +class TextNodeParser extends NodeParser { + const TextNodeParser(); + + @override + String get id => 'text'; + + @override + String transform(Node node) { + assert(node is TextNode); + final textNode = node as TextNode; + final delta = jsonEncode( + textNode.delta + ..add(TextInsert('\n')) + ..toJson(), + ); + final markdown = deltaToMarkdown(delta); + final attributes = textNode.attributes; + var result = markdown; + var suffix = ''; + if (attributes.isNotEmpty && + attributes.containsKey(BuiltInAttributeKey.subtype)) { + final subtype = attributes[BuiltInAttributeKey.subtype]; + if (node.next?.subtype != subtype) { + suffix = '\n'; + } + if (subtype == 'heading') { + final heading = attributes[BuiltInAttributeKey.heading]; + if (heading == 'h1') { + result = '# $markdown'; + } else if (heading == 'h2') { + result = '## $markdown'; + } else if (heading == 'h3') { + result = '### $markdown'; + } else if (heading == 'h4') { + result = '#### $markdown'; + } else if (heading == 'h5') { + result = '##### $markdown'; + } else if (heading == 'h6') { + result = '###### $markdown'; + } + } else if (subtype == 'quote') { + result = '> $markdown'; + } else if (subtype == 'code') { + result = '`$markdown`'; + } else if (subtype == 'code-block') { + result = '```\n$markdown\n```'; + } else if (subtype == 'bulleted-list') { + result = '- $markdown'; + } else if (subtype == 'number-list') { + final number = attributes['number']; + result = '$number. $markdown'; + } else if (subtype == 'checkbox') { + if (attributes[BuiltInAttributeKey.checkbox] == true) { + result = '- [x] $markdown'; + } else { + result = '- [ ] $markdown'; + } + } + } + return '$result$suffix'; + } +} diff --git a/frontend/app_flowy/lib/workspace/application/view/view_bloc.dart b/frontend/app_flowy/lib/workspace/application/view/view_bloc.dart index e343b6f9fe..86db402209 100644 --- a/frontend/app_flowy/lib/workspace/application/view/view_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/view/view_bloc.dart @@ -56,7 +56,7 @@ class ViewBloc extends Bloc { ); }, duplicate: (e) async { - final result = await service.duplicate(viewId: view.id); + final result = await service.duplicate(view: view); emit( result.fold( (l) => state.copyWith(successOrFailure: left(unit)), diff --git a/frontend/app_flowy/lib/workspace/application/view/view_service.dart b/frontend/app_flowy/lib/workspace/application/view/view_service.dart index b73cf25cad..7cfbbfeeb7 100644 --- a/frontend/app_flowy/lib/workspace/application/view/view_service.dart +++ b/frontend/app_flowy/lib/workspace/application/view/view_service.dart @@ -10,7 +10,8 @@ class ViewService { return FolderEventReadView(request).send(); } - Future> updateView({required String viewId, String? name, String? desc}) { + Future> updateView( + {required String viewId, String? name, String? desc}) { final request = UpdateViewPayloadPB.create()..viewId = viewId; if (name != null) { @@ -29,8 +30,7 @@ class ViewService { return FolderEventDeleteView(request).send(); } - Future> duplicate({required String viewId}) { - final request = ViewIdPB(value: viewId); - return FolderEventDuplicateView(request).send(); + Future> duplicate({required ViewPB view}) { + return FolderEventDuplicateView(view).send(); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json index 2d441d3367..39994cdfb2 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json @@ -2,16 +2,12 @@ "document": { "type": "editor", "children": [ - { - "type": "image", - "attributes": { - "image_src": "https://s1.ax1x.com/2022/08/26/v2sSbR.jpg", - "align": "center" - } - }, { "type": "text", - "attributes": { "subtype": "heading", "heading": "h1" }, + "attributes": { + "subtype": "heading", + "heading": "h2" + }, "delta": [ { "insert": "👋 " }, { "insert": "Welcome to ", "attributes": { "bold": true } }, 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 588804e9ac..488839a980 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:example/plugin/underscore_to_italic.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -141,7 +140,6 @@ class _MyHomePageState extends State { shortcutEvents: [ enterInCodeBlock, ignoreKeysInCodeBlock, - underscoreToItalic, insertHorizontalRule, ], selectionMenuItems: [ diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic.dart deleted file mode 100644 index 5efbee91d2..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -ShortcutEvent underscoreToItalic = ShortcutEvent( - key: 'Underscore to italic', - command: 'shift+underscore', - handler: _underscoreToItalicHandler, -); - -ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) { - // Obtain the selection and selected nodes of the current document through the 'selectionService' - // to determine whether the selection is collapsed and whether the selected node is a text node. - final selectionService = editorState.service.selectionService; - final selection = selectionService.currentSelection.value; - final textNodes = selectionService.currentSelectedNodes.whereType(); - if (selection == null || !selection.isSingle || textNodes.length != 1) { - return KeyEventResult.ignored; - } - - final textNode = textNodes.first; - final text = textNode.toPlainText(); - // Determine if an 'underscore' already exists in the text node and only once. - final firstUnderscore = text.indexOf('_'); - final lastUnderscore = text.lastIndexOf('_'); - if (firstUnderscore == -1 || - firstUnderscore != lastUnderscore || - firstUnderscore == selection.start.offset - 1) { - return KeyEventResult.ignored; - } - - // Delete the previous 'underscore', - // update the style of the text surrounded by the two underscores to 'italic', - // and update the cursor position. - final transaction = editorState.transaction - ..deleteText(textNode, firstUnderscore, 1) - ..formatText( - textNode, - firstUnderscore, - selection.end.offset - firstUnderscore - 1, - { - BuiltInAttributeKey.italic: true, - }, - ) - ..afterSelection = Selection.collapsed( - Position( - path: textNode.path, - offset: selection.end.offset - 1, - ), - ); - editorState.apply(transaction); - - return KeyEventResult.handled; -}; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/document.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/document.dart index b172e78554..f085118b70 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/document.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/document.dart @@ -57,8 +57,8 @@ class Document { final parent = nodeAtPath(path.parent); if (parent != null) { - for (final node in nodes) { - parent.insert(node, index: path.last); + for (var i = 0; i < nodes.length; i++) { + parent.insert(nodes.elementAt(i), index: path.last + i); } return true; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/transaction.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/transaction.dart index 297600288d..4df4adb228 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/transaction.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/transform/transaction.dart @@ -103,7 +103,7 @@ class Transaction { Map toJson() { final json = {}; if (operations.isNotEmpty) { - json['operations'] = operations.map((o) => o.toJson()); + json['operations'] = operations.map((o) => o.toJson()).toList(); } if (afterSelection != null) { json['after_selection'] = afterSelection!.toJson(); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/flutter/overlay.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/flutter/overlay.dart new file mode 100644 index 0000000000..0a91229e0a --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/flutter/overlay.dart @@ -0,0 +1,904 @@ +// TODO: Remove this file until we update the flutter version to 3.5.x +// +// This file is copied from flutter(3.5.x) repo. +// +// We Need to commit(https://github.com/flutter/flutter/pull/113770) to fix the +// overflow issue. + +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:collection'; +import 'dart:math' as math; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; + +/// A place in an [Overlay] that can contain a widget. +/// +/// Overlay entries are inserted into an [Overlay] using the +/// [OverlayState.insert] or [OverlayState.insertAll] functions. To find the +/// closest enclosing overlay for a given [BuildContext], use the [Overlay.of] +/// function. +/// +/// An overlay entry can be in at most one overlay at a time. To remove an entry +/// from its overlay, call the [remove] function on the overlay entry. +/// +/// Because an [Overlay] uses a [Stack] layout, overlay entries can use +/// [Positioned] and [AnimatedPositioned] to position themselves within the +/// overlay. +/// +/// For example, [Draggable] uses an [OverlayEntry] to show the drag avatar that +/// follows the user's finger across the screen after the drag begins. Using the +/// overlay to display the drag avatar lets the avatar float over the other +/// widgets in the app. As the user's finger moves, draggable calls +/// [markNeedsBuild] on the overlay entry to cause it to rebuild. In its build, +/// the entry includes a [Positioned] with its top and left property set to +/// position the drag avatar near the user's finger. When the drag is over, +/// [Draggable] removes the entry from the overlay to remove the drag avatar +/// from view. +/// +/// By default, if there is an entirely [opaque] entry over this one, then this +/// one will not be included in the widget tree (in particular, stateful widgets +/// within the overlay entry will not be instantiated). To ensure that your +/// overlay entry is still built even if it is not visible, set [maintainState] +/// to true. This is more expensive, so should be done with care. In particular, +/// if widgets in an overlay entry with [maintainState] set to true repeatedly +/// call [State.setState], the user's battery will be drained unnecessarily. +/// +/// [OverlayEntry] is a [ChangeNotifier] that notifies when the widget built by +/// [builder] is mounted or unmounted, whose exact state can be queried by +/// [mounted]. +/// +/// See also: +/// +/// * [Overlay] +/// * [OverlayState] +/// * [WidgetsApp] +/// * [MaterialApp] +class OverlayEntry extends ChangeNotifier { + /// Creates an overlay entry. + /// + /// To insert the entry into an [Overlay], first find the overlay using + /// [Overlay.of] and then call [OverlayState.insert]. To remove the entry, + /// call [remove] on the overlay entry itself. + OverlayEntry({ + required this.builder, + bool opaque = false, + bool maintainState = false, + }) : _opaque = opaque, + _maintainState = maintainState; + + /// This entry will include the widget built by this builder in the overlay at + /// the entry's position. + /// + /// To cause this builder to be called again, call [markNeedsBuild] on this + /// overlay entry. + final WidgetBuilder builder; + + /// Whether this entry occludes the entire overlay. + /// + /// If an entry claims to be opaque, then, for efficiency, the overlay will + /// skip building entries below that entry unless they have [maintainState] + /// set. + bool get opaque => _opaque; + bool _opaque; + set opaque(bool value) { + if (_opaque == value) return; + _opaque = value; + _overlay?._didChangeEntryOpacity(); + } + + /// Whether this entry must be included in the tree even if there is a fully + /// [opaque] entry above it. + /// + /// By default, if there is an entirely [opaque] entry over this one, then this + /// one will not be included in the widget tree (in particular, stateful widgets + /// within the overlay entry will not be instantiated). To ensure that your + /// overlay entry is still built even if it is not visible, set [maintainState] + /// to true. This is more expensive, so should be done with care. In particular, + /// if widgets in an overlay entry with [maintainState] set to true repeatedly + /// call [State.setState], the user's battery will be drained unnecessarily. + /// + /// This is used by the [Navigator] and [Route] objects to ensure that routes + /// are kept around even when in the background, so that [Future]s promised + /// from subsequent routes will be handled properly when they complete. + bool get maintainState => _maintainState; + bool _maintainState; + set maintainState(bool value) { + if (_maintainState == value) return; + _maintainState = value; + assert(_overlay != null); + _overlay!._didChangeEntryOpacity(); + } + + /// Whether the [OverlayEntry] is currently mounted in the widget tree. + /// + /// The [OverlayEntry] notifies its listeners when this value changes. + bool get mounted => _mounted; + bool _mounted = false; + void _updateMounted(bool value) { + if (value == _mounted) { + return; + } + _mounted = value; + notifyListeners(); + } + + OverlayState? _overlay; + final GlobalKey<_OverlayEntryWidgetState> _key = + GlobalKey<_OverlayEntryWidgetState>(); + + /// Remove this entry from the overlay. + /// + /// This should only be called once. + /// + /// This method removes this overlay entry from the overlay immediately. The + /// UI will be updated in the same frame if this method is called before the + /// overlay rebuild in this frame; otherwise, the UI will be updated in the + /// next frame. This means that it is safe to call during builds, but also + /// that if you do call this after the overlay rebuild, the UI will not update + /// until the next frame (i.e. many milliseconds later). + void remove() { + assert(_overlay != null); + final OverlayState overlay = _overlay!; + _overlay = null; + if (!overlay.mounted) return; + + overlay._entries.remove(this); + if (SchedulerBinding.instance.schedulerPhase == + SchedulerPhase.persistentCallbacks) { + SchedulerBinding.instance.addPostFrameCallback((Duration duration) { + overlay._markDirty(); + }); + } else { + overlay._markDirty(); + } + } + + /// Cause this entry to rebuild during the next pipeline flush. + /// + /// You need to call this function if the output of [builder] has changed. + void markNeedsBuild() { + _key.currentState?._markNeedsBuild(); + } + + @override + String toString() => + '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)'; +} + +class _OverlayEntryWidget extends StatefulWidget { + const _OverlayEntryWidget({ + required Key key, + required this.entry, + this.tickerEnabled = true, + }) : super(key: key); + + final OverlayEntry entry; + final bool tickerEnabled; + + @override + _OverlayEntryWidgetState createState() => _OverlayEntryWidgetState(); +} + +class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> { + @override + void initState() { + super.initState(); + widget.entry._updateMounted(true); + } + + @override + void dispose() { + widget.entry._updateMounted(false); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return TickerMode( + enabled: widget.tickerEnabled, + child: widget.entry.builder(context), + ); + } + + void _markNeedsBuild() { + setState(() {/* the state that changed is in the builder */}); + } +} + +/// A stack of entries that can be managed independently. +/// +/// Overlays let independent child widgets "float" visual elements on top of +/// other widgets by inserting them into the overlay's stack. The overlay lets +/// each of these widgets manage their participation in the overlay using +/// [OverlayEntry] objects. +/// +/// Although you can create an [Overlay] directly, it's most common to use the +/// overlay created by the [Navigator] in a [WidgetsApp] or a [MaterialApp]. The +/// navigator uses its overlay to manage the visual appearance of its routes. +/// +/// The [Overlay] widget uses a custom stack implementation, which is very +/// similar to the [Stack] widget. The main use case of [Overlay] is related to +/// navigation and being able to insert widgets on top of the pages in an app. +/// To simply display a stack of widgets, consider using [Stack] instead. +/// +/// See also: +/// +/// * [OverlayEntry], the class that is used for describing the overlay entries. +/// * [OverlayState], which is used to insert the entries into the overlay. +/// * [WidgetsApp], which inserts an [Overlay] widget indirectly via its [Navigator]. +/// * [MaterialApp], which inserts an [Overlay] widget indirectly via its [Navigator]. +/// * [Stack], which allows directly displaying a stack of widgets. +class Overlay extends StatefulWidget { + /// Creates an overlay. + /// + /// The initial entries will be inserted into the overlay when its associated + /// [OverlayState] is initialized. + /// + /// Rather than creating an overlay, consider using the overlay that is + /// created by the [Navigator] in a [WidgetsApp] or a [MaterialApp] for the application. + const Overlay({ + Key? key, + this.initialEntries = const [], + this.clipBehavior = Clip.hardEdge, + }) : super(key: key); + + /// The entries to include in the overlay initially. + /// + /// These entries are only used when the [OverlayState] is initialized. If you + /// are providing a new [Overlay] description for an overlay that's already in + /// the tree, then the new entries are ignored. + /// + /// To add entries to an [Overlay] that is already in the tree, use + /// [Overlay.of] to obtain the [OverlayState] (or assign a [GlobalKey] to the + /// [Overlay] widget and obtain the [OverlayState] via + /// [GlobalKey.currentState]), and then use [OverlayState.insert] or + /// [OverlayState.insertAll]. + /// + /// To remove an entry from an [Overlay], use [OverlayEntry.remove]. + final List initialEntries; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge], and must not be null. + final Clip clipBehavior; + + /// The state from the closest instance of this class that encloses the given context. + /// + /// In debug mode, if the `debugRequiredFor` argument is provided then this + /// function will assert that an overlay was found and will throw an exception + /// if not. The exception attempts to explain that the calling [Widget] (the + /// one given by the `debugRequiredFor` argument) needs an [Overlay] to be + /// present to function. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// OverlayState overlay = Overlay.of(context); + /// ``` + /// + /// If `rootOverlay` is set to true, the state from the furthest instance of + /// this class is given instead. Useful for installing overlay entries + /// above all subsequent instances of [Overlay]. + /// + /// This method can be expensive (it walks the element tree). + static OverlayState? of( + BuildContext context, { + bool rootOverlay = false, + Widget? debugRequiredFor, + }) { + final OverlayState? result = rootOverlay + ? context.findRootAncestorStateOfType() + : context.findAncestorStateOfType(); + assert(() { + if (debugRequiredFor != null && result == null) { + final List information = [ + ErrorSummary('No Overlay widget found.'), + ErrorDescription( + '${debugRequiredFor.runtimeType} widgets require an Overlay widget ancestor for correct operation.'), + ErrorHint( + 'The most common way to add an Overlay to an application is to include a MaterialApp or Navigator widget in the runApp() call.'), + DiagnosticsProperty( + 'The specific widget that failed to find an overlay was', + debugRequiredFor, + style: DiagnosticsTreeStyle.errorProperty), + if (context.widget != debugRequiredFor) + context.describeElement( + 'The context from which that widget was searching for an overlay was'), + ]; + + throw FlutterError.fromParts(information); + } + return true; + }()); + return result; + } + + @override + OverlayState createState() => OverlayState(); +} + +/// The current state of an [Overlay]. +/// +/// Used to insert [OverlayEntry]s into the overlay using the [insert] and +/// [insertAll] functions. +class OverlayState extends State with TickerProviderStateMixin { + final List _entries = []; + + @override + void initState() { + super.initState(); + insertAll(widget.initialEntries); + } + + int _insertionIndex(OverlayEntry? below, OverlayEntry? above) { + assert(above == null || below == null); + if (below != null) return _entries.indexOf(below); + if (above != null) return _entries.indexOf(above) + 1; + return _entries.length; + } + + /// Insert the given entry into the overlay. + /// + /// If `below` is non-null, the entry is inserted just below `below`. + /// If `above` is non-null, the entry is inserted just above `above`. + /// Otherwise, the entry is inserted on top. + /// + /// It is an error to specify both `above` and `below`. + void insert(OverlayEntry entry, {OverlayEntry? below, OverlayEntry? above}) { + assert(_debugVerifyInsertPosition(above, below)); + assert(!_entries.contains(entry), + 'The specified entry is already present in the Overlay.'); + assert(entry._overlay == null, + 'The specified entry is already present in another Overlay.'); + entry._overlay = this; + setState(() { + _entries.insert(_insertionIndex(below, above), entry); + }); + } + + /// Insert all the entries in the given iterable. + /// + /// If `below` is non-null, the entries are inserted just below `below`. + /// If `above` is non-null, the entries are inserted just above `above`. + /// Otherwise, the entries are inserted on top. + /// + /// It is an error to specify both `above` and `below`. + void insertAll(Iterable entries, + {OverlayEntry? below, OverlayEntry? above}) { + assert(_debugVerifyInsertPosition(above, below)); + assert( + entries.every((OverlayEntry entry) => !_entries.contains(entry)), + 'One or more of the specified entries are already present in the Overlay.', + ); + assert( + entries.every((OverlayEntry entry) => entry._overlay == null), + 'One or more of the specified entries are already present in another Overlay.', + ); + if (entries.isEmpty) return; + for (final OverlayEntry entry in entries) { + assert(entry._overlay == null); + entry._overlay = this; + } + setState(() { + _entries.insertAll(_insertionIndex(below, above), entries); + }); + } + + bool _debugVerifyInsertPosition(OverlayEntry? above, OverlayEntry? below, + {Iterable? newEntries}) { + assert( + above == null || below == null, + 'Only one of `above` and `below` may be specified.', + ); + assert( + above == null || + (above._overlay == this && + _entries.contains(above) && + (newEntries?.contains(above) ?? true)), + 'The provided entry used for `above` must be present in the Overlay${newEntries != null ? ' and in the `newEntriesList`' : ''}.', + ); + assert( + below == null || + (below._overlay == this && + _entries.contains(below) && + (newEntries?.contains(below) ?? true)), + 'The provided entry used for `below` must be present in the Overlay${newEntries != null ? ' and in the `newEntriesList`' : ''}.', + ); + return true; + } + + /// Remove all the entries listed in the given iterable, then reinsert them + /// into the overlay in the given order. + /// + /// Entries mention in `newEntries` but absent from the overlay are inserted + /// as if with [insertAll]. + /// + /// Entries not mentioned in `newEntries` but present in the overlay are + /// positioned as a group in the resulting list relative to the entries that + /// were moved, as specified by one of `below` or `above`, which, if + /// specified, must be one of the entries in `newEntries`: + /// + /// If `below` is non-null, the group is positioned just below `below`. + /// If `above` is non-null, the group is positioned just above `above`. + /// Otherwise, the group is left on top, with all the rearranged entries + /// below. + /// + /// It is an error to specify both `above` and `below`. + void rearrange(Iterable newEntries, + {OverlayEntry? below, OverlayEntry? above}) { + final List newEntriesList = newEntries is List + ? newEntries + : newEntries.toList(growable: false); + assert( + _debugVerifyInsertPosition(above, below, newEntries: newEntriesList)); + assert( + newEntriesList.every((OverlayEntry entry) => + entry._overlay == null || entry._overlay == this), + 'One or more of the specified entries are already present in another Overlay.', + ); + assert( + newEntriesList.every((OverlayEntry entry) => + _entries.indexOf(entry) == _entries.lastIndexOf(entry)), + 'One or more of the specified entries are specified multiple times.', + ); + if (newEntriesList.isEmpty) return; + if (listEquals(_entries, newEntriesList)) return; + final LinkedHashSet old = + LinkedHashSet.of(_entries); + for (final OverlayEntry entry in newEntriesList) { + entry._overlay ??= this; + } + setState(() { + _entries.clear(); + _entries.addAll(newEntriesList); + old.removeAll(newEntriesList); + _entries.insertAll(_insertionIndex(below, above), old); + }); + } + + void _markDirty() { + if (mounted) { + setState(() {}); + } + } + + /// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an + /// opaque entry). + /// + /// This is an O(N) algorithm, and should not be necessary except for debug + /// asserts. To avoid people depending on it, this function is implemented + /// only in debug mode, and always returns false in release mode. + bool debugIsVisible(OverlayEntry entry) { + bool result = false; + assert(_entries.contains(entry)); + assert(() { + for (int i = _entries.length - 1; i > 0; i -= 1) { + final OverlayEntry candidate = _entries[i]; + if (candidate == entry) { + result = true; + break; + } + if (candidate.opaque) break; + } + return true; + }()); + return result; + } + + void _didChangeEntryOpacity() { + setState(() { + // We use the opacity of the entry in our build function, which means we + // our state has changed. + }); + } + + @override + Widget build(BuildContext context) { + // This list is filled backwards and then reversed below before + // it is added to the tree. + final List children = []; + bool onstage = true; + int onstageCount = 0; + for (int i = _entries.length - 1; i >= 0; i -= 1) { + final OverlayEntry entry = _entries[i]; + if (onstage) { + onstageCount += 1; + children.add(_OverlayEntryWidget( + key: entry._key, + entry: entry, + )); + if (entry.opaque) onstage = false; + } else if (entry.maintainState) { + children.add(_OverlayEntryWidget( + key: entry._key, + entry: entry, + tickerEnabled: false, + )); + } + } + return _Theatre( + skipCount: children.length - onstageCount, + clipBehavior: widget.clipBehavior, + children: children.reversed.toList(growable: false), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + // TODO(jacobr): use IterableProperty instead as that would + // provide a slightly more consistent string summary of the List. + properties + .add(DiagnosticsProperty>('entries', _entries)); + } +} + +/// Special version of a [Stack], that doesn't layout and render the first +/// [skipCount] children. +/// +/// The first [skipCount] children are considered "offstage". +class _Theatre extends MultiChildRenderObjectWidget { + _Theatre({ + Key? key, + this.skipCount = 0, + this.clipBehavior = Clip.hardEdge, + List children = const [], + }) : assert(skipCount >= 0), + assert(children.length >= skipCount), + super(key: key, children: children); + + final int skipCount; + + final Clip clipBehavior; + + @override + _TheatreElement createElement() => _TheatreElement(this); + + @override + _RenderTheatre createRenderObject(BuildContext context) { + return _RenderTheatre( + skipCount: skipCount, + textDirection: Directionality.of(context), + clipBehavior: clipBehavior, + ); + } + + @override + void updateRenderObject(BuildContext context, _RenderTheatre renderObject) { + renderObject + ..skipCount = skipCount + ..textDirection = Directionality.of(context) + ..clipBehavior = clipBehavior; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IntProperty('skipCount', skipCount)); + } +} + +class _TheatreElement extends MultiChildRenderObjectElement { + _TheatreElement(_Theatre widget) : super(widget); + + @override + _RenderTheatre get renderObject => super.renderObject as _RenderTheatre; + + @override + void debugVisitOnstageChildren(ElementVisitor visitor) { + final _Theatre theatre = widget as _Theatre; + assert(children.length >= theatre.skipCount); + children.skip(theatre.skipCount).forEach(visitor); + } +} + +class _RenderTheatre extends RenderBox + with ContainerRenderObjectMixin { + _RenderTheatre({ + List? children, + required TextDirection textDirection, + int skipCount = 0, + Clip clipBehavior = Clip.hardEdge, + }) : assert(skipCount >= 0), + _textDirection = textDirection, + _skipCount = skipCount, + _clipBehavior = clipBehavior { + addAll(children); + } + + bool _hasVisualOverflow = false; + + @override + void setupParentData(RenderBox child) { + if (child.parentData is! StackParentData) { + child.parentData = StackParentData(); + } + } + + Alignment? _resolvedAlignment; + + void _resolve() { + if (_resolvedAlignment != null) return; + _resolvedAlignment = AlignmentDirectional.topStart.resolve(textDirection); + } + + void _markNeedResolution() { + _resolvedAlignment = null; + markNeedsLayout(); + } + + TextDirection get textDirection => _textDirection; + TextDirection _textDirection; + set textDirection(TextDirection value) { + if (_textDirection == value) return; + _textDirection = value; + _markNeedResolution(); + } + + int get skipCount => _skipCount; + int _skipCount; + set skipCount(int value) { + if (_skipCount != value) { + _skipCount = value; + markNeedsLayout(); + } + } + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge], and must not be null. + Clip get clipBehavior => _clipBehavior; + Clip _clipBehavior = Clip.hardEdge; + set clipBehavior(Clip value) { + if (value != _clipBehavior) { + _clipBehavior = value; + markNeedsPaint(); + markNeedsSemanticsUpdate(); + } + } + + RenderBox? get _firstOnstageChild { + if (skipCount == super.childCount) { + return null; + } + RenderBox? child = super.firstChild; + for (int toSkip = skipCount; toSkip > 0; toSkip--) { + final StackParentData childParentData = + child!.parentData! as StackParentData; + child = childParentData.nextSibling; + assert(child != null); + } + return child; + } + + RenderBox? get _lastOnstageChild => + skipCount == super.childCount ? null : lastChild; + + int get _onstageChildCount => childCount - skipCount; + + @override + double computeMinIntrinsicWidth(double height) { + return RenderStack.getIntrinsicDimension(_firstOnstageChild, + (RenderBox child) => child.getMinIntrinsicWidth(height)); + } + + @override + double computeMaxIntrinsicWidth(double height) { + return RenderStack.getIntrinsicDimension(_firstOnstageChild, + (RenderBox child) => child.getMaxIntrinsicWidth(height)); + } + + @override + double computeMinIntrinsicHeight(double width) { + return RenderStack.getIntrinsicDimension(_firstOnstageChild, + (RenderBox child) => child.getMinIntrinsicHeight(width)); + } + + @override + double computeMaxIntrinsicHeight(double width) { + return RenderStack.getIntrinsicDimension(_firstOnstageChild, + (RenderBox child) => child.getMaxIntrinsicHeight(width)); + } + + @override + double? computeDistanceToActualBaseline(TextBaseline baseline) { + assert(!debugNeedsLayout); + double? result; + RenderBox? child = _firstOnstageChild; + while (child != null) { + assert(!child.debugNeedsLayout); + final StackParentData childParentData = + child.parentData! as StackParentData; + double? candidate = child.getDistanceToActualBaseline(baseline); + if (candidate != null) { + candidate += childParentData.offset.dy; + if (result != null) { + result = math.min(result, candidate); + } else { + result = candidate; + } + } + child = childParentData.nextSibling; + } + return result; + } + + @override + bool get sizedByParent => true; + + @override + Size computeDryLayout(BoxConstraints constraints) { + assert(constraints.biggest.isFinite); + return constraints.biggest; + } + + @override + void performLayout() { + _hasVisualOverflow = false; + + if (_onstageChildCount == 0) { + return; + } + + _resolve(); + assert(_resolvedAlignment != null); + + // Same BoxConstraints as used by RenderStack for StackFit.expand. + final BoxConstraints nonPositionedConstraints = + BoxConstraints.tight(constraints.biggest); + + RenderBox? child = _firstOnstageChild; + while (child != null) { + final StackParentData childParentData = + child.parentData! as StackParentData; + + if (!childParentData.isPositioned) { + child.layout(nonPositionedConstraints, parentUsesSize: true); + childParentData.offset = + _resolvedAlignment!.alongOffset(size - child.size as Offset); + } else { + _hasVisualOverflow = RenderStack.layoutPositionedChild( + child, childParentData, size, _resolvedAlignment!) || + _hasVisualOverflow; + } + + assert(child.parentData == childParentData); + child = childParentData.nextSibling; + } + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + RenderBox? child = _lastOnstageChild; + for (int i = 0; i < _onstageChildCount; i++) { + assert(child != null); + final StackParentData childParentData = + child!.parentData! as StackParentData; + final bool isHit = result.addWithPaintOffset( + offset: childParentData.offset, + position: position, + hitTest: (BoxHitTestResult result, Offset transformed) { + assert(transformed == position - childParentData.offset); + return child!.hitTest(result, position: transformed); + }, + ); + if (isHit) return true; + child = childParentData.previousSibling; + } + return false; + } + + @protected + void paintStack(PaintingContext context, Offset offset) { + RenderBox? child = _firstOnstageChild; + while (child != null) { + final StackParentData childParentData = + child.parentData! as StackParentData; + context.paintChild(child, childParentData.offset + offset); + child = childParentData.nextSibling; + } + } + + @override + void paint(PaintingContext context, Offset offset) { + _hasVisualOverflow = true; + if (_hasVisualOverflow && clipBehavior != Clip.none) { + _clipRectLayer.layer = context.pushClipRect( + needsCompositing, + offset, + Offset.zero & size, + paintStack, + clipBehavior: clipBehavior, + oldLayer: _clipRectLayer.layer, + ); + } else { + _clipRectLayer.layer = null; + paintStack(context, offset); + } + } + + final LayerHandle _clipRectLayer = + LayerHandle(); + + @override + void dispose() { + _clipRectLayer.layer = null; + super.dispose(); + } + + @override + void visitChildrenForSemantics(RenderObjectVisitor visitor) { + RenderBox? child = _firstOnstageChild; + while (child != null) { + visitor(child); + final StackParentData childParentData = + child.parentData! as StackParentData; + child = childParentData.nextSibling; + } + } + + @override + Rect? describeApproximatePaintClip(RenderObject child) => + _hasVisualOverflow ? Offset.zero & size : null; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IntProperty('skipCount', skipCount)); + properties.add(EnumProperty('textDirection', textDirection)); + } + + @override + List debugDescribeChildren() { + final List offstageChildren = []; + final List onstageChildren = []; + + int count = 1; + bool onstage = false; + RenderBox? child = firstChild; + final RenderBox? firstOnstageChild = _firstOnstageChild; + while (child != null) { + if (child == firstOnstageChild) { + onstage = true; + count = 1; + } + + if (onstage) { + onstageChildren.add( + child.toDiagnosticsNode( + name: 'onstage $count', + ), + ); + } else { + offstageChildren.add( + child.toDiagnosticsNode( + name: 'offstage $count', + style: DiagnosticsTreeStyle.offstage, + ), + ); + } + + final StackParentData childParentData = + child.parentData! as StackParentData; + child = childParentData.nextSibling; + count += 1; + } + + return [ + ...onstageChildren, + if (offstageChildren.isNotEmpty) + ...offstageChildren + else + DiagnosticsNode.message( + 'no offstage children', + style: DiagnosticsTreeStyle.offstage, + ), + ]; + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart index 6087606c56..bb02c74600 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart @@ -1,5 +1,3 @@ -import 'dart:collection'; - import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; @@ -185,12 +183,12 @@ extension on EditorState { } final imageNode = Node( type: 'image', - children: LinkedList(), attributes: { 'image_src': src, 'align': 'center', }, ); + final transaction = this.transaction; transaction.insertNode( selection.start.path, imageNode, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index c49122dea4..40e0cffb26 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -26,7 +26,7 @@ class FlowyRichText extends StatefulWidget { const FlowyRichText({ Key? key, this.cursorHeight, - this.cursorWidth = 1.0, + this.cursorWidth = 1.5, this.lineHeight = 1.0, this.textSpanDecorator, this.placeholderText = ' ', @@ -55,7 +55,7 @@ class _FlowyRichTextState extends State with SelectableMixin { RenderParagraph get _renderParagraph => _textKey.currentContext?.findRenderObject() as RenderParagraph; - RenderParagraph get _placeholderRenderParagraph => + RenderParagraph? get _placeholderRenderParagraph => _placeholderTextKey.currentContext?.findRenderObject() as RenderParagraph; @override @@ -79,7 +79,7 @@ class _FlowyRichTextState extends State with SelectableMixin { @override Position end() => Position( - path: widget.textNode.path, offset: widget.textNode.delta.length); + path: widget.textNode.path, offset: widget.textNode.toPlainText().length); @override Rect? getCursorRectInPosition(Position position) { @@ -90,12 +90,13 @@ class _FlowyRichTextState extends State with SelectableMixin { _renderParagraph.getOffsetForCaret(textPosition, Rect.zero); if (cursorHeight == null) { cursorHeight = - _placeholderRenderParagraph.getFullHeightForCaret(textPosition); - cursorOffset = _placeholderRenderParagraph.getOffsetForCaret( - textPosition, Rect.zero); + _placeholderRenderParagraph?.getFullHeightForCaret(textPosition); + cursorOffset = _placeholderRenderParagraph?.getOffsetForCaret( + textPosition, Rect.zero) ?? + Offset.zero; } final rect = Rect.fromLTWH( - cursorOffset.dx - (widget.cursorWidth / 2), + cursorOffset.dx - (widget.cursorWidth / 2.0), cursorOffset.dy, widget.cursorWidth, widget.cursorHeight ?? cursorHeight ?? 16.0, @@ -297,6 +298,8 @@ class _FlowyRichTextState extends State with SelectableMixin { timer = Timer(const Duration(milliseconds: 200), () { tapCount = 0; + widget.editorState.service.selectionService + .updateSelection(selection); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { showLinkMenu( context, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart index 614546ef99..c36b9adb8e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart @@ -59,10 +59,16 @@ class SelectionMenu implements SelectionMenuService { // Workaround: We can customize the padding through the [EditorStyle], // but the coordinates of overlay are not properly converted currently. // Just subtract the padding here as a result. + const menuHeight = 200.0; + const menuOffset = Offset(10, 10); final baseOffset = editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; - final offset = - selectionRects.first.bottomRight + const Offset(10, 10) - baseOffset; + var offset = selectionRects.first.bottomRight + menuOffset; + if (offset.dy >= + baseOffset.dy + editorState.renderBox!.size.height - menuHeight) { + offset = selectionRects.first.topRight - menuOffset; + offset = offset.translate(0, -menuHeight); + } _topLeft = offset; _selectionMenuEntry = OverlayEntry(builder: (context) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index 359fde0ac0..56008dced2 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -1,13 +1,14 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/commands/text/text_commands.dart'; import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart'; +import 'package:appflowy_editor/src/flutter/overlay.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; import 'package:appflowy_editor/src/extensions/editor_state_extensions.dart'; import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Overlay, OverlayEntry; import 'package:rich_clipboard/rich_clipboard.dart'; typedef ToolbarItemEventHandler = void Function( @@ -206,7 +207,9 @@ List defaultToolbarItems = [ BuiltInAttributeKey.subtype, (value) => value == BuiltInAttributeKey.quote, ), - handler: (editorState, context) => formatQuote(editorState), + handler: (editorState, context) { + formatQuote(editorState); + }, ), ToolbarItem( id: 'appflowy.toolbar.bulleted_list', diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart index 4fefa8eadb..71cfd030d5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart @@ -25,7 +25,7 @@ class ToolbarItemWidget extends StatelessWidget { child: MouseRegion( cursor: SystemMouseCursors.click, child: IconButton( - highlightColor: Colors.yellow, + highlightColor: Colors.transparent, padding: EdgeInsets.zero, icon: item.iconBuilder(isHighlight), iconSize: 28, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart index 18c2cdc0b1..d987b2f87b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart @@ -1,6 +1,7 @@ +import 'package:appflowy_editor/src/flutter/overlay.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Overlay, OverlayEntry; import 'package:appflowy_editor/src/editor_state.dart'; @@ -67,6 +68,7 @@ class _ToolbarWidgetState extends State with ToolbarMixin { isHighlight: item.highlightCallback(widget.editorState), onPressed: () { item.handler(widget.editorState, context); + widget.editorState.service.keyboardService?.enable(); }, ), ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart index 3d9599383d..dd00a3e0c9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart @@ -1,9 +1,10 @@ +import 'package:appflowy_editor/src/flutter/overlay.dart'; import 'package:appflowy_editor/src/render/image/image_node_builder.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart'; import 'package:appflowy_editor/src/render/style/editor_style.dart'; import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart'; import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Overlay, OverlayEntry; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/render/editor/editor_entry.dart'; 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 63aa61925f..25f52c6914 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 @@ -97,6 +97,7 @@ void _pasteHTML(EditorState editorState, String html) { final firstTextNode = firstNode as TextNode; tb.updateText( textNodeAtPath, (Delta()..retain(startOffset)) + firstTextNode.delta); + tb.updateNode(textNodeAtPath, firstTextNode.attributes); tb.afterSelection = (Selection.collapsed(Position( path: path, offset: startOffset + firstTextNode.delta.length))); editorState.apply(tb); @@ -114,7 +115,7 @@ void _pasteMultipleLinesInText( final firstNode = nodes[0]; final nodeAtPath = editorState.document.nodeAtPath(path)!; - if (nodeAtPath.type == "text" && firstNode.type == "text") { + if (nodeAtPath.type == 'text' && firstNode.type == 'text') { int? startNumber; if (nodeAtPath.subtype == BuiltInAttributeKey.numberList) { startNumber = nodeAtPath.attributes[BuiltInAttributeKey.number] as int; @@ -131,6 +132,7 @@ void _pasteMultipleLinesInText( ..retain(offset) ..delete(remain.length)) + firstTextNode.delta); + tb.updateNode(textNodeAtPath, firstTextNode.attributes); final tailNodes = nodes.sublist(1); final originalPath = [...path]; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart index 1b37e5fa11..94caff83b1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart @@ -393,3 +393,48 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) { editorState.apply(transaction); return KeyEventResult.handled; }; + +ShortcutEventHandler underscoreToItalicHandler = (editorState, event) { + // Obtain the selection and selected nodes of the current document through the 'selectionService' + // to determine whether the selection is collapsed and whether the selected node is a text node. + final selectionService = editorState.service.selectionService; + final selection = selectionService.currentSelection.value; + final textNodes = selectionService.currentSelectedNodes.whereType(); + if (selection == null || !selection.isSingle || textNodes.length != 1) { + return KeyEventResult.ignored; + } + + final textNode = textNodes.first; + final text = textNode.toPlainText(); + // Determine if an 'underscore' already exists in the text node and only once. + final firstUnderscore = text.indexOf('_'); + final lastUnderscore = text.lastIndexOf('_'); + if (firstUnderscore == -1 || + firstUnderscore != lastUnderscore || + firstUnderscore == selection.start.offset - 1) { + return KeyEventResult.ignored; + } + + // Delete the previous 'underscore', + // update the style of the text surrounded by the two underscores to 'italic', + // and update the cursor position. + final transaction = editorState.transaction + ..deleteText(textNode, firstUnderscore, 1) + ..formatText( + textNode, + firstUnderscore, + selection.end.offset - firstUnderscore - 1, + { + BuiltInAttributeKey.italic: true, + }, + ) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: selection.end.offset - 1, + ), + ); + editorState.apply(transaction); + + return KeyEventResult.handled; +}; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart index 210a9a703e..8732c0f0ea 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart @@ -189,7 +189,8 @@ KeyEventResult _toHeadingStyle( int _countOfSign(String text, Selection selection) { for (var i = 6; i >= 0; i--) { - if (text.substring(0, selection.end.offset).contains('#' * i)) { + final heading = text.substring(0, selection.end.offset); + if (heading.contains('#' * i) && heading.length == i) { return i; } } 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 d68e686d61..f00edc4a56 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 @@ -92,11 +92,16 @@ class _AppFlowyScrollState extends State Widget build(BuildContext context) { return Listener( onPointerSignal: _onPointerSignal, - child: SingleChildScrollView( + child: CustomScrollView( key: _scrollViewKey, physics: const NeverScrollableScrollPhysics(), controller: _scrollController, - child: widget.child, + slivers: [ + SliverFillRemaining( + hasScrollBody: false, + child: widget.child, + ) + ], ), ); } 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 e562db0f7e..dc0ef59203 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,7 +1,8 @@ +import 'package:appflowy_editor/src/flutter/overlay.dart'; import 'package:appflowy_editor/src/infra/log.dart'; import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart'; import 'package:appflowy_editor/src/service/context_menu/context_menu.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Overlay, OverlayEntry; import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/core/document/node_iterator.dart'; @@ -505,9 +506,14 @@ class _AppFlowySelectionState extends State } void _showContextMenu(TapDownDetails details) { + _clearContextMenu(); + + final baseOffset = + editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; + final offset = details.globalPosition + const Offset(10, 10) - baseOffset; final contextMenu = OverlayEntry( builder: (context) => ContextMenu( - position: details.globalPosition, + position: offset, editorState: editorState, items: builtInContextMenuItems, onPressed: () => _clearContextMenu(), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart index 34aa469d85..b70c7279e2 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart @@ -285,6 +285,11 @@ List builtInShortcutEvents = [ command: 'escape', handler: exitEditingModeEventHandler, ), + ShortcutEvent( + key: 'Underscore to italic', + command: 'shift+underscore', + handler: underscoreToItalicHandler, + ), // https://github.com/flutter/flutter/issues/104944 // Workaround: Using space editing on the web platform often results in errors, // so adding a shortcut event to handle the space input instead of using the diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart index 3d3def574e..8991d0a30a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -1,5 +1,6 @@ +import 'package:appflowy_editor/src/flutter/overlay.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Overlay, OverlayEntry; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart index cef16a1cec..c20748779a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart @@ -1,6 +1,8 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; void main() async { setUpAll(() { @@ -39,5 +41,34 @@ void main() async { expect(submittedText, link); }); + + testWidgets('test tap linked text', (tester) async { + const link = 'appflowy.io'; + // This is a link [appflowy.io](appflowy.io) + final editor = tester.editor + ..insertTextNode( + null, + delta: Delta() + ..insert( + 'appflowy.io', + attributes: { + BuiltInAttributeKey.href: link, + }, + ), + ); + await editor.startTesting(); + final finder = find.byType(RichText); + expect(finder, findsOneWidget); + + // tap the link + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0, endOffset: link.length), + ); + await tester.tap(finder); + await tester.pumpAndSettle(const Duration(milliseconds: 350)); + final linkMenu = find.byType(LinkMenu); + expect(linkMenu, findsOneWidget); + expect(find.text(link, findRichText: true), findsNWidgets(2)); + }); }); } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart index d06a865b64..f62d977ece 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart @@ -213,5 +213,19 @@ void main() async { expect(textNode.attributes.check, true); expect(textNode.toPlainText(), insertedText); }); + + testWidgets('Presses # at the end of the text', (tester) async { + const text = 'Welcome to Appflowy 😁 #'; + final editor = tester.editor..insertTextNode(text); + await editor.startTesting(); + + final textNode = editor.nodeAtPath([0]) as TextNode; + await editor.updateSelection( + Selection.single(path: [0], startOffset: text.length), + ); + await editor.pressLogicKey(LogicalKeyboardKey.space); + expect(textNode.subtype, null); + expect(textNode.toPlainText(), text); + }); }); } diff --git a/frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dispatch.dart b/frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dispatch.dart index 0716a2f4c6..020c8f1ed0 100644 --- a/frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dispatch.dart +++ b/frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dispatch.dart @@ -18,7 +18,6 @@ import 'package:flowy_sdk/protobuf/dart-ffi/protobuf.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/protobuf.dart'; import 'package:flowy_sdk/protobuf/flowy-document/protobuf.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; -import 'package:flowy_sdk/protobuf/flowy-sync/protobuf.dart'; // ignore: unused_import import 'package:protobuf/protobuf.dart'; diff --git a/frontend/app_flowy/pubspec.lock b/frontend/app_flowy/pubspec.lock index f280d3b4ca..918dc043a4 100644 --- a/frontend/app_flowy/pubspec.lock +++ b/frontend/app_flowy/pubspec.lock @@ -586,6 +586,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.2" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" graphs: dependency: transitive description: diff --git a/frontend/app_flowy/pubspec.yaml b/frontend/app_flowy/pubspec.yaml index 302d8b920e..0b4b6e9800 100644 --- a/frontend/app_flowy/pubspec.yaml +++ b/frontend/app_flowy/pubspec.yaml @@ -91,6 +91,7 @@ dependencies: bloc: ^8.1.0 textstyle_extensions: "2.0.0-nullsafety" shared_preferences: ^2.0.15 + google_fonts: ^3.0.1 dev_dependencies: flutter_lints: ^2.0.1 @@ -129,6 +130,26 @@ flutter: - family: FlowyIconData fonts: - asset: assets/fonts/FlowyIconData.ttf + - family: Poppins + fonts: + - asset: assets/google_fonts/Poppins/Poppins-ExtraLight.ttf + weight: 100 + - asset: assets/google_fonts/Poppins/Poppins-Thin.ttf + weight: 200 + - asset: assets/google_fonts/Poppins/Poppins-Light.ttf + weight: 300 + - asset: assets/google_fonts/Poppins/Poppins-Regular.ttf + weight: 400 + - asset: assets/google_fonts/Poppins/Poppins-Medium.ttf + weight: 500 + - asset: assets/google_fonts/Poppins/Poppins-SemiBold.ttf + weight: 600 + - asset: assets/google_fonts/Poppins/Poppins-Bold.ttf + weight: 700 + - asset: assets/google_fonts/Poppins/Poppins-Black.ttf + weight: 800 + - asset: assets/google_fonts/Poppins/Poppins-ExtraBold.ttf + weight: 900 # To add assets to your application, add an assets section, like this: assets: diff --git a/frontend/app_flowy/test/bloc_test/grid_test/util.dart b/frontend/app_flowy/test/bloc_test/grid_test/util.dart index e81028dedc..8f3a1dae1e 100644 --- a/frontend/app_flowy/test/bloc_test/grid_test/util.dart +++ b/frontend/app_flowy/test/bloc_test/grid_test/util.dart @@ -30,7 +30,7 @@ class AppFlowyGridTest { final result = await AppService().createView( appId: app.id, name: "Test Grid", - dataType: builder.dataType, + dataFormatType: builder.dataFormatType, pluginType: builder.pluginType, layoutType: builder.layoutType!, ); diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index b28c9789ab..4578294e1c 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -871,6 +871,7 @@ dependencies = [ "lib-ot", "lib-ws", "log", + "md5", "protobuf", "rand 0.8.5", "serde", @@ -1773,6 +1774,7 @@ dependencies = [ "bytes", "dashmap", "derive_more", + "indexmap", "indextree", "lazy_static", "log", diff --git a/frontend/rust-lib/dart-ffi/src/lib.rs b/frontend/rust-lib/dart-ffi/src/lib.rs index 2759a29de6..a6a001d708 100644 --- a/frontend/rust-lib/dart-ffi/src/lib.rs +++ b/frontend/rust-lib/dart-ffi/src/lib.rs @@ -23,7 +23,7 @@ pub extern "C" fn init_sdk(path: *mut c_char) -> i64 { let path: &str = c_str.to_str().unwrap(); let server_config = get_client_server_configuration().unwrap(); - let config = FlowySDKConfig::new(path, "appflowy", server_config, false).log_filter("info"); + let config = FlowySDKConfig::new(path, "appflowy", server_config).log_filter("info"); FLOWY_SDK.get_or_init(|| FlowySDK::new(config)); 0 diff --git a/frontend/rust-lib/flowy-database/migrations/2022-10-22-033122_document/down.sql b/frontend/rust-lib/flowy-database/migrations/2022-10-22-033122_document/down.sql new file mode 100644 index 0000000000..46d0fce4d8 --- /dev/null +++ b/frontend/rust-lib/flowy-database/migrations/2022-10-22-033122_document/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE grid_view_rev_table; diff --git a/frontend/rust-lib/flowy-database/migrations/2022-10-22-033122_document/up.sql b/frontend/rust-lib/flowy-database/migrations/2022-10-22-033122_document/up.sql new file mode 100644 index 0000000000..34d4e71d41 --- /dev/null +++ b/frontend/rust-lib/flowy-database/migrations/2022-10-22-033122_document/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here +CREATE TABLE document_rev_table ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + document_id TEXT NOT NULL DEFAULT '', + base_rev_id BIGINT NOT NULL DEFAULT 0, + rev_id BIGINT NOT NULL DEFAULT 0, + data BLOB NOT NULL DEFAULT (x''), + state INTEGER NOT NULL DEFAULT 0 +); diff --git a/frontend/rust-lib/flowy-database/src/schema.rs b/frontend/rust-lib/flowy-database/src/schema.rs index 065a13b85f..19d27e43bc 100644 --- a/frontend/rust-lib/flowy-database/src/schema.rs +++ b/frontend/rust-lib/flowy-database/src/schema.rs @@ -13,6 +13,17 @@ table! { } } +table! { + document_rev_table (id) { + id -> Integer, + document_id -> Text, + base_rev_id -> BigInt, + rev_id -> BigInt, + data -> Binary, + state -> Integer, + } +} + table! { grid_block_index_table (row_id) { row_id -> Text, @@ -133,6 +144,7 @@ table! { allow_tables_to_appear_in_same_query!( app_table, + document_rev_table, grid_block_index_table, grid_meta_rev_table, grid_rev_table, diff --git a/frontend/rust-lib/flowy-document/Cargo.toml b/frontend/rust-lib/flowy-document/Cargo.toml index 6e3ab066a3..e44b183bd8 100644 --- a/frontend/rust-lib/flowy-document/Cargo.toml +++ b/frontend/rust-lib/flowy-document/Cargo.toml @@ -28,6 +28,7 @@ tokio = {version = "1", features = ["sync"]} tracing = { version = "0.1", features = ["log"] } bytes = { version = "1.1" } +md5 = "0.7.0" strum = "0.21" strum_macros = "0.21" dashmap = "5" diff --git a/frontend/rust-lib/flowy-document/src/editor/READ_ME.json b/frontend/rust-lib/flowy-document/src/editor/READ_ME.json new file mode 100644 index 0000000000..b47f43ee16 --- /dev/null +++ b/frontend/rust-lib/flowy-document/src/editor/READ_ME.json @@ -0,0 +1,419 @@ +{ + "document": { + "type": "editor", + "children": [ + { + "type": "text", + "attributes": { + "subtype": "heading", + "heading": "h1" + }, + "delta": [ + { + "insert": "🌟 Welcome to AppFlowy!" + } + ] + }, + { + "type": "text", + "attributes": { + "subtype": "heading", + "heading": "h2" + }, + "delta": [ + { + "insert": "Here are the basics" + } + ] + }, + { + "type": "text", + "attributes": { + "subtype": "checkbox", + "checkbox": null + }, + "delta": [ + { + "insert": "Click anywhere and just start typing." + } + ] + }, + { + "type": "text", + "attributes": { + "subtype": "checkbox", + "checkbox": null + }, + "delta": [ + { + "insert": "Highlight", + "attributes": { + "backgroundColor": "0x6000BCF0" + } + }, + { + "insert": " any text, and use the editing menu to " + }, + { + "insert": "style", + "attributes": { + "italic": true + } + }, + { + "insert": " " + }, + { + "insert": "your", + "attributes": { + "bold": true + } + }, + { + "insert": " " + }, + { + "insert": "writing", + "attributes": { + "underline": true + } + }, + { + "insert": " " + }, + { + "insert": "however", + "attributes": { + "code": true + } + }, + { + "insert": " you " + }, + { + "insert": "like.", + "attributes": { + "strikethrough": true + } + } + ] + }, + { + "type": "text", + "attributes": { + "subtype": "checkbox", + "checkbox": null + }, + "delta": [ + { + "insert": "As soon as you type " + }, + { + "insert": "/", + "attributes": { + "code": true + } + }, + { + "insert": " a menu will pop up. Select different types of content blocks you can add." + } + ] + }, + { + "type": "text", + "attributes": { + "subtype": "checkbox", + "checkbox": null + }, + "delta": [ + { + "insert": "Type " + }, + { + "insert": "/", + "attributes": { + "code": true + } + }, + { + "insert": " followed by " + }, + { + "insert": "/bullet", + "attributes": { + "code": true + } + }, + { + "insert": " or " + }, + { + "insert": "/c.", + "attributes": { + "code": true + } + } + ] + }, + { + "type": "text", + "attributes": { + "subtype": "checkbox", + "checkbox": true + }, + "delta": [ + { + "insert": "Click " + }, + { + "insert": "+ New Page ", + "attributes": { + "code": true + } + }, + { + "insert": "button at the bottom of your sidebar to add a new page." + } + ] + }, + { + "type": "text", + "attributes": { + "subtype": "checkbox", + "checkbox": null + }, + "delta": [ + { + "insert": "Click " + }, + { + "insert": "+", + "attributes": { + "code": true + } + }, + { + "insert": " next to any page title in the sidebar to quickly add a new subpage." + } + ] + }, + { + "type": "text", + "attributes": { + "checkbox": null + }, + "delta": [] + }, + { + "type": "text", + "attributes": { + "subtype": "heading", + "checkbox": null, + "heading": "h2" + }, + "delta": [ + { + "insert": "Markdown" + } + ] + }, + { + "type": "text", + "attributes": { + "subtype": "number-list", + "number": 1, + "heading": null + }, + "delta": [ + { + "insert": "Heading " + } + ] + }, + { + "type": "text", + "attributes": { + "subtype": "number-list", + "number": 2 + }, + "delta": [ + { + "insert": "bold text", + "attributes": { + "bold": true, + "defaultFormating": true + } + } + ] + }, + { + "type": "text", + "attributes": { + "subtype": "number-list", + "number": 3 + }, + "delta": [ + { + "insert": "italicized text", + "attributes": { + "italic": true + } + } + ] + }, + { + "type": "text", + "attributes": { + "subtype": "number-list", + "number": 4, + "number-list": null + }, + "delta": [ + { + "insert": "Ordered List" + } + ] + }, + { + "type": "text", + "attributes": { + "number": 5, + "subtype": "number-list" + }, + "delta": [ + { + "insert": "code", + "attributes": { + "code": true + } + } + ] + }, + { + "type": "text", + "attributes": { + "number": 6, + "subtype": "number-list" + }, + "delta": [ + { + "insert": "Strikethrough", + "attributes": { + "strikethrough": true + } + }, + { + "retain": 1, + "attributes": { + "strikethrough": true + } + } + ] + }, + { + "type": "text", + "attributes": { + "checkbox": null + }, + "delta": [] + }, + { + "type": "text", + "attributes": { + "subtype": "heading", + "checkbox": null, + "heading": "h2" + }, + "delta": [ + { + "insert": "Have a question?" + } + ] + }, + { + "type": "text", + "attributes": { + "subtype": "quote" + }, + "delta": [ + { + "insert": "Click " + }, + { + "insert": "?", + "attributes": { + "code": true + } + }, + { + "insert": " at the bottom right for help and support." + } + ] + }, + { + "type": "text", + "delta": [] + }, + { + "type": "text", + "attributes": { + "subtype": "heading", + "heading": "h2" + }, + "delta": [ + { + "insert": "Like AppFlowy? Follow us:" + } + ] + }, + { + "type": "text", + "attributes": { + "subtype": "bulleted-list", + "quote": null + }, + "delta": [ + { + "insert": "GitHub", + "attributes": { + "href": "https://github.com/AppFlowy-IO/AppFlowy" + } + } + ] + }, + { + "type": "text", + "attributes": { + "subtype": "bulleted-list" + }, + "delta": [ + { + "insert": "Twitter: @appflowy" + } + ] + }, + { + "type": "text", + "attributes": { + "subtype": "bulleted-list" + }, + "delta": [ + { + "insert": "Newsletter", + "attributes": { + "href": "https://blog-appflowy.ghost.io/" + } + } + ] + }, + { + "type": "text", + "attributes": { + "subtype": null, + "heading": null + }, + "delta": [] + } + ] + } +} diff --git a/frontend/rust-lib/flowy-document/src/editor/document.rs b/frontend/rust-lib/flowy-document/src/editor/document.rs index 3ef22209eb..21894c3a66 100644 --- a/frontend/rust-lib/flowy-document/src/editor/document.rs +++ b/frontend/rust-lib/flowy-document/src/editor/document.rs @@ -1,11 +1,11 @@ use bytes::Bytes; use flowy_error::{FlowyError, FlowyResult}; -use flowy_revision::{RevisionObjectDeserializer, RevisionObjectSerializer}; +use flowy_revision::{RevisionCompress, RevisionObjectDeserializer, RevisionObjectSerializer}; use flowy_sync::entities::revision::Revision; use lib_ot::core::{ Body, Extension, NodeDataBuilder, NodeOperation, NodeTree, NodeTreeContext, Selection, Transaction, }; -use lib_ot::text_delta::TextOperationBuilder; +use lib_ot::text_delta::DeltaTextOperationBuilder; #[derive(Debug)] pub struct Document { @@ -30,6 +30,11 @@ impl Document { } } + pub fn md5(&self) -> String { + // format!("{:x}", md5::compute(bytes)) + "".to_owned() + } + pub fn get_tree(&self) -> &NodeTree { &self.tree } @@ -40,7 +45,7 @@ pub(crate) fn make_tree_context() -> NodeTreeContext { } pub fn initial_document_content() -> String { - let delta = TextOperationBuilder::new().insert("").build(); + let delta = DeltaTextOperationBuilder::new().insert("").build(); let node_data = NodeDataBuilder::new("text").insert_body(Body::Delta(delta)).build(); let editor_node = NodeDataBuilder::new("editor").add_node_data(node_data).build(); let node_operation = NodeOperation::Insert { @@ -78,7 +83,7 @@ impl RevisionObjectDeserializer for DocumentRevisionSerde { fn deserialize_revisions(_object_id: &str, revisions: Vec) -> FlowyResult { let mut tree = NodeTree::new(make_tree_context()); - let transaction = make_transaction_from_revisions(revisions)?; + let transaction = make_transaction_from_revisions(&revisions)?; let _ = tree.apply_transaction(transaction)?; let document = Document::new(tree); Result::::Ok(document) @@ -87,12 +92,20 @@ impl RevisionObjectDeserializer for DocumentRevisionSerde { impl RevisionObjectSerializer for DocumentRevisionSerde { fn combine_revisions(revisions: Vec) -> FlowyResult { - let transaction = make_transaction_from_revisions(revisions)?; + let transaction = make_transaction_from_revisions(&revisions)?; Ok(Bytes::from(transaction.to_bytes()?)) } } -fn make_transaction_from_revisions(revisions: Vec) -> FlowyResult { +pub(crate) struct DocumentRevisionCompress(); +impl RevisionCompress for DocumentRevisionCompress { + fn combine_revisions(&self, revisions: Vec) -> FlowyResult { + DocumentRevisionSerde::combine_revisions(revisions) + } +} + +#[tracing::instrument(level = "trace", skip_all, err)] +pub fn make_transaction_from_revisions(revisions: &[Revision]) -> FlowyResult { let mut transaction = Transaction::new(); for revision in revisions { let _ = transaction.compose(Transaction::from_bytes(&revision.bytes)?)?; diff --git a/frontend/rust-lib/flowy-document/src/editor/document_serde.rs b/frontend/rust-lib/flowy-document/src/editor/document_serde.rs index d0ac859a42..099da01454 100644 --- a/frontend/rust-lib/flowy-document/src/editor/document_serde.rs +++ b/frontend/rust-lib/flowy-document/src/editor/document_serde.rs @@ -3,11 +3,12 @@ use crate::editor::document::Document; use bytes::Bytes; use flowy_error::FlowyResult; use lib_ot::core::{ - AttributeHashMap, Body, Changeset, Extension, NodeData, NodeId, NodeOperation, NodeTree, Path, Selection, - Transaction, + AttributeHashMap, Body, Changeset, Extension, NodeData, NodeId, NodeOperation, NodeTree, NodeTreeContext, Path, + Selection, Transaction, }; -use lib_ot::text_delta::TextOperations; -use serde::de::{self, MapAccess, Visitor}; + +use lib_ot::text_delta::DeltaTextOperations; +use serde::de::{self, MapAccess, Unexpected, Visitor}; use serde::ser::{SerializeMap, SerializeSeq}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt; @@ -44,14 +45,14 @@ impl<'de> Deserialize<'de> for Document { where M: MapAccess<'de>, { - let mut node_tree = None; + let mut document_node = None; while let Some(key) = map.next_key()? { match key { "document" => { - if node_tree.is_some() { + if document_node.is_some() { return Err(de::Error::duplicate_field("document")); } - node_tree = Some(map.next_value::()?) + document_node = Some(map.next_value::()?) } s => { return Err(de::Error::unknown_field(s, FIELDS)); @@ -59,8 +60,13 @@ impl<'de> Deserialize<'de> for Document { } } - match node_tree { - Some(tree) => Ok(Document::new(tree)), + match document_node { + Some(document_node) => { + match NodeTree::from_node_data(document_node.into(), NodeTreeContext::default()) { + Ok(tree) => Ok(Document::new(tree)), + Err(err) => Err(de::Error::invalid_value(Unexpected::Other(&format!("{}", err)), &"")), + } + } None => Err(de::Error::missing_field("document")), } } @@ -69,10 +75,20 @@ impl<'de> Deserialize<'de> for Document { } } -#[derive(Debug)] -struct DocumentContentSerializer<'a>(pub &'a Document); +pub fn make_transaction_from_document_content(content: &str) -> FlowyResult { + let document_node: DocumentNode = serde_json::from_str::(content)?.document; + let document_operation = DocumentOperation::Insert { + path: 0_usize.into(), + nodes: vec![document_node], + }; + let mut document_transaction = DocumentTransaction::default(); + document_transaction.operations.push(document_operation); + Ok(document_transaction.into()) +} -#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DocumentContentSerde {} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct DocumentTransaction { #[serde(default)] operations: Vec, @@ -161,8 +177,8 @@ pub enum DocumentOperation { #[serde(rename = "update_text")] UpdateText { path: Path, - delta: TextOperations, - inverted: TextOperations, + delta: DeltaTextOperations, + inverted: DeltaTextOperations, }, } @@ -230,20 +246,27 @@ pub struct DocumentNode { #[serde(default)] pub attributes: AttributeHashMap, - #[serde(skip_serializing_if = "TextOperations::is_empty")] - pub delta: TextOperations, + #[serde(skip_serializing_if = "DeltaTextOperations::is_empty")] + #[serde(default)] + pub delta: DeltaTextOperations, #[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default)] pub children: Vec, } +impl DocumentNode { + pub fn new() -> Self { + Self::default() + } +} + impl std::convert::From for DocumentNode { fn from(node_data: NodeData) -> Self { let delta = if let Body::Delta(operations) = node_data.body { operations } else { - TextOperations::default() + DeltaTextOperations::default() }; DocumentNode { node_type: node_data.node_type, @@ -265,6 +288,14 @@ impl std::convert::From for NodeData { } } +#[derive(Debug, Deserialize)] +struct DocumentContentDeserializer { + document: DocumentNode, +} + +#[derive(Debug)] +struct DocumentContentSerializer<'a>(pub &'a Document); + impl<'a> Serialize for DocumentContentSerializer<'a> { fn serialize(&self, serializer: S) -> Result where @@ -299,6 +330,12 @@ impl<'a> Serialize for DocumentContentSerializer<'a> { mod tests { use crate::editor::document::Document; use crate::editor::document_serde::DocumentTransaction; + use crate::editor::initial_read_me; + + #[test] + fn load_read_me() { + let _ = initial_read_me(); + } #[test] fn transaction_deserialize_update_text_operation_test() { diff --git a/frontend/rust-lib/flowy-document/src/editor/editor.rs b/frontend/rust-lib/flowy-document/src/editor/editor.rs index 4c526c2402..76f3c9ad17 100644 --- a/frontend/rust-lib/flowy-document/src/editor/editor.rs +++ b/frontend/rust-lib/flowy-document/src/editor/editor.rs @@ -1,5 +1,6 @@ use crate::editor::document::{Document, DocumentRevisionSerde}; use crate::editor::document_serde::DocumentTransaction; +use crate::editor::make_transaction_from_revisions; use crate::editor::queue::{Command, CommandSender, DocumentQueue}; use crate::{DocumentEditor, DocumentUser}; use bytes::Bytes; @@ -17,6 +18,7 @@ pub struct AppFlowyDocumentEditor { #[allow(dead_code)] doc_id: String, command_sender: CommandSender, + rev_manager: Arc, } impl AppFlowyDocumentEditor { @@ -28,9 +30,13 @@ impl AppFlowyDocumentEditor { ) -> FlowyResult> { let document = rev_manager.load::(Some(cloud_service)).await?; let rev_manager = Arc::new(rev_manager); - let command_sender = spawn_edit_queue(user, rev_manager, document); + let command_sender = spawn_edit_queue(user, rev_manager.clone(), document); let doc_id = doc_id.to_string(); - let editor = Arc::new(Self { doc_id, command_sender }); + let editor = Arc::new(Self { + doc_id, + command_sender, + rev_manager, + }); Ok(editor) } @@ -53,6 +59,13 @@ impl AppFlowyDocumentEditor { let content = rx.await.map_err(internal_error)??; Ok(content) } + + pub async fn duplicate_document(&self) -> FlowyResult { + let revisions = self.rev_manager.load_revisions().await?; + let transaction = make_transaction_from_revisions(&revisions)?; + let json = transaction.to_json()?; + Ok(json) + } } fn spawn_edit_queue( @@ -67,11 +80,24 @@ fn spawn_edit_queue( } impl DocumentEditor for Arc { + fn close(&self) {} + fn export(&self) -> FutureResult { let this = self.clone(); FutureResult::new(async move { this.get_content(false).await }) } + fn duplicate(&self) -> FutureResult { + let this = self.clone(); + FutureResult::new(async move { this.duplicate_document().await }) + } + + fn receive_ws_data(&self, _data: ServerRevisionWSData) -> FutureResult<(), FlowyError> { + FutureResult::new(async move { Ok(()) }) + } + + fn receive_ws_state(&self, _state: &WSConnectState) {} + fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError> { let this = self.clone(); FutureResult::new(async move { @@ -81,14 +107,6 @@ impl DocumentEditor for Arc { }) } - fn close(&self) {} - - fn receive_ws_data(&self, _data: ServerRevisionWSData) -> FutureResult<(), FlowyError> { - FutureResult::new(async move { Ok(()) }) - } - - fn receive_ws_state(&self, _state: &WSConnectState) {} - fn as_any(&self) -> &dyn Any { self } diff --git a/frontend/rust-lib/flowy-document/src/editor/migration/delta_migration.rs b/frontend/rust-lib/flowy-document/src/editor/migration/delta_migration.rs new file mode 100644 index 0000000000..ca390e5058 --- /dev/null +++ b/frontend/rust-lib/flowy-document/src/editor/migration/delta_migration.rs @@ -0,0 +1,419 @@ +use crate::editor::{DocumentNode, DocumentOperation}; +use flowy_error::FlowyResult; + +use lib_ot::core::{AttributeHashMap, DeltaOperation, Insert, Transaction}; +use lib_ot::text_delta::{DeltaTextOperation, DeltaTextOperations}; + +pub struct DeltaRevisionMigration(); + +impl DeltaRevisionMigration { + pub fn run(delta: DeltaTextOperations) -> FlowyResult { + let migrate_background_attribute = |insert: &mut Insert| { + if let Some(Some(color)) = insert.attributes.get("background").map(|value| value.str_value()) { + insert.attributes.remove_key("background"); + insert.attributes.insert("backgroundColor", color); + } + }; + let migrate_strike_attribute = |insert: &mut Insert| { + if let Some(Some(_)) = insert.attributes.get("strike").map(|value| value.str_value()) { + insert.attributes.remove_key("strike"); + insert.attributes.insert("strikethrough", true); + } + }; + + let migrate_link_attribute = |insert: &mut Insert| { + if let Some(Some(link)) = insert.attributes.get("link").map(|value| value.str_value()) { + insert.attributes.remove_key("link"); + insert.attributes.insert("href", link); + } + }; + + let migrate_list_attribute = + |attribute_node: &mut DocumentNode, value: &str, number_list_number: &mut usize| { + if value == "unchecked" { + *number_list_number = 0; + attribute_node.attributes.insert("subtype", "checkbox"); + attribute_node.attributes.insert("checkbox", false); + } + if value == "checked" { + *number_list_number = 0; + attribute_node.attributes.insert("subtype", "checkbox"); + attribute_node.attributes.insert("checkbox", true); + } + + if value == "bullet" { + *number_list_number = 0; + attribute_node.attributes.insert("subtype", "bulleted-list"); + } + + if value == "ordered" { + *number_list_number += 1; + attribute_node.attributes.insert("subtype", "number-list"); + attribute_node.attributes.insert("number", *number_list_number); + } + }; + + let generate_new_op_with_double_new_lines = |insert: &mut Insert| { + let pattern = "\n\n"; + let mut new_ops = vec![]; + if insert.s.as_str().contains(pattern) { + let insert_str = insert.s.clone(); + let insert_strings = insert_str.split(pattern).map(|s| s.to_owned()); + for (index, new_s) in insert_strings.enumerate() { + if index == 0 { + insert.s = new_s.into(); + } else { + new_ops.push(DeltaOperation::Insert(Insert { + s: new_s.into(), + attributes: AttributeHashMap::default(), + })); + } + } + } + new_ops + }; + + let create_text_node = |ops: Vec| { + let mut document_node = DocumentNode::new(); + document_node.node_type = "text".to_owned(); + ops.into_iter().for_each(|op| document_node.delta.add(op)); + document_node + }; + + let transform_op = |mut insert: Insert| { + // Rename the attribute name from background to backgroundColor + migrate_background_attribute(&mut insert); + migrate_strike_attribute(&mut insert); + migrate_link_attribute(&mut insert); + + let new_ops = generate_new_op_with_double_new_lines(&mut insert); + (DeltaOperation::Insert(insert), new_ops) + }; + let mut index: usize = 0; + let mut number_list_number = 0; + let mut editor_node = DocumentNode::new(); + editor_node.node_type = "editor".to_owned(); + + let mut transaction = Transaction::new(); + transaction.push_operation(DocumentOperation::Insert { + path: 0.into(), + nodes: vec![editor_node], + }); + + let mut iter = delta.ops.into_iter().enumerate(); + while let Some((_, op)) = iter.next() { + let mut document_node = create_text_node(vec![]); + let mut split_document_nodes = vec![]; + match op { + DeltaOperation::Delete(_) => tracing::warn!("Should not contain delete operation"), + DeltaOperation::Retain(_) => tracing::warn!("Should not contain retain operation"), + DeltaOperation::Insert(insert) => { + if insert.s.as_str() != "\n" { + let (op, new_ops) = transform_op(insert); + document_node.delta.add(op); + if !new_ops.is_empty() { + split_document_nodes.push(create_text_node(new_ops)); + } + } + + while let Some((_, DeltaOperation::Insert(insert))) = iter.next() { + if insert.s.as_str() != "\n" { + let (op, new_ops) = transform_op(insert); + document_node.delta.add(op); + + if !new_ops.is_empty() { + split_document_nodes.push(create_text_node(new_ops)); + } + } else { + let attribute_node = match split_document_nodes.last_mut() { + None => &mut document_node, + Some(split_document_node) => split_document_node, + }; + + if let Some(value) = insert.attributes.get("header") { + attribute_node.attributes.insert("subtype", "heading"); + if let Some(v) = value.int_value() { + number_list_number = 0; + attribute_node.attributes.insert("heading", format!("h{}", v)); + } + } + + if insert.attributes.get("blockquote").is_some() { + attribute_node.attributes.insert("subtype", "quote"); + } + + if let Some(value) = insert.attributes.get("list") { + if let Some(s) = value.str_value() { + migrate_list_attribute(attribute_node, &s, &mut number_list_number); + } + } + break; + } + } + } + } + let mut operations = vec![document_node]; + operations.extend(split_document_nodes); + operations.into_iter().for_each(|node| { + // println!("{}", serde_json::to_string(&node).unwrap()); + let operation = DocumentOperation::Insert { + path: vec![0, index].into(), + nodes: vec![node], + }; + transaction.push_operation(operation); + index += 1; + }); + } + Ok(transaction) + } +} + +#[cfg(test)] +mod tests { + use crate::editor::migration::delta_migration::DeltaRevisionMigration; + use crate::editor::Document; + use lib_ot::text_delta::DeltaTextOperations; + + #[test] + fn transform_delta_to_transaction_test() { + let delta = DeltaTextOperations::from_json(DELTA_STR).unwrap(); + let transaction = DeltaRevisionMigration::run(delta).unwrap(); + let document = Document::from_transaction(transaction).unwrap(); + let s = document.get_content(true).unwrap(); + assert!(!s.is_empty()); + } + + const DELTA_STR: &str = r#"[ + { + "insert": "\n👋 Welcome to AppFlowy!" + }, + { + "insert": "\n", + "attributes": { + "header": 1 + } + }, + { + "insert": "\nHere are the basics" + }, + { + "insert": "\n", + "attributes": { + "header": 2 + } + }, + { + "insert": "Click anywhere and just start typing" + }, + { + "insert": "\n", + "attributes": { + "list": "unchecked" + } + }, + { + "insert": "Highlight", + "attributes": { + "background": "$fff2cd" + } + }, + { + "insert": " any text, and use the menu at the bottom to " + }, + { + "insert": "style", + "attributes": { + "italic": true + } + }, + { + "insert": " " + }, + { + "insert": "your", + "attributes": { + "bold": true + } + }, + { + "insert": " " + }, + { + "insert": "writing", + "attributes": { + "underline": true + } + }, + { + "insert": " " + }, + { + "insert": "however", + "attributes": { + "code": true + } + }, + { + "insert": " " + }, + { + "insert": "you", + "attributes": { + "strike": true + } + }, + { + "insert": " " + }, + { + "insert": "like", + "attributes": { + "background": "$e8e0ff" + } + }, + { + "insert": "\n", + "attributes": { + "list": "unchecked" + } + }, + { + "insert": "Click " + }, + { + "insert": "+ New Page", + "attributes": { + "background": "$defff1", + "bold": true + } + }, + { + "insert": " button at the bottom of your sidebar to add a new page" + }, + { + "insert": "\n", + "attributes": { + "list": "unchecked" + } + }, + { + "insert": "Click the " + }, + { + "insert": "'", + "attributes": { + "background": "$defff1" + } + }, + { + "insert": "+", + "attributes": { + "background": "$defff1", + "bold": true + } + }, + { + "insert": "'", + "attributes": { + "background": "$defff1" + } + }, + { + "insert": " next to any page title in the sidebar to quickly add a new subpage" + }, + { + "insert": "\n", + "attributes": { + "list": "unchecked" + } + }, + { + "insert": "\nHave a question? " + }, + { + "insert": "\n", + "attributes": { + "header": 2 + } + }, + { + "insert": "Click the " + }, + { + "insert": "'?'", + "attributes": { + "background": "$defff1", + "bold": true + } + }, + { + "insert": " at the bottom right for help and support.\n\nLike AppFlowy? Follow us:" + }, + { + "insert": "\n", + "attributes": { + "header": 2 + } + }, + { + "insert": "GitHub: https://github.com/AppFlowy-IO/appflowy" + }, + { + "insert": "\n", + "attributes": { + "blockquote": true + } + }, + { + "insert": "Twitter: https://twitter.com/appflowy" + }, + { + "insert": "\n", + "attributes": { + "blockquote": true + } + }, + { + "insert": "Newsletter: https://www.appflowy.io/blog" + }, + { + "insert": "\n", + "attributes": { + "blockquote": true + } + }, + { + "insert": "item 1" + }, + { + "insert": "\n", + "attributes": { + "list": "ordered" + } + }, + { + "insert": "item 2" + }, + { + "insert": "\n", + "attributes": { + "list": "ordered" + } + }, + { + "insert": "item3" + }, + { + "insert": "\n", + "attributes": { + "list": "ordered" + } + }, + { + "insert": "appflowy", + "attributes": { + "link": "https://www.appflowy.io/" + } + } +]"#; +} diff --git a/frontend/rust-lib/flowy-document/src/editor/migration/mod.rs b/frontend/rust-lib/flowy-document/src/editor/migration/mod.rs new file mode 100644 index 0000000000..b838953a26 --- /dev/null +++ b/frontend/rust-lib/flowy-document/src/editor/migration/mod.rs @@ -0,0 +1,3 @@ +mod delta_migration; + +pub use delta_migration::*; diff --git a/frontend/rust-lib/flowy-document/src/editor/mod.rs b/frontend/rust-lib/flowy-document/src/editor/mod.rs index f7048685f3..ec1a6e43af 100644 --- a/frontend/rust-lib/flowy-document/src/editor/mod.rs +++ b/frontend/rust-lib/flowy-document/src/editor/mod.rs @@ -2,7 +2,17 @@ mod document; mod document_serde; mod editor; +mod migration; mod queue; pub use document::*; +pub use document_serde::*; pub use editor::*; +pub use migration::*; + +#[inline] +pub fn initial_read_me() -> String { + let document_content = include_str!("READ_ME.json"); + let transaction = make_transaction_from_document_content(document_content).unwrap(); + transaction.to_json().unwrap() +} diff --git a/frontend/rust-lib/flowy-document/src/editor/queue.rs b/frontend/rust-lib/flowy-document/src/editor/queue.rs index f793f880c5..3fb1f35fbd 100644 --- a/frontend/rust-lib/flowy-document/src/editor/queue.rs +++ b/frontend/rust-lib/flowy-document/src/editor/queue.rs @@ -1,13 +1,17 @@ use crate::editor::document::Document; use crate::DocumentUser; use async_stream::stream; +use bytes::Bytes; use flowy_error::FlowyError; use flowy_revision::RevisionManager; +use flowy_sync::entities::revision::{RevId, Revision}; use futures::stream::StreamExt; use lib_ot::core::Transaction; + use std::sync::Arc; use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::{oneshot, RwLock}; + pub struct DocumentQueue { #[allow(dead_code)] user: Arc, @@ -56,7 +60,10 @@ impl DocumentQueue { async fn handle_command(&self, command: Command) -> Result<(), FlowyError> { match command { Command::ComposeTransaction { transaction, ret } => { - self.document.write().await.apply_transaction(transaction)?; + self.document.write().await.apply_transaction(transaction.clone())?; + let _ = self + .save_local_operations(transaction, self.document.read().await.md5()) + .await?; let _ = ret.send(Ok(())); } Command::GetDocumentContent { pretty, ret } => { @@ -66,6 +73,16 @@ impl DocumentQueue { } Ok(()) } + + #[tracing::instrument(level = "trace", skip(self, transaction, md5), err)] + async fn save_local_operations(&self, transaction: Transaction, md5: String) -> Result { + let bytes = Bytes::from(transaction.to_bytes()?); + let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair(); + let user_id = self.user.user_id()?; + let revision = Revision::new(&self.rev_manager.object_id, base_rev_id, rev_id, bytes, &user_id, md5); + let _ = self.rev_manager.add_local_revision(&revision).await?; + Ok(rev_id.into()) + } } pub(crate) type CommandSender = Sender; diff --git a/frontend/rust-lib/flowy-document/src/entities.rs b/frontend/rust-lib/flowy-document/src/entities.rs index f95cdefefc..4fadde6d7f 100644 --- a/frontend/rust-lib/flowy-document/src/entities.rs +++ b/frontend/rust-lib/flowy-document/src/entities.rs @@ -74,12 +74,41 @@ pub struct ExportPayloadPB { #[pb(index = 2)] pub export_type: ExportType, + + #[pb(index = 3)] + pub document_version: DocumentVersionPB, +} + +#[derive(PartialEq, Debug, ProtoBuf_Enum, Clone)] +pub enum DocumentVersionPB { + /// this version's content of the document is build from `Delta`. It uses + /// `DeltaDocumentEditor`. + V0 = 0, + /// this version's content of the document is build from `NodeTree`. It uses + /// `AppFlowyDocumentEditor` + V1 = 1, +} + +impl std::default::Default for DocumentVersionPB { + fn default() -> Self { + Self::V0 + } +} + +#[derive(Default, ProtoBuf)] +pub struct OpenDocumentContextPB { + #[pb(index = 1)] + pub document_id: String, + + #[pb(index = 2)] + pub document_version: DocumentVersionPB, } #[derive(Default, Debug)] pub struct ExportParams { pub view_id: String, pub export_type: ExportType, + pub document_version: DocumentVersionPB, } impl TryInto for ExportPayloadPB { @@ -88,6 +117,7 @@ impl TryInto for ExportPayloadPB { Ok(ExportParams { view_id: self.view_id, export_type: self.export_type, + document_version: self.document_version, }) } } diff --git a/frontend/rust-lib/flowy-document/src/event_handler.rs b/frontend/rust-lib/flowy-document/src/event_handler.rs index d51a9f19f7..2d8d99968b 100644 --- a/frontend/rust-lib/flowy-document/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document/src/event_handler.rs @@ -1,21 +1,23 @@ -use crate::entities::{DocumentSnapshotPB, EditParams, EditPayloadPB, ExportDataPB, ExportParams, ExportPayloadPB}; +use crate::entities::{ + DocumentSnapshotPB, EditParams, EditPayloadPB, ExportDataPB, ExportParams, ExportPayloadPB, OpenDocumentContextPB, +}; use crate::DocumentManager; use flowy_error::FlowyError; -use flowy_sync::entities::document::DocumentIdPB; + use lib_dispatch::prelude::{data_result, AppData, Data, DataResult}; use std::convert::TryInto; use std::sync::Arc; pub(crate) async fn get_document_handler( - data: Data, + data: Data, manager: AppData>, ) -> DataResult { - let document_id: DocumentIdPB = data.into_inner(); - let editor = manager.open_document_editor(&document_id).await?; - let operations_str = editor.export().await?; + let context: OpenDocumentContextPB = data.into_inner(); + let editor = manager.open_document_editor(&context.document_id).await?; + let document_data = editor.export().await?; data_result(DocumentSnapshotPB { - doc_id: document_id.into(), - snapshot: operations_str, + doc_id: context.document_id, + snapshot: document_data, }) } diff --git a/frontend/rust-lib/flowy-document/src/event_map.rs b/frontend/rust-lib/flowy-document/src/event_map.rs index 71042cad03..50aa9cf7cf 100644 --- a/frontend/rust-lib/flowy-document/src/event_map.rs +++ b/frontend/rust-lib/flowy-document/src/event_map.rs @@ -19,7 +19,7 @@ pub fn create(document_manager: Arc) -> Module { #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[event_err = "FlowyError"] pub enum DocumentEvent { - #[event(input = "DocumentIdPB", output = "DocumentSnapshotPB")] + #[event(input = "OpenDocumentContextPB", output = "DocumentSnapshotPB")] GetDocument = 0, #[event(input = "EditPayloadPB")] diff --git a/frontend/rust-lib/flowy-document/src/lib.rs b/frontend/rust-lib/flowy-document/src/lib.rs index c5496507c1..0e8bba3bbc 100644 --- a/frontend/rust-lib/flowy-document/src/lib.rs +++ b/frontend/rust-lib/flowy-document/src/lib.rs @@ -1,4 +1,4 @@ -mod entities; +pub mod entities; mod event_handler; pub mod event_map; pub mod manager; @@ -6,6 +6,7 @@ pub mod manager; pub mod editor; pub mod old_editor; pub mod protobuf; +mod services; pub use manager::*; pub mod errors { diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index da82ec8c29..f349c52cd9 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -1,23 +1,23 @@ -use crate::editor::{initial_document_content, AppFlowyDocumentEditor}; -use crate::entities::EditParams; -use crate::old_editor::editor::{DeltaDocumentEditor, DocumentRevisionCompress}; +use crate::editor::{initial_document_content, AppFlowyDocumentEditor, DocumentRevisionCompress}; +use crate::entities::{DocumentVersionPB, EditParams}; +use crate::old_editor::editor::{DeltaDocumentEditor, DeltaDocumentRevisionCompress}; +use crate::services::DocumentPersistence; use crate::{errors::FlowyError, DocumentCloudService}; use bytes::Bytes; use dashmap::DashMap; use flowy_database::ConnectionPool; use flowy_error::FlowyResult; -use flowy_revision::disk::SQLiteDocumentRevisionPersistence; +use flowy_revision::disk::{SQLiteDeltaDocumentRevisionPersistence, SQLiteDocumentRevisionPersistence}; use flowy_revision::{ RevisionCloudService, RevisionManager, RevisionPersistence, RevisionWebSocket, SQLiteRevisionSnapshotPersistence, }; -use flowy_sync::client_document::initial_old_document_content; +use flowy_sync::client_document::initial_delta_document_content; use flowy_sync::entities::{ - document::{DocumentIdPB, DocumentOperationsPB}, + document::DocumentIdPB, revision::{md5, RepeatedRevision, Revision}, ws_data::ServerRevisionWSData, }; use lib_infra::future::FutureResult; - use lib_ws::WSConnectState; use std::any::Any; use std::{convert::TryInto, sync::Arc}; @@ -26,17 +26,31 @@ pub trait DocumentUser: Send + Sync { fn user_dir(&self) -> Result; fn user_id(&self) -> Result; fn token(&self) -> Result; +} + +pub trait DocumentDatabase: Send + Sync { fn db_pool(&self) -> Result, FlowyError>; } pub trait DocumentEditor: Send + Sync { - fn export(&self) -> FutureResult; - fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError>; + /// Called when the document get closed fn close(&self); + /// Exports the document content. The content is encoded in the corresponding + /// editor data format. + fn export(&self) -> FutureResult; + + /// Duplicate the document inner data into String + fn duplicate(&self) -> FutureResult; + fn receive_ws_data(&self, data: ServerRevisionWSData) -> FutureResult<(), FlowyError>; + fn receive_ws_state(&self, state: &WSConnectState); + /// Receives the local operations made by the user input. The operations are encoded + /// in binary format. + fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError>; + /// Returns the `Any` reference that can be used to downcast back to the original, /// concrete type. /// @@ -50,7 +64,15 @@ pub trait DocumentEditor: Send + Sync { #[derive(Clone, Debug)] pub struct DocumentConfig { - pub use_new_editor: bool, + pub version: DocumentVersionPB, +} + +impl std::default::Default for DocumentConfig { + fn default() -> Self { + Self { + version: DocumentVersionPB::V1, + } + } } pub struct DocumentManager { @@ -58,6 +80,8 @@ pub struct DocumentManager { rev_web_socket: Arc, editor_map: Arc, user: Arc, + persistence: Arc, + #[allow(dead_code)] config: DocumentConfig, } @@ -65,6 +89,7 @@ impl DocumentManager { pub fn new( cloud_service: Arc, document_user: Arc, + database: Arc, rev_web_socket: Arc, config: DocumentConfig, ) -> Self { @@ -73,24 +98,31 @@ impl DocumentManager { rev_web_socket, editor_map: Arc::new(DocumentEditorMap::new()), user: document_user, + persistence: Arc::new(DocumentPersistence::new(database)), config, } } - pub fn init(&self) -> FlowyResult<()> { + /// Called immediately after the application launched with the user sign in/sign up. + #[tracing::instrument(level = "trace", skip_all, err)] + pub async fn initialize(&self, user_id: &str) -> FlowyResult<()> { + let _ = self.persistence.initialize(user_id)?; listen_ws_state_changed(self.rev_web_socket.clone(), self.editor_map.clone()); - Ok(()) } - #[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)] + pub async fn initialize_with_new_user(&self, _user_id: &str, _token: &str) -> FlowyResult<()> { + Ok(()) + } + + #[tracing::instrument(level = "trace", skip_all, fields(document_id), err)] pub async fn open_document_editor>( &self, - editor_id: T, + document_id: T, ) -> Result, FlowyError> { - let editor_id = editor_id.as_ref(); - tracing::Span::current().record("editor_id", &editor_id); - self.init_document_editor(editor_id).await + let document_id = document_id.as_ref(); + tracing::Span::current().record("document_id", &document_id); + self.init_document_editor(document_id).await } #[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)] @@ -101,22 +133,6 @@ impl DocumentManager { Ok(()) } - #[tracing::instrument(level = "debug", skip(self, payload), err)] - pub async fn receive_local_operations( - &self, - payload: DocumentOperationsPB, - ) -> Result { - let editor = self.get_document_editor(&payload.doc_id).await?; - let _ = editor - .compose_local_operations(Bytes::from(payload.operations_str)) - .await?; - let operations_str = editor.export().await?; - Ok(DocumentOperationsPB { - doc_id: payload.doc_id.clone(), - operations_str, - }) - } - pub async fn apply_edit(&self, params: EditParams) -> FlowyResult<()> { let editor = self.get_document_editor(¶ms.doc_id).await?; let _ = editor.compose_local_operations(Bytes::from(params.operations)).await?; @@ -125,9 +141,9 @@ impl DocumentManager { pub async fn create_document>(&self, doc_id: T, revisions: RepeatedRevision) -> FlowyResult<()> { let doc_id = doc_id.as_ref().to_owned(); - let db_pool = self.user.db_pool()?; + let db_pool = self.persistence.database.db_pool()?; // Maybe we could save the document to disk without creating the RevisionManager - let rev_manager = self.make_document_rev_manager(&doc_id, db_pool)?; + let rev_manager = self.make_rev_manager(&doc_id, db_pool)?; let _ = rev_manager.reset_object(revisions).await?; Ok(()) } @@ -149,10 +165,9 @@ impl DocumentManager { } pub fn initial_document_content(&self) -> String { - if self.config.use_new_editor { - initial_document_content() - } else { - initial_old_document_content() + match self.config.version { + DocumentVersionPB::V0 => initial_delta_document_content(), + DocumentVersionPB::V1 => initial_document_content(), } } } @@ -168,7 +183,11 @@ impl DocumentManager { /// async fn get_document_editor(&self, doc_id: &str) -> FlowyResult> { match self.editor_map.get(doc_id) { - None => self.init_document_editor(doc_id).await, + None => { + // + tracing::warn!("Should call init_document_editor first"); + self.init_document_editor(doc_id).await + } Some(editor) => Ok(editor), } } @@ -184,25 +203,39 @@ impl DocumentManager { /// #[tracing::instrument(level = "trace", skip(self), err)] pub async fn init_document_editor(&self, doc_id: &str) -> Result, FlowyError> { - let pool = self.user.db_pool()?; + let pool = self.persistence.database.db_pool()?; let user = self.user.clone(); let token = self.user.token()?; - let rev_manager = self.make_document_rev_manager(doc_id, pool.clone())?; let cloud_service = Arc::new(DocumentRevisionCloudService { token, server: self.cloud_service.clone(), }); - let editor: Arc = if self.config.use_new_editor { - let editor = AppFlowyDocumentEditor::new(doc_id, user, rev_manager, cloud_service).await?; - Arc::new(editor) - } else { - let editor = - DeltaDocumentEditor::new(doc_id, user, rev_manager, self.rev_web_socket.clone(), cloud_service).await?; - Arc::new(editor) - }; - self.editor_map.insert(doc_id, editor.clone()); - Ok(editor) + match self.config.version { + DocumentVersionPB::V0 => { + let rev_manager = self.make_delta_document_rev_manager(doc_id, pool.clone())?; + let editor: Arc = Arc::new( + DeltaDocumentEditor::new(doc_id, user, rev_manager, self.rev_web_socket.clone(), cloud_service) + .await?, + ); + self.editor_map.insert(doc_id, editor.clone()); + Ok(editor) + } + DocumentVersionPB::V1 => { + let rev_manager = self.make_document_rev_manager(doc_id, pool.clone())?; + let editor: Arc = + Arc::new(AppFlowyDocumentEditor::new(doc_id, user, rev_manager, cloud_service).await?); + self.editor_map.insert(doc_id, editor.clone()); + Ok(editor) + } + } + } + + fn make_rev_manager(&self, doc_id: &str, pool: Arc) -> Result { + match self.config.version { + DocumentVersionPB::V0 => self.make_delta_document_rev_manager(doc_id, pool), + DocumentVersionPB::V1 => self.make_document_rev_manager(doc_id, pool), + } } fn make_document_rev_manager( @@ -215,13 +248,31 @@ impl DocumentManager { let rev_persistence = RevisionPersistence::new(&user_id, doc_id, disk_cache); // let history_persistence = SQLiteRevisionHistoryPersistence::new(doc_id, pool.clone()); let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(doc_id, pool); - let rev_compactor = DocumentRevisionCompress(); - Ok(RevisionManager::new( &user_id, doc_id, rev_persistence, - rev_compactor, + DocumentRevisionCompress(), + // history_persistence, + snapshot_persistence, + )) + } + + fn make_delta_document_rev_manager( + &self, + doc_id: &str, + pool: Arc, + ) -> Result { + let user_id = self.user.user_id()?; + let disk_cache = SQLiteDeltaDocumentRevisionPersistence::new(&user_id, pool.clone()); + let rev_persistence = RevisionPersistence::new(&user_id, doc_id, disk_cache); + // let history_persistence = SQLiteRevisionHistoryPersistence::new(doc_id, pool.clone()); + let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(doc_id, pool); + Ok(RevisionManager::new( + &user_id, + doc_id, + rev_persistence, + DeltaDocumentRevisionCompress(), // history_persistence, snapshot_persistence, )) diff --git a/frontend/rust-lib/flowy-document/src/old_editor/editor.rs b/frontend/rust-lib/flowy-document/src/old_editor/editor.rs index 178a220793..3f1794c73c 100644 --- a/frontend/rust-lib/flowy-document/src/old_editor/editor.rs +++ b/frontend/rust-lib/flowy-document/src/old_editor/editor.rs @@ -18,7 +18,7 @@ use lib_infra::future::FutureResult; use lib_ot::core::{AttributeEntry, AttributeHashMap}; use lib_ot::{ core::{DeltaOperation, Interval}, - text_delta::TextOperations, + text_delta::DeltaTextOperations, }; use lib_ws::WSConnectState; use std::any::Any; @@ -46,7 +46,7 @@ impl DeltaDocumentEditor { let document = rev_manager .load::(Some(cloud_service)) .await?; - let operations = TextOperations::from_bytes(&document.content)?; + let operations = DeltaTextOperations::from_bytes(&document.content)?; let rev_manager = Arc::new(rev_manager); let doc_id = doc_id.to_string(); let user_id = user.user_id()?; @@ -147,6 +147,11 @@ impl DeltaDocumentEditor { } impl DocumentEditor for Arc { + fn close(&self) { + #[cfg(feature = "sync")] + self.ws_manager.stop(); + } + fn export(&self) -> FutureResult { let (ret, rx) = oneshot::channel::>(); let msg = EditorCommand::GetOperationsString { ret }; @@ -158,22 +163,8 @@ impl DocumentEditor for Arc { }) } - fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError> { - let edit_cmd_tx = self.edit_cmd_tx.clone(); - FutureResult::new(async move { - let operations = TextOperations::from_bytes(&data)?; - let (ret, rx) = oneshot::channel::>(); - let msg = EditorCommand::ComposeLocalOperations { operations, ret }; - - let _ = edit_cmd_tx.send(msg).await; - let _ = rx.await.map_err(internal_error)??; - Ok(()) - }) - } - - fn close(&self) { - #[cfg(feature = "sync")] - self.ws_manager.stop(); + fn duplicate(&self) -> FutureResult { + self.export() } #[allow(unused_variables)] @@ -193,6 +184,19 @@ impl DocumentEditor for Arc { self.ws_manager.connect_state_changed(state.clone()); } + fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError> { + let edit_cmd_tx = self.edit_cmd_tx.clone(); + FutureResult::new(async move { + let operations = DeltaTextOperations::from_bytes(&data)?; + let (ret, rx) = oneshot::channel::>(); + let msg = EditorCommand::ComposeLocalOperations { operations, ret }; + + let _ = edit_cmd_tx.send(msg).await; + let _ = rx.await.map_err(internal_error)??; + Ok(()) + }) + } + fn as_any(&self) -> &dyn Any { self } @@ -207,7 +211,7 @@ impl std::ops::Drop for DeltaDocumentEditor { fn spawn_edit_queue( user: Arc, rev_manager: Arc, - delta: TextOperations, + delta: DeltaTextOperations, ) -> EditorCommandSender { let (sender, receiver) = mpsc::channel(1000); let edit_queue = EditDocumentQueue::new(user, rev_manager, delta, receiver); @@ -226,8 +230,8 @@ fn spawn_edit_queue( #[cfg(feature = "flowy_unit_test")] impl DeltaDocumentEditor { - pub async fn document_operations(&self) -> FlowyResult { - let (ret, rx) = oneshot::channel::>(); + pub async fn document_operations(&self) -> FlowyResult { + let (ret, rx) = oneshot::channel::>(); let msg = EditorCommand::GetOperations { ret }; let _ = self.edit_cmd_tx.send(msg).await; let delta = rx.await.map_err(internal_error)??; @@ -264,8 +268,8 @@ impl RevisionObjectSerializer for DeltaDocumentRevisionSerde { } } -pub(crate) struct DocumentRevisionCompress(); -impl RevisionCompress for DocumentRevisionCompress { +pub(crate) struct DeltaDocumentRevisionCompress(); +impl RevisionCompress for DeltaDocumentRevisionCompress { fn combine_revisions(&self, revisions: Vec) -> FlowyResult { DeltaDocumentRevisionSerde::combine_revisions(revisions) } @@ -273,7 +277,7 @@ impl RevisionCompress for DocumentRevisionCompress { // quill-editor requires the delta should end with '\n' and only contains the // insert operation. The function, correct_delta maybe be removed in the future. -fn correct_delta(delta: &mut TextOperations) { +fn correct_delta(delta: &mut DeltaTextOperations) { if let Some(op) = delta.ops.last() { let op_data = op.get_data(); if !op_data.ends_with('\n') { diff --git a/frontend/rust-lib/flowy-document/src/old_editor/queue.rs b/frontend/rust-lib/flowy-document/src/old_editor/queue.rs index a5a70b8cd8..4a8c4731b4 100644 --- a/frontend/rust-lib/flowy-document/src/old_editor/queue.rs +++ b/frontend/rust-lib/flowy-document/src/old_editor/queue.rs @@ -1,4 +1,4 @@ -use crate::old_editor::web_socket::DocumentResolveOperations; +use crate::old_editor::web_socket::DeltaDocumentResolveOperations; use crate::DocumentUser; use async_stream::stream; use flowy_error::FlowyError; @@ -12,7 +12,7 @@ use futures::stream::StreamExt; use lib_ot::core::AttributeEntry; use lib_ot::{ core::{Interval, OperationTransform}, - text_delta::TextOperations, + text_delta::DeltaTextOperations, }; use std::sync::Arc; use tokio::sync::mpsc::{Receiver, Sender}; @@ -31,7 +31,7 @@ impl EditDocumentQueue { pub(crate) fn new( user: Arc, rev_manager: Arc, - operations: TextOperations, + operations: DeltaTextOperations, receiver: EditorCommandReceiver, ) -> Self { let document = Arc::new(RwLock::new(ClientDocument::from_operations(operations))); @@ -91,8 +91,8 @@ impl EditDocumentQueue { EditorCommand::TransformOperations { operations, ret } => { let f = || async { let read_guard = self.document.read().await; - let mut server_operations: Option = None; - let client_operations: TextOperations; + let mut server_operations: Option = None; + let client_operations: DeltaTextOperations; if read_guard.is_empty() { // Do nothing @@ -100,11 +100,11 @@ impl EditDocumentQueue { } else { let (s_prime, c_prime) = read_guard.get_operations().transform(&operations)?; client_operations = c_prime; - server_operations = Some(DocumentResolveOperations(s_prime)); + server_operations = Some(DeltaDocumentResolveOperations(s_prime)); } drop(read_guard); Ok::(TransformOperations { - client_operations: DocumentResolveOperations(client_operations), + client_operations: DeltaDocumentResolveOperations(client_operations), server_operations, }) }; @@ -174,7 +174,7 @@ impl EditDocumentQueue { Ok(()) } - async fn save_local_operations(&self, operations: TextOperations, md5: String) -> Result { + async fn save_local_operations(&self, operations: DeltaTextOperations, md5: String) -> Result { let bytes = operations.json_bytes(); let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair(); let user_id = self.user.user_id()?; @@ -184,26 +184,26 @@ impl EditDocumentQueue { } } -pub type TextTransformOperations = TransformOperations; +pub type TextTransformOperations = TransformOperations; pub(crate) type EditorCommandSender = Sender; pub(crate) type EditorCommandReceiver = Receiver; pub(crate) type Ret = oneshot::Sender>; pub(crate) enum EditorCommand { ComposeLocalOperations { - operations: TextOperations, + operations: DeltaTextOperations, ret: Ret<()>, }, ComposeRemoteOperation { - client_operations: TextOperations, + client_operations: DeltaTextOperations, ret: Ret, }, ResetOperations { - operations: TextOperations, + operations: DeltaTextOperations, ret: Ret, }, TransformOperations { - operations: TextOperations, + operations: DeltaTextOperations, ret: Ret, }, Insert { @@ -242,7 +242,7 @@ pub(crate) enum EditorCommand { }, #[allow(dead_code)] GetOperations { - ret: Ret, + ret: Ret, }, } diff --git a/frontend/rust-lib/flowy-document/src/old_editor/web_socket.rs b/frontend/rust-lib/flowy-document/src/old_editor/web_socket.rs index 28c680ef01..39a15e86b1 100644 --- a/frontend/rust-lib/flowy-document/src/old_editor/web_socket.rs +++ b/frontend/rust-lib/flowy-document/src/old_editor/web_socket.rs @@ -13,33 +13,35 @@ use flowy_sync::{ errors::CollaborateResult, }; use lib_infra::future::{BoxResultFuture, FutureResult}; -use lib_ot::text_delta::TextOperations; +use lib_ot::text_delta::DeltaTextOperations; use lib_ws::WSConnectState; use std::{sync::Arc, time::Duration}; use tokio::sync::{broadcast, oneshot}; #[derive(Clone)] -pub struct DocumentResolveOperations(pub TextOperations); +pub struct DeltaDocumentResolveOperations(pub DeltaTextOperations); -impl OperationsDeserializer for DocumentResolveOperations { - fn deserialize_revisions(revisions: Vec) -> FlowyResult { - Ok(DocumentResolveOperations(make_operations_from_revisions(revisions)?)) +impl OperationsDeserializer for DeltaDocumentResolveOperations { + fn deserialize_revisions(revisions: Vec) -> FlowyResult { + Ok(DeltaDocumentResolveOperations(make_operations_from_revisions( + revisions, + )?)) } } -impl OperationsSerializer for DocumentResolveOperations { +impl OperationsSerializer for DeltaDocumentResolveOperations { fn serialize_operations(&self) -> Bytes { self.0.json_bytes() } } -impl DocumentResolveOperations { - pub fn into_inner(self) -> TextOperations { +impl DeltaDocumentResolveOperations { + pub fn into_inner(self) -> DeltaTextOperations { self.0 } } -pub type DocumentConflictController = ConflictController; +pub type DocumentConflictController = ConflictController; #[allow(dead_code)] pub(crate) async fn make_document_ws_manager( @@ -129,8 +131,11 @@ struct DocumentConflictResolver { edit_cmd_tx: EditorCommandSender, } -impl ConflictResolver for DocumentConflictResolver { - fn compose_operations(&self, operations: DocumentResolveOperations) -> BoxResultFuture { +impl ConflictResolver for DocumentConflictResolver { + fn compose_operations( + &self, + operations: DeltaDocumentResolveOperations, + ) -> BoxResultFuture { let tx = self.edit_cmd_tx.clone(); let operations = operations.into_inner(); Box::pin(async move { @@ -150,8 +155,8 @@ impl ConflictResolver for DocumentConflictResolver { fn transform_operations( &self, - operations: DocumentResolveOperations, - ) -> BoxResultFuture, FlowyError> { + operations: DeltaDocumentResolveOperations, + ) -> BoxResultFuture, FlowyError> { let tx = self.edit_cmd_tx.clone(); let operations = operations.into_inner(); Box::pin(async move { @@ -166,7 +171,10 @@ impl ConflictResolver for DocumentConflictResolver { }) } - fn reset_operations(&self, operations: DocumentResolveOperations) -> BoxResultFuture { + fn reset_operations( + &self, + operations: DeltaDocumentResolveOperations, + ) -> BoxResultFuture { let tx = self.edit_cmd_tx.clone(); let operations = operations.into_inner(); Box::pin(async move { diff --git a/frontend/rust-lib/flowy-document/src/services/migration.rs b/frontend/rust-lib/flowy-document/src/services/migration.rs new file mode 100644 index 0000000000..f3fc8f350d --- /dev/null +++ b/frontend/rust-lib/flowy-document/src/services/migration.rs @@ -0,0 +1,75 @@ +use crate::editor::DeltaRevisionMigration; +use crate::DocumentDatabase; +use bytes::Bytes; +use flowy_database::kv::KV; +use flowy_error::FlowyResult; +use flowy_revision::disk::{DeltaRevisionSql, RevisionDiskCache, RevisionRecord, SQLiteDocumentRevisionPersistence}; +use flowy_sync::entities::revision::{md5, Revision}; +use flowy_sync::util::make_operations_from_revisions; +use std::sync::Arc; + +const V1_MIGRATION: &str = "DOCUMENT_V1_MIGRATION"; +pub(crate) struct DocumentMigration { + user_id: String, + database: Arc, +} + +impl DocumentMigration { + pub fn new(user_id: &str, database: Arc) -> Self { + let user_id = user_id.to_owned(); + Self { user_id, database } + } + + pub fn run_v1_migration(&self) -> FlowyResult<()> { + let key = migration_flag_key(&self.user_id, V1_MIGRATION); + if KV::get_bool(&key) { + return Ok(()); + } + + let pool = self.database.db_pool()?; + let conn = &*pool.get()?; + let disk_cache = SQLiteDocumentRevisionPersistence::new(&self.user_id, pool); + let documents = DeltaRevisionSql::read_all_documents(&self.user_id, conn)?; + tracing::info!("[Document Migration]: try migrate {} documents", documents.len()); + for revisions in documents { + if revisions.is_empty() { + continue; + } + + let document_id = revisions.first().unwrap().object_id.clone(); + match make_operations_from_revisions(revisions) { + Ok(delta) => match DeltaRevisionMigration::run(delta) { + Ok(transaction) => { + let bytes = Bytes::from(transaction.to_bytes()?); + let md5 = format!("{:x}", md5::compute(&bytes)); + let revision = Revision::new(&document_id, 0, 1, bytes, &self.user_id, md5); + let record = RevisionRecord::new(revision); + match disk_cache.create_revision_records(vec![record]) { + Ok(_) => {} + Err(err) => { + tracing::error!("[Document Migration]: Save revisions to disk failed {:?}", err); + } + } + } + Err(err) => { + tracing::error!( + "[Document Migration]: Migrate revisions to transaction failed {:?}", + err + ); + } + }, + Err(e) => { + tracing::error!("[Document migration]: Make delta from revisions failed: {:?}", e); + } + } + } + // + + KV::set_bool(&key, true); + tracing::info!("Run document v1 migration"); + Ok(()) + } +} +fn migration_flag_key(user_id: &str, version: &str) -> String { + md5(format!("{}{}", user_id, version,)) +} diff --git a/frontend/rust-lib/flowy-document/src/services/mod.rs b/frontend/rust-lib/flowy-document/src/services/mod.rs new file mode 100644 index 0000000000..d92b4a1a53 --- /dev/null +++ b/frontend/rust-lib/flowy-document/src/services/mod.rs @@ -0,0 +1,4 @@ +mod migration; +mod persistence; + +pub use persistence::*; diff --git a/frontend/rust-lib/flowy-document/src/services/persistence.rs b/frontend/rust-lib/flowy-document/src/services/persistence.rs new file mode 100644 index 0000000000..5eb6859f78 --- /dev/null +++ b/frontend/rust-lib/flowy-document/src/services/persistence.rs @@ -0,0 +1,23 @@ +use crate::services::migration::DocumentMigration; +use crate::DocumentDatabase; +use flowy_error::FlowyResult; +use std::sync::Arc; + +pub struct DocumentPersistence { + pub database: Arc, +} + +impl DocumentPersistence { + pub fn new(database: Arc) -> Self { + Self { database } + } + + #[tracing::instrument(level = "trace", skip_all, err)] + pub fn initialize(&self, user_id: &str) -> FlowyResult<()> { + let migration = DocumentMigration::new(user_id, self.database.clone()); + if let Err(e) = migration.run_v1_migration() { + tracing::error!("[Document Migration]: run v1 migration failed: {:?}", e); + } + Ok(()) + } +} diff --git a/frontend/rust-lib/flowy-document/tests/editor/attribute_test.rs b/frontend/rust-lib/flowy-document/tests/editor/attribute_test.rs index f73c763aab..f17f8dad15 100644 --- a/frontend/rust-lib/flowy-document/tests/editor/attribute_test.rs +++ b/frontend/rust-lib/flowy-document/tests/editor/attribute_test.rs @@ -3,7 +3,7 @@ use crate::editor::{TestBuilder, TestOp::*}; use flowy_sync::client_document::{NewlineDocument, EmptyDocument}; use lib_ot::core::{Interval, OperationTransform, NEW_LINE, WHITESPACE, OTString}; use unicode_segmentation::UnicodeSegmentation; -use lib_ot::text_delta::TextOperations; +use lib_ot::text_delta::DeltaTextOperations; #[test] fn attributes_bold_added() { @@ -29,7 +29,7 @@ fn attributes_bold_added_and_invert_all() { Bold(0, Interval::new(0, 3), true), AssertDocJson(0, r#"[{"insert":"123","attributes":{"bold":true}}]"#), Bold(0, Interval::new(0, 3), false), - AssertDocJson(0, r#"[{"insert":"123"}]"#), + AssertDocJson(0, r#"[{"insert":"123","attributes":{"bold":false}}]"#), ]; TestBuilder::new().run_scripts::(ops); } @@ -41,7 +41,7 @@ fn attributes_bold_added_and_invert_partial_suffix() { Bold(0, Interval::new(0, 4), true), AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#), Bold(0, Interval::new(2, 4), false), - AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34"}]"#), + AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34","attributes":{"bold":false}}]"#), ]; TestBuilder::new().run_scripts::(ops); } @@ -53,7 +53,7 @@ fn attributes_bold_added_and_invert_partial_suffix2() { Bold(0, Interval::new(0, 4), true), AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#), Bold(0, Interval::new(2, 4), false), - AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34"}]"#), + AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34","attributes":{"bold":false}}]"#), Bold(0, Interval::new(2, 4), true), AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#), ]; @@ -95,7 +95,7 @@ fn attributes_bold_added_and_invert_partial_prefix() { Bold(0, Interval::new(0, 4), true), AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#), Bold(0, Interval::new(0, 2), false), - AssertDocJson(0, r#"[{"insert":"12"},{"insert":"34","attributes":{"bold":true}}]"#), + AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":false}},{"insert":"34","attributes":{"bold":true}}]"#), ]; TestBuilder::new().run_scripts::(ops); } @@ -762,12 +762,12 @@ fn attributes_preserve_list_format_on_merge() { #[test] fn delta_compose() { - let mut delta = TextOperations::from_json(r#"[{"insert":"\n"}]"#).unwrap(); + let mut delta = DeltaTextOperations::from_json(r#"[{"insert":"\n"}]"#).unwrap(); let deltas = vec![ - TextOperations::from_json(r#"[{"retain":1,"attributes":{"list":"unchecked"}}]"#).unwrap(), - TextOperations::from_json(r#"[{"insert":"a"}]"#).unwrap(), - TextOperations::from_json(r#"[{"retain":1},{"insert":"\n","attributes":{"list":"unchecked"}}]"#).unwrap(), - TextOperations::from_json(r#"[{"retain":2},{"retain":1,"attributes":{"list":""}}]"#).unwrap(), + DeltaTextOperations::from_json(r#"[{"retain":1,"attributes":{"list":"unchecked"}}]"#).unwrap(), + DeltaTextOperations::from_json(r#"[{"insert":"a"}]"#).unwrap(), + DeltaTextOperations::from_json(r#"[{"retain":1},{"insert":"\n","attributes":{"list":"unchecked"}}]"#).unwrap(), + DeltaTextOperations::from_json(r#"[{"retain":2},{"retain":1,"attributes":{"list":""}}]"#).unwrap(), ]; for d in deltas { diff --git a/frontend/rust-lib/flowy-document/tests/editor/mod.rs b/frontend/rust-lib/flowy-document/tests/editor/mod.rs index d750b459ca..f33cfb8057 100644 --- a/frontend/rust-lib/flowy-document/tests/editor/mod.rs +++ b/frontend/rust-lib/flowy-document/tests/editor/mod.rs @@ -8,7 +8,7 @@ use derive_more::Display; use flowy_sync::client_document::{ClientDocument, InitialDocument}; use lib_ot::{ core::*, - text_delta::{BuildInTextAttribute, TextOperations}, + text_delta::{BuildInTextAttribute, DeltaTextOperations}, }; use rand::{prelude::*, Rng as WrappedRng}; use std::{sync::Once, time::Duration}; @@ -81,8 +81,8 @@ pub enum TestOp { pub struct TestBuilder { documents: Vec, - deltas: Vec>, - primes: Vec>, + deltas: Vec>, + primes: Vec>, } impl TestBuilder { @@ -226,20 +226,20 @@ impl TestBuilder { TestOp::AssertDocJson(delta_i, expected) => { let delta_json = self.documents[*delta_i].get_operations_json(); - let expected_delta: TextOperations = serde_json::from_str(expected).unwrap(); - let target_delta: TextOperations = serde_json::from_str(&delta_json).unwrap(); + let expected_delta: DeltaTextOperations = serde_json::from_str(expected).unwrap(); + let target_delta: DeltaTextOperations = serde_json::from_str(&delta_json).unwrap(); if expected_delta != target_delta { - log::error!("✅ expect: {}", expected,); - log::error!("❌ receive: {}", delta_json); + println!("✅ expect: {}", expected,); + println!("❌ receive: {}", delta_json); } assert_eq!(target_delta, expected_delta); } TestOp::AssertPrimeJson(doc_i, expected) => { let prime_json = self.primes[*doc_i].as_ref().unwrap().json_str(); - let expected_prime: TextOperations = serde_json::from_str(expected).unwrap(); - let target_prime: TextOperations = serde_json::from_str(&prime_json).unwrap(); + let expected_prime: DeltaTextOperations = serde_json::from_str(expected).unwrap(); + let target_prime: DeltaTextOperations = serde_json::from_str(&prime_json).unwrap(); if expected_prime != target_prime { log::error!("✅ expect prime: {}", expected,); @@ -297,8 +297,8 @@ impl Rng { .collect() } - pub fn gen_delta(&mut self, s: &str) -> TextOperations { - let mut delta = TextOperations::default(); + pub fn gen_delta(&mut self, s: &str) -> DeltaTextOperations { + let mut delta = DeltaTextOperations::default(); let s = OTString::from(s); loop { let left = s.utf16_len() - delta.utf16_base_len; diff --git a/frontend/rust-lib/flowy-document/tests/editor/op_test.rs b/frontend/rust-lib/flowy-document/tests/editor/op_test.rs index 5dbc071125..b51febfedb 100644 --- a/frontend/rust-lib/flowy-document/tests/editor/op_test.rs +++ b/frontend/rust-lib/flowy-document/tests/editor/op_test.rs @@ -1,8 +1,8 @@ #![allow(clippy::all)] use crate::editor::{Rng, TestBuilder, TestOp::*}; use flowy_sync::client_document::{EmptyDocument, NewlineDocument}; -use lib_ot::text_delta::TextOperationBuilder; -use lib_ot::{core::Interval, core::*, text_delta::TextOperations}; +use lib_ot::text_delta::DeltaTextOperationBuilder; +use lib_ot::{core::Interval, core::*, text_delta::DeltaTextOperations}; #[test] fn attributes_insert_text() { @@ -37,7 +37,7 @@ fn attributes_insert_text_at_middle() { #[test] fn delta_get_ops_in_interval_1() { let operations = OperationsBuilder::new().insert("123").insert("4").build(); - let delta = TextOperationBuilder::from_operations(operations); + let delta = DeltaTextOperationBuilder::from_operations(operations); let mut iterator = OperationIterator::from_interval(&delta, Interval::new(0, 4)); assert_eq!(iterator.ops(), delta.ops); @@ -45,7 +45,7 @@ fn delta_get_ops_in_interval_1() { #[test] fn delta_get_ops_in_interval_2() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); let insert_a = DeltaOperation::insert("123"); let insert_b = DeltaOperation::insert("4"); let insert_c = DeltaOperation::insert("5"); @@ -89,7 +89,7 @@ fn delta_get_ops_in_interval_2() { #[test] fn delta_get_ops_in_interval_3() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); let insert_a = DeltaOperation::insert("123456"); delta.add(insert_a.clone()); assert_eq!( @@ -100,7 +100,7 @@ fn delta_get_ops_in_interval_3() { #[test] fn delta_get_ops_in_interval_4() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); let insert_a = DeltaOperation::insert("12"); let insert_b = DeltaOperation::insert("34"); let insert_c = DeltaOperation::insert("56"); @@ -130,7 +130,7 @@ fn delta_get_ops_in_interval_4() { #[test] fn delta_get_ops_in_interval_5() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); let insert_a = DeltaOperation::insert("123456"); let insert_b = DeltaOperation::insert("789"); delta.ops.push(insert_a.clone()); @@ -148,7 +148,7 @@ fn delta_get_ops_in_interval_5() { #[test] fn delta_get_ops_in_interval_6() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); let insert_a = DeltaOperation::insert("12345678"); delta.add(insert_a.clone()); assert_eq!( @@ -159,7 +159,7 @@ fn delta_get_ops_in_interval_6() { #[test] fn delta_get_ops_in_interval_7() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); let insert_a = DeltaOperation::insert("12345"); let retain_a = DeltaOperation::retain(3); @@ -179,7 +179,7 @@ fn delta_get_ops_in_interval_7() { #[test] fn delta_op_seek() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); let insert_a = DeltaOperation::insert("12345"); let retain_a = DeltaOperation::retain(3); delta.add(insert_a.clone()); @@ -191,7 +191,7 @@ fn delta_op_seek() { #[test] fn delta_utf16_code_unit_seek() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); delta.add(DeltaOperation::insert("12345")); let mut iter = OperationIterator::new(&delta); @@ -201,7 +201,7 @@ fn delta_utf16_code_unit_seek() { #[test] fn delta_utf16_code_unit_seek_with_attributes() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); let attributes = AttributeBuilder::new() .insert("bold", true) .insert("italic", true) @@ -221,7 +221,7 @@ fn delta_utf16_code_unit_seek_with_attributes() { #[test] fn delta_next_op_len() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); delta.add(DeltaOperation::insert("12345")); let mut iter = OperationIterator::new(&delta); assert_eq!(iter.next_op_with_len(2).unwrap(), DeltaOperation::insert("12")); @@ -232,7 +232,7 @@ fn delta_next_op_len() { #[test] fn delta_next_op_len_with_chinese() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); delta.add(DeltaOperation::insert("你好")); let mut iter = OperationIterator::new(&delta); @@ -242,7 +242,7 @@ fn delta_next_op_len_with_chinese() { #[test] fn delta_next_op_len_with_english() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); delta.add(DeltaOperation::insert("ab")); let mut iter = OperationIterator::new(&delta); assert_eq!(iter.next_op_len().unwrap(), 2); @@ -251,7 +251,7 @@ fn delta_next_op_len_with_english() { #[test] fn delta_next_op_len_after_seek() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); delta.add(DeltaOperation::insert("12345")); let mut iter = OperationIterator::new(&delta); assert_eq!(iter.next_op_len().unwrap(), 5); @@ -264,7 +264,7 @@ fn delta_next_op_len_after_seek() { #[test] fn delta_next_op_len_none() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); delta.add(DeltaOperation::insert("12345")); let mut iter = OperationIterator::new(&delta); @@ -275,7 +275,7 @@ fn delta_next_op_len_none() { #[test] fn delta_next_op_with_len_zero() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); delta.add(DeltaOperation::insert("12345")); let mut iter = OperationIterator::new(&delta); assert_eq!(iter.next_op_with_len(0), None,); @@ -284,7 +284,7 @@ fn delta_next_op_with_len_zero() { #[test] fn delta_next_op_with_len_cross_op_return_last() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); delta.add(DeltaOperation::insert("12345")); delta.add(DeltaOperation::retain(1)); delta.add(DeltaOperation::insert("678")); @@ -297,7 +297,7 @@ fn delta_next_op_with_len_cross_op_return_last() { #[test] fn lengths() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); assert_eq!(delta.utf16_base_len, 0); assert_eq!(delta.utf16_target_len, 0); delta.retain(5, AttributeHashMap::default()); @@ -315,7 +315,7 @@ fn lengths() { } #[test] fn sequence() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); delta.retain(5, AttributeHashMap::default()); delta.retain(0, AttributeHashMap::default()); delta.insert("appflowy", AttributeHashMap::default()); @@ -348,7 +348,7 @@ fn apply_test() { #[test] fn base_len_test() { - let mut delta_a = TextOperations::default(); + let mut delta_a = DeltaTextOperations::default(); delta_a.insert("a", AttributeHashMap::default()); delta_a.insert("b", AttributeHashMap::default()); delta_a.insert("c", AttributeHashMap::default()); @@ -387,7 +387,7 @@ fn invert_test() { #[test] fn empty_ops() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); delta.retain(0, AttributeHashMap::default()); delta.insert("", AttributeHashMap::default()); delta.delete(0); @@ -395,12 +395,12 @@ fn empty_ops() { } #[test] fn eq() { - let mut delta_a = TextOperations::default(); + let mut delta_a = DeltaTextOperations::default(); delta_a.delete(1); delta_a.insert("lo", AttributeHashMap::default()); delta_a.retain(2, AttributeHashMap::default()); delta_a.retain(3, AttributeHashMap::default()); - let mut delta_b = TextOperations::default(); + let mut delta_b = DeltaTextOperations::default(); delta_b.delete(1); delta_b.insert("l", AttributeHashMap::default()); delta_b.insert("o", AttributeHashMap::default()); @@ -412,7 +412,7 @@ fn eq() { } #[test] fn ops_merging() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); assert_eq!(delta.ops.len(), 0); delta.retain(2, AttributeHashMap::default()); assert_eq!(delta.ops.len(), 1); @@ -436,7 +436,7 @@ fn ops_merging() { #[test] fn is_noop() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); assert!(delta.is_noop()); delta.retain(5, AttributeHashMap::default()); assert!(delta.is_noop()); @@ -484,13 +484,13 @@ fn transform_random_delta() { #[test] fn transform_with_two_delta() { - let mut a = TextOperations::default(); + let mut a = DeltaTextOperations::default(); let mut a_s = String::new(); a.insert("123", AttributeBuilder::new().insert("bold", true).build()); a_s = a.apply(&a_s).unwrap(); assert_eq!(&a_s, "123"); - let mut b = TextOperations::default(); + let mut b = DeltaTextOperations::default(); let mut b_s = String::new(); b.insert("456", AttributeHashMap::default()); b_s = b.apply(&b_s).unwrap(); @@ -580,10 +580,10 @@ fn transform_two_conflict_non_seq_delta() { #[test] fn delta_invert_no_attribute_delta() { - let mut delta = TextOperations::default(); + let mut delta = DeltaTextOperations::default(); delta.add(DeltaOperation::insert("123")); - let mut change = TextOperations::default(); + let mut change = DeltaTextOperations::default(); change.add(DeltaOperation::retain(3)); change.add(DeltaOperation::insert("456")); let undo = change.invert(&delta); diff --git a/frontend/rust-lib/flowy-document/tests/editor/serde_test.rs b/frontend/rust-lib/flowy-document/tests/editor/serde_test.rs index f41f94294c..4e34d8ca91 100644 --- a/frontend/rust-lib/flowy-document/tests/editor/serde_test.rs +++ b/frontend/rust-lib/flowy-document/tests/editor/serde_test.rs @@ -1,8 +1,8 @@ use flowy_sync::client_document::{ClientDocument, EmptyDocument}; -use lib_ot::text_delta::TextOperation; +use lib_ot::text_delta::DeltaTextOperation; use lib_ot::{ core::*, - text_delta::{BuildInTextAttribute, TextOperations}, + text_delta::{BuildInTextAttribute, DeltaTextOperations}, }; #[test] @@ -15,7 +15,7 @@ fn operation_insert_serialize_test() { let json = serde_json::to_string(&operation).unwrap(); eprintln!("{}", json); - let insert_op: TextOperation = serde_json::from_str(&json).unwrap(); + let insert_op: DeltaTextOperation = serde_json::from_str(&json).unwrap(); assert_eq!(insert_op, operation); } @@ -24,15 +24,15 @@ fn operation_retain_serialize_test() { let operation = DeltaOperation::Retain(12.into()); let json = serde_json::to_string(&operation).unwrap(); eprintln!("{}", json); - let insert_op: TextOperation = serde_json::from_str(&json).unwrap(); + let insert_op: DeltaTextOperation = serde_json::from_str(&json).unwrap(); assert_eq!(insert_op, operation); } #[test] fn operation_delete_serialize_test() { - let operation = TextOperation::Delete(2); + let operation = DeltaTextOperation::Delete(2); let json = serde_json::to_string(&operation).unwrap(); - let insert_op: TextOperation = serde_json::from_str(&json).unwrap(); + let insert_op: DeltaTextOperation = serde_json::from_str(&json).unwrap(); assert_eq!(insert_op, operation); } @@ -77,7 +77,7 @@ fn delta_deserialize_test() { {"retain":2,"attributes":{"italic":true,"bold":true}}, {"retain":2,"attributes":{"italic":true,"bold":true}} ]"#; - let delta = TextOperations::from_json(json).unwrap(); + let delta = DeltaTextOperations::from_json(json).unwrap(); eprintln!("{}", delta); } @@ -86,12 +86,12 @@ fn delta_deserialize_null_test() { let json = r#"[ {"retain":7,"attributes":{"bold":null}} ]"#; - let delta1 = TextOperations::from_json(json).unwrap(); + let delta1 = DeltaTextOperations::from_json(json).unwrap(); let mut attribute = BuildInTextAttribute::Bold(true); attribute.remove_value(); - let delta2 = OperationBuilder::new() + let delta2 = DeltaOperationBuilder::new() .retain_with_attributes(7, attribute.into()) .build(); diff --git a/frontend/rust-lib/flowy-document/tests/new_document/document_compose_test.rs b/frontend/rust-lib/flowy-document/tests/new_document/document_compose_test.rs new file mode 100644 index 0000000000..4480623fbf --- /dev/null +++ b/frontend/rust-lib/flowy-document/tests/new_document/document_compose_test.rs @@ -0,0 +1,24 @@ +use crate::new_document::script::DocumentEditorTest; +use crate::new_document::script::EditScript::*; + +#[tokio::test] +async fn document_insert_h1_style_test() { + let scripts = vec![ + ComposeTransactionStr { + transaction: r#"{"operations":[{"op":"update_text","path":[0,0],"delta":[{"insert":"/"}],"inverted":[{"delete":1}]}],"after_selection":{"start":{"path":[0,0],"offset":1},"end":{"path":[0,0],"offset":1}},"before_selection":{"start":{"path":[0,0],"offset":0},"end":{"path":[0,0],"offset":0}}}"#, + }, + AssertContent { + expected: r#"{"document":{"type":"editor","children":[{"type":"text","delta":[{"insert":"/"}]}]}}"#, + }, + ComposeTransactionStr { + transaction: r#"{"operations":[{"op":"update_text","path":[0,0],"delta":[{"delete":1}],"inverted":[{"insert":"/"}]}],"after_selection":{"start":{"path":[0,0],"offset":0},"end":{"path":[0,0],"offset":0}},"before_selection":{"start":{"path":[0,0],"offset":1},"end":{"path":[0,0],"offset":1}}}"#, + }, + ComposeTransactionStr { + transaction: r#"{"operations":[{"op":"update","path":[0,0],"attributes":{"subtype":"heading","heading":"h1"},"oldAttributes":{"subtype":null,"heading":null}}],"after_selection":{"start":{"path":[0,0],"offset":0},"end":{"path":[0,0],"offset":0}},"before_selection":{"start":{"path":[0,0],"offset":0},"end":{"path":[0,0],"offset":0}}}"#, + }, + AssertContent { + expected: r#"{"document":{"type":"editor","children":[{"type":"text","attributes":{"subtype":"heading","heading":"h1"}}]}}"#, + }, + ]; + DocumentEditorTest::new().await.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-document/tests/new_document/mod.rs b/frontend/rust-lib/flowy-document/tests/new_document/mod.rs index 63d424afaf..f4464c9147 100644 --- a/frontend/rust-lib/flowy-document/tests/new_document/mod.rs +++ b/frontend/rust-lib/flowy-document/tests/new_document/mod.rs @@ -1,2 +1,3 @@ +mod document_compose_test; mod script; mod test; diff --git a/frontend/rust-lib/flowy-document/tests/new_document/script.rs b/frontend/rust-lib/flowy-document/tests/new_document/script.rs index d822905394..8694915f85 100644 --- a/frontend/rust-lib/flowy-document/tests/new_document/script.rs +++ b/frontend/rust-lib/flowy-document/tests/new_document/script.rs @@ -1,17 +1,37 @@ -use flowy_document::editor::AppFlowyDocumentEditor; +use flowy_document::editor::{AppFlowyDocumentEditor, Document, DocumentTransaction}; +use flowy_document::entities::DocumentVersionPB; use flowy_test::helper::ViewTest; use flowy_test::FlowySDKTest; use lib_ot::core::{Body, Changeset, NodeDataBuilder, NodeOperation, Path, Transaction}; -use lib_ot::text_delta::TextOperations; +use lib_ot::text_delta::DeltaTextOperations; use std::sync::Arc; pub enum EditScript { - InsertText { path: Path, delta: TextOperations }, - UpdateText { path: Path, delta: TextOperations }, - Delete { path: Path }, - AssertContent { expected: &'static str }, - AssertPrettyContent { expected: &'static str }, + InsertText { + path: Path, + delta: DeltaTextOperations, + }, + UpdateText { + path: Path, + delta: DeltaTextOperations, + }, + #[allow(dead_code)] + ComposeTransaction { + transaction: Transaction, + }, + ComposeTransactionStr { + transaction: &'static str, + }, + Delete { + path: Path, + }, + AssertContent { + expected: &'static str, + }, + AssertPrettyContent { + expected: &'static str, + }, } pub struct DocumentEditorTest { @@ -21,7 +41,8 @@ pub struct DocumentEditorTest { impl DocumentEditorTest { pub async fn new() -> Self { - let sdk = FlowySDKTest::new(true); + let version = DocumentVersionPB::V1; + let sdk = FlowySDKTest::new(version.clone()); let _ = sdk.init_user().await; let test = ViewTest::new_document_view(&sdk).await; @@ -62,6 +83,14 @@ impl DocumentEditorTest { .await .unwrap(); } + EditScript::ComposeTransaction { transaction } => { + self.editor.apply_transaction(transaction).await.unwrap(); + } + EditScript::ComposeTransactionStr { transaction } => { + let document_transaction = serde_json::from_str::(transaction).unwrap(); + let transaction: Transaction = document_transaction.into(); + self.editor.apply_transaction(transaction).await.unwrap(); + } EditScript::Delete { path } => { let operation = NodeOperation::Delete { path, nodes: vec![] }; self.editor @@ -72,6 +101,9 @@ impl DocumentEditorTest { EditScript::AssertContent { expected } => { // let content = self.editor.get_content(false).await.unwrap(); + let expected_document: Document = serde_json::from_str(expected).unwrap(); + let expected = serde_json::to_string(&expected_document).unwrap(); + assert_eq!(content, expected); } EditScript::AssertPrettyContent { expected } => { diff --git a/frontend/rust-lib/flowy-document/tests/new_document/test.rs b/frontend/rust-lib/flowy-document/tests/new_document/test.rs index 6c5c344167..c54c676a8f 100644 --- a/frontend/rust-lib/flowy-document/tests/new_document/test.rs +++ b/frontend/rust-lib/flowy-document/tests/new_document/test.rs @@ -1,7 +1,7 @@ use crate::new_document::script::DocumentEditorTest; use crate::new_document::script::EditScript::*; -use lib_ot::text_delta::TextOperationBuilder; +use lib_ot::text_delta::DeltaTextOperationBuilder; #[tokio::test] async fn document_initialize_test() { @@ -13,7 +13,7 @@ async fn document_initialize_test() { #[tokio::test] async fn document_insert_text_test() { - let delta = TextOperationBuilder::new().insert("Hello world").build(); + let delta = DeltaTextOperationBuilder::new().insert("Hello world").build(); let expected = r#"{ "document": { "type": "editor", @@ -49,7 +49,7 @@ async fn document_update_text_test() { let scripts = vec![ UpdateText { path: vec![0, 0].into(), - delta: TextOperationBuilder::new().insert(&hello_world).build(), + delta: DeltaTextOperationBuilder::new().insert(&hello_world).build(), }, AssertPrettyContent { expected: r#"{ @@ -75,7 +75,7 @@ async fn document_update_text_test() { let scripts = vec![ UpdateText { path: vec![0, 0].into(), - delta: TextOperationBuilder::new() + delta: DeltaTextOperationBuilder::new() .retain(hello_world.len()) .insert(", AppFlowy") .build(), @@ -122,11 +122,11 @@ async fn document_delete_text_test() { let scripts = vec![ UpdateText { path: vec![0, 0].into(), - delta: TextOperationBuilder::new().insert(&hello_world).build(), + delta: DeltaTextOperationBuilder::new().insert(&hello_world).build(), }, UpdateText { path: vec![0, 0].into(), - delta: TextOperationBuilder::new().retain(5).delete(6).build(), + delta: DeltaTextOperationBuilder::new().retain(5).delete(6).build(), }, AssertPrettyContent { expected }, ]; @@ -139,7 +139,7 @@ async fn document_delete_node_test() { let scripts = vec![ UpdateText { path: vec![0, 0].into(), - delta: TextOperationBuilder::new().insert("Hello world").build(), + delta: DeltaTextOperationBuilder::new().insert("Hello world").build(), }, AssertContent { expected: r#"{"document":{"type":"editor","children":[{"type":"text","delta":[{"insert":"Hello world"}]}]}}"#, diff --git a/frontend/rust-lib/flowy-document/tests/old_document/script.rs b/frontend/rust-lib/flowy-document/tests/old_document/script.rs index 66d36ad3ba..cd8e8f79f4 100644 --- a/frontend/rust-lib/flowy-document/tests/old_document/script.rs +++ b/frontend/rust-lib/flowy-document/tests/old_document/script.rs @@ -2,7 +2,7 @@ use flowy_document::old_editor::editor::DeltaDocumentEditor; use flowy_document::TEXT_BLOCK_SYNC_INTERVAL_IN_MILLIS; use flowy_revision::disk::RevisionState; use flowy_test::{helper::ViewTest, FlowySDKTest}; -use lib_ot::{core::Interval, text_delta::TextOperations}; +use lib_ot::{core::Interval, text_delta::DeltaTextOperations}; use std::sync::Arc; use tokio::time::{sleep, Duration}; @@ -75,7 +75,7 @@ impl DeltaDocumentEditorTest { assert_eq!(next_revision.rev_id, rev_id.unwrap()); } EditorScript::AssertJson(expected) => { - let expected_delta: TextOperations = serde_json::from_str(expected).unwrap(); + let expected_delta: DeltaTextOperations = serde_json::from_str(expected).unwrap(); let delta = self.editor.document_operations().await.unwrap(); if expected_delta != delta { eprintln!("✅ expect: {}", expected,); diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 61e63c53cb..681c50ec70 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -7,7 +7,7 @@ use crate::{ impl_def_and_def_mut, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_folder_data_model::revision::{gen_view_id, ViewDataTypeRevision, ViewLayoutTypeRevision, ViewRevision}; +use flowy_folder_data_model::revision::{gen_view_id, ViewDataFormatRevision, ViewLayoutTypeRevision, ViewRevision}; use std::convert::TryInto; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] @@ -22,7 +22,7 @@ pub struct ViewPB { pub name: String, #[pb(index = 4)] - pub data_type: ViewDataTypePB, + pub data_format: ViewDataFormatPB, #[pb(index = 5)] pub modified_time: i64, @@ -40,7 +40,7 @@ impl std::convert::From for ViewPB { id: rev.id, app_id: rev.app_id, name: rev.name, - data_type: rev.data_type.into(), + data_format: rev.data_format.into(), modified_time: rev.modified_time, create_time: rev.create_time, layout: rev.layout.into(), @@ -49,31 +49,34 @@ impl std::convert::From for ViewPB { } #[derive(Eq, PartialEq, Hash, Debug, ProtoBuf_Enum, Clone)] -pub enum ViewDataTypePB { - Text = 0, - Database = 1, +pub enum ViewDataFormatPB { + DeltaFormat = 0, + DatabaseFormat = 1, + TreeFormat = 2, } -impl std::default::Default for ViewDataTypePB { +impl std::default::Default for ViewDataFormatPB { fn default() -> Self { - ViewDataTypeRevision::default().into() + ViewDataFormatRevision::default().into() } } -impl std::convert::From for ViewDataTypePB { - fn from(rev: ViewDataTypeRevision) -> Self { +impl std::convert::From for ViewDataFormatPB { + fn from(rev: ViewDataFormatRevision) -> Self { match rev { - ViewDataTypeRevision::Text => ViewDataTypePB::Text, - ViewDataTypeRevision::Database => ViewDataTypePB::Database, + ViewDataFormatRevision::DeltaFormat => ViewDataFormatPB::DeltaFormat, + ViewDataFormatRevision::DatabaseFormat => ViewDataFormatPB::DatabaseFormat, + ViewDataFormatRevision::TreeFormat => ViewDataFormatPB::TreeFormat, } } } -impl std::convert::From for ViewDataTypeRevision { - fn from(ty: ViewDataTypePB) -> Self { +impl std::convert::From for ViewDataFormatRevision { + fn from(ty: ViewDataFormatPB) -> Self { match ty { - ViewDataTypePB::Text => ViewDataTypeRevision::Text, - ViewDataTypePB::Database => ViewDataTypeRevision::Database, + ViewDataFormatPB::DeltaFormat => ViewDataFormatRevision::DeltaFormat, + ViewDataFormatPB::DatabaseFormat => ViewDataFormatRevision::DatabaseFormat, + ViewDataFormatPB::TreeFormat => ViewDataFormatRevision::TreeFormat, } } } @@ -146,7 +149,7 @@ pub struct CreateViewPayloadPB { pub thumbnail: Option, #[pb(index = 5)] - pub data_type: ViewDataTypePB, + pub data_format: ViewDataFormatPB, #[pb(index = 6)] pub layout: ViewLayoutTypePB, @@ -161,7 +164,7 @@ pub struct CreateViewParams { pub name: String, pub desc: String, pub thumbnail: String, - pub data_type: ViewDataTypePB, + pub data_format: ViewDataFormatPB, pub layout: ViewLayoutTypePB, pub view_id: String, pub view_content_data: Vec, @@ -183,7 +186,7 @@ impl TryInto for CreateViewPayloadPB { belong_to_id, name, desc: self.desc, - data_type: self.data_type, + data_format: self.data_format, layout: self.layout, thumbnail, view_id, diff --git a/frontend/rust-lib/flowy-folder/src/entities/view_info.rs b/frontend/rust-lib/flowy-folder/src/entities/view_info.rs index 42dbc42517..e4031d0ecb 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view_info.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view_info.rs @@ -1,4 +1,4 @@ -use crate::entities::{RepeatedViewPB, ViewDataTypePB}; +use crate::entities::{RepeatedViewPB, ViewDataFormatPB}; use flowy_derive::ProtoBuf; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] @@ -16,7 +16,7 @@ pub struct ViewInfoPB { pub desc: String, #[pb(index = 5)] - pub data_type: ViewDataTypePB, + pub data_type: ViewDataFormatPB, #[pb(index = 6)] pub belongings: RepeatedViewPB, diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index 71e436b848..8728820eca 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -126,7 +126,7 @@ pub enum FolderEvent { #[event(input = "RepeatedViewIdPB")] DeleteView = 204, - #[event(input = "ViewIdPB")] + #[event(input = "ViewPB")] DuplicateView = 205, #[event(input = "ViewIdPB")] diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 45b1cac098..2d561501c1 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -1,5 +1,5 @@ -use crate::entities::view::ViewDataTypePB; -use crate::entities::ViewLayoutTypePB; +use crate::entities::view::ViewDataFormatPB; +use crate::entities::{ViewLayoutTypePB, ViewPB}; use crate::services::folder_editor::FolderRevisionCompress; use crate::{ dart_notification::{send_dart_notification, FolderNotification}, @@ -14,13 +14,14 @@ use crate::{ use bytes::Bytes; use flowy_error::FlowyError; use flowy_folder_data_model::user_default; -use flowy_revision::disk::SQLiteDocumentRevisionPersistence; +use flowy_revision::disk::SQLiteDeltaDocumentRevisionPersistence; use flowy_revision::{RevisionManager, RevisionPersistence, RevisionWebSocket, SQLiteRevisionSnapshotPersistence}; -use flowy_sync::client_document::default::initial_read_me; +use flowy_document::editor::initial_read_me; use flowy_sync::{client_folder::FolderPad, entities::ws_data::ServerRevisionWSData}; use lazy_static::lazy_static; use lib_infra::future::FutureResult; + use std::{collections::HashMap, convert::TryInto, fmt::Formatter, sync::Arc}; use tokio::sync::RwLock as TokioRwLock; lazy_static! { @@ -64,7 +65,6 @@ pub struct FolderManager { pub(crate) trash_controller: Arc, web_socket: Arc, folder_editor: Arc>>>, - data_processors: ViewDataProcessorMap, } impl FolderManager { @@ -95,7 +95,7 @@ impl FolderManager { persistence.clone(), cloud_service.clone(), trash_controller.clone(), - data_processors.clone(), + data_processors, )); let app_controller = Arc::new(AppController::new( @@ -122,7 +122,6 @@ impl FolderManager { trash_controller, web_socket, folder_editor, - data_processors, } } @@ -151,6 +150,7 @@ impl FolderManager { } } + /// Called immediately after the application launched with the user sign in/sign up. #[tracing::instrument(level = "trace", skip(self), err)] pub async fn initialize(&self, user_id: &str, token: &str) -> FlowyResult<()> { let mut write_guard = INIT_FOLDER_FLAG.write().await; @@ -165,7 +165,7 @@ impl FolderManager { let pool = self.persistence.db_pool()?; let object_id = folder_id.as_ref(); - let disk_cache = SQLiteDocumentRevisionPersistence::new(user_id, pool.clone()); + let disk_cache = SQLiteDeltaDocumentRevisionPersistence::new(user_id, pool.clone()); let rev_persistence = RevisionPersistence::new(user_id, object_id, disk_cache); let rev_compactor = FolderRevisionCompress(); // let history_persistence = SQLiteRevisionHistoryPersistence::new(object_id, pool.clone()); @@ -184,17 +184,24 @@ impl FolderManager { let _ = self.app_controller.initialize()?; let _ = self.view_controller.initialize()?; - - self.data_processors.iter().for_each(|(_, processor)| { - processor.initialize(); - }); - write_guard.insert(user_id.to_owned(), true); Ok(()) } - pub async fn initialize_with_new_user(&self, user_id: &str, token: &str) -> FlowyResult<()> { - DefaultFolderBuilder::build(token, user_id, self.persistence.clone(), self.view_controller.clone()).await?; + pub async fn initialize_with_new_user( + &self, + user_id: &str, + token: &str, + view_data_format: ViewDataFormatPB, + ) -> FlowyResult<()> { + DefaultFolderBuilder::build( + token, + user_id, + self.persistence.clone(), + self.view_controller.clone(), + || (view_data_format.clone(), Bytes::from(initial_read_me())), + ) + .await?; self.initialize(user_id, token).await } @@ -205,23 +212,24 @@ impl FolderManager { struct DefaultFolderBuilder(); impl DefaultFolderBuilder { - async fn build( + async fn build (ViewDataFormatPB, Bytes)>( token: &str, user_id: &str, persistence: Arc, view_controller: Arc, + create_view_fn: F, ) -> FlowyResult<()> { log::debug!("Create user default workspace"); let workspace_rev = user_default::create_default_workspace(); set_current_workspace(&workspace_rev.id); for app in workspace_rev.apps.iter() { for (index, view) in app.belongings.iter().enumerate() { + let (view_data_type, view_data) = create_view_fn(); if index == 0 { - let view_data = initial_read_me().json_str(); let _ = view_controller.set_latest_view(&view.id); let layout_type = ViewLayoutTypePB::from(view.layout.clone()); let _ = view_controller - .create_view(&view.id, ViewDataTypePB::Text, layout_type, Bytes::from(view_data)) + .create_view(&view.id, view_data_type, layout_type, view_data) .await?; } } @@ -247,25 +255,24 @@ impl FolderManager { } pub trait ViewDataProcessor { - fn initialize(&self) -> FutureResult<(), FlowyError>; - - fn create_container( + fn create_view( &self, user_id: &str, view_id: &str, layout: ViewLayoutTypePB, - delta_data: Bytes, + view_data: Bytes, ) -> FutureResult<(), FlowyError>; - fn close_container(&self, view_id: &str) -> FutureResult<(), FlowyError>; + fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError>; - fn get_view_data(&self, view_id: &str) -> FutureResult; + fn get_view_data(&self, view: &ViewPB) -> FutureResult; fn create_default_view( &self, user_id: &str, view_id: &str, layout: ViewLayoutTypePB, + data_format: ViewDataFormatPB, ) -> FutureResult; fn create_view_from_delta_data( @@ -276,7 +283,7 @@ pub trait ViewDataProcessor { layout: ViewLayoutTypePB, ) -> FutureResult; - fn data_type(&self) -> ViewDataTypePB; + fn data_types(&self) -> Vec; } -pub type ViewDataProcessorMap = Arc>>; +pub type ViewDataProcessorMap = Arc>>; diff --git a/frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs b/frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs index 1b2c99d8cb..31844e8a28 100644 --- a/frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs +++ b/frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs @@ -7,12 +7,13 @@ use bytes::Bytes; use flowy_database::kv::KV; use flowy_error::{FlowyError, FlowyResult}; use flowy_folder_data_model::revision::{AppRevision, FolderRevision, ViewRevision, WorkspaceRevision}; -use flowy_revision::disk::SQLiteDocumentRevisionPersistence; +use flowy_revision::disk::SQLiteDeltaDocumentRevisionPersistence; use flowy_revision::reset::{RevisionResettable, RevisionStructReset}; use flowy_sync::client_folder::make_folder_rev_json_str; use flowy_sync::entities::revision::Revision; +use flowy_sync::server_folder::FolderOperationsBuilder; use flowy_sync::{client_folder::FolderPad, entities::revision::md5}; -use lib_ot::core::DeltaBuilder; + use std::sync::Arc; const V1_MIGRATION: &str = "FOLDER_V1_MIGRATION"; @@ -112,7 +113,7 @@ impl FolderMigration { }; let pool = self.database.db_pool()?; - let disk_cache = SQLiteDocumentRevisionPersistence::new(&self.user_id, pool); + let disk_cache = SQLiteDeltaDocumentRevisionPersistence::new(&self.user_id, pool); let reset = RevisionStructReset::new(&self.user_id, object, Arc::new(disk_cache)); reset.run().await } @@ -134,7 +135,7 @@ impl RevisionResettable for FolderRevisionResettable { fn reset_data(&self, revisions: Vec) -> FlowyResult { let pad = FolderPad::from_revisions(revisions)?; let json = pad.to_json()?; - let bytes = DeltaBuilder::new().insert(&json).build().json_bytes(); + let bytes = FolderOperationsBuilder::new().insert(&json).build().json_bytes(); Ok(bytes) } diff --git a/frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs b/frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs index 1e0c5e9b28..9ef1049369 100644 --- a/frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs +++ b/frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs @@ -13,7 +13,8 @@ use flowy_folder_data_model::revision::{AppRevision, TrashRevision, ViewRevision use flowy_revision::disk::{RevisionRecord, RevisionState}; use flowy_revision::mk_text_block_revision_disk_cache; use flowy_sync::{client_folder::FolderPad, entities::revision::Revision}; -use lib_ot::core::DeltaBuilder; + +use flowy_sync::server_folder::FolderOperationsBuilder; use std::sync::Arc; use tokio::sync::RwLock; pub use version_1::{app_sql::*, trash_sql::*, v1_impl::V1Transaction, view_sql::*, workspace_sql::*}; @@ -108,7 +109,7 @@ impl FolderPersistence { pub async fn save_folder(&self, user_id: &str, folder_id: &FolderId, folder: FolderPad) -> FlowyResult<()> { let pool = self.database.db_pool()?; let json = folder.to_json()?; - let delta_data = DeltaBuilder::new().insert(&json).build().json_bytes(); + let delta_data = FolderOperationsBuilder::new().insert(&json).build().json_bytes(); let revision = Revision::initial_revision(user_id, folder_id.as_ref(), delta_data); let record = RevisionRecord { revision, diff --git a/frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs b/frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs index c1b21cbe32..26b88651ef 100644 --- a/frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs +++ b/frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs @@ -13,7 +13,7 @@ use flowy_database::{ SqliteConnection, }; -use flowy_folder_data_model::revision::{ViewDataTypeRevision, ViewLayoutTypeRevision, ViewRevision}; +use flowy_folder_data_model::revision::{ViewDataFormatRevision, ViewLayoutTypeRevision, ViewRevision}; use lib_infra::util::timestamp; pub struct ViewTableSql(); @@ -78,7 +78,7 @@ pub(crate) struct ViewTable { pub modified_time: i64, pub create_time: i64, pub thumbnail: String, - pub view_type: SqlViewDataType, + pub view_type: SqlViewDataFormat, pub version: i64, pub is_trash: bool, pub ext_data: String, @@ -86,9 +86,10 @@ pub(crate) struct ViewTable { impl ViewTable { pub fn new(view_rev: ViewRevision) -> Self { - let data_type = match view_rev.data_type { - ViewDataTypeRevision::Text => SqlViewDataType::Block, - ViewDataTypeRevision::Database => SqlViewDataType::Grid, + let data_type = match view_rev.data_format { + ViewDataFormatRevision::DeltaFormat => SqlViewDataFormat::Delta, + ViewDataFormatRevision::DatabaseFormat => SqlViewDataFormat::Database, + ViewDataFormatRevision::TreeFormat => SqlViewDataFormat::Tree, }; ViewTable { @@ -110,8 +111,9 @@ impl ViewTable { impl std::convert::From for ViewRevision { fn from(table: ViewTable) -> Self { let data_type = match table.view_type { - SqlViewDataType::Block => ViewDataTypeRevision::Text, - SqlViewDataType::Grid => ViewDataTypeRevision::Database, + SqlViewDataFormat::Delta => ViewDataFormatRevision::DeltaFormat, + SqlViewDataFormat::Database => ViewDataFormatRevision::DatabaseFormat, + SqlViewDataFormat::Tree => ViewDataFormatRevision::TreeFormat, }; ViewRevision { @@ -119,7 +121,7 @@ impl std::convert::From for ViewRevision { app_id: table.belong_to_id, name: table.name, desc: table.desc, - data_type, + data_format: data_type, belongings: vec![], modified_time: table.modified_time, version: table.version, @@ -180,34 +182,36 @@ impl ViewChangeset { #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, FromSqlRow, AsExpression)] #[repr(i32)] #[sql_type = "Integer"] -pub enum SqlViewDataType { - Block = 0, - Grid = 1, +pub enum SqlViewDataFormat { + Delta = 0, + Database = 1, + Tree = 2, } -impl std::default::Default for SqlViewDataType { +impl std::default::Default for SqlViewDataFormat { fn default() -> Self { - SqlViewDataType::Block + SqlViewDataFormat::Delta } } -impl std::convert::From for SqlViewDataType { +impl std::convert::From for SqlViewDataFormat { fn from(value: i32) -> Self { match value { - 0 => SqlViewDataType::Block, - 1 => SqlViewDataType::Grid, + 0 => SqlViewDataFormat::Delta, + 1 => SqlViewDataFormat::Database, + 2 => SqlViewDataFormat::Tree, o => { log::error!("Unsupported view type {}, fallback to ViewType::Block", o); - SqlViewDataType::Block + SqlViewDataFormat::Delta } } } } -impl SqlViewDataType { +impl SqlViewDataFormat { pub fn value(&self) -> i32 { *self as i32 } } -impl_sql_integer_expression!(SqlViewDataType); +impl_sql_integer_expression!(SqlViewDataFormat); diff --git a/frontend/rust-lib/flowy-folder/src/services/view/controller.rs b/frontend/rust-lib/flowy-folder/src/services/view/controller.rs index b77a44623d..67eda75caf 100644 --- a/frontend/rust-lib/flowy-folder/src/services/view/controller.rs +++ b/frontend/rust-lib/flowy-folder/src/services/view/controller.rs @@ -1,4 +1,4 @@ -pub use crate::entities::view::ViewDataTypePB; +pub use crate::entities::view::ViewDataFormatPB; use crate::entities::{DeletedViewPB, ViewInfoPB, ViewLayoutTypePB}; use crate::manager::{ViewDataProcessor, ViewDataProcessorMap}; use crate::{ @@ -58,12 +58,17 @@ impl ViewController { &self, mut params: CreateViewParams, ) -> Result { - let processor = self.get_data_processor(params.data_type.clone())?; + let processor = self.get_data_processor(params.data_format.clone())?; let user_id = self.user.user_id()?; if params.view_content_data.is_empty() { tracing::trace!("Create view with build-in data"); let view_data = processor - .create_default_view(&user_id, ¶ms.view_id, params.layout.clone()) + .create_default_view( + &user_id, + ¶ms.view_id, + params.layout.clone(), + params.data_format.clone(), + ) .await?; params.view_content_data = view_data.to_vec(); } else { @@ -79,7 +84,7 @@ impl ViewController { let _ = self .create_view( ¶ms.view_id, - params.data_type.clone(), + params.data_format.clone(), params.layout.clone(), delta_data, ) @@ -91,22 +96,20 @@ impl ViewController { Ok(view_rev) } - #[tracing::instrument(level = "debug", skip(self, view_id, delta_data), err)] + #[tracing::instrument(level = "debug", skip(self, view_id, view_data), err)] pub(crate) async fn create_view( &self, view_id: &str, - data_type: ViewDataTypePB, + data_type: ViewDataFormatPB, layout_type: ViewLayoutTypePB, - delta_data: Bytes, + view_data: Bytes, ) -> Result<(), FlowyError> { - if delta_data.is_empty() { + if view_data.is_empty() { return Err(FlowyError::internal().context("The content of the view should not be empty")); } let user_id = self.user.user_id()?; let processor = self.get_data_processor(data_type)?; - let _ = processor - .create_container(&user_id, view_id, layout_type, delta_data) - .await?; + let _ = processor.create_view(&user_id, view_id, layout_type, view_data).await?; Ok(()) } @@ -156,7 +159,7 @@ impl ViewController { belong_to_id: view_rev.app_id, name: view_rev.name, desc: view_rev.desc, - data_type: view_rev.data_type.into(), + data_type: view_rev.data_format.into(), belongings: RepeatedViewPB { items }, ext_data: view_rev.ext_data, }; @@ -188,7 +191,7 @@ impl ViewController { #[tracing::instrument(level = "debug", skip(self), err)] pub(crate) async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> { let processor = self.get_data_processor_from_view_id(view_id).await?; - let _ = processor.close_container(view_id).await?; + let _ = processor.close_view(view_id).await?; Ok(()) } @@ -223,7 +226,7 @@ impl ViewController { .send(); let processor = self.get_data_processor_from_view_id(&view_id).await?; - let _ = processor.close_container(&view_id).await?; + let _ = processor.close_view(&view_id).await?; Ok(()) } @@ -242,20 +245,20 @@ impl ViewController { } #[tracing::instrument(level = "debug", skip(self), err)] - pub(crate) async fn duplicate_view(&self, view_id: &str) -> Result<(), FlowyError> { + pub(crate) async fn duplicate_view(&self, view: ViewPB) -> Result<(), FlowyError> { let view_rev = self .persistence - .begin_transaction(|transaction| transaction.read_view(view_id)) + .begin_transaction(|transaction| transaction.read_view(&view.id)) .await?; - let processor = self.get_data_processor(view_rev.data_type.clone())?; - let view_data = processor.get_view_data(view_id).await?; + let processor = self.get_data_processor(view_rev.data_format.clone())?; + let view_data = processor.get_view_data(&view).await?; let duplicate_params = CreateViewParams { belong_to_id: view_rev.app_id.clone(), name: format!("{} (copy)", &view_rev.name), desc: view_rev.desc, thumbnail: view_rev.thumbnail, - data_type: view_rev.data_type.into(), + data_format: view_rev.data_format.into(), layout: view_rev.layout.into(), view_content_data: view_data.to_vec(), view_id: gen_view_id(), @@ -399,11 +402,11 @@ impl ViewController { .persistence .begin_transaction(|transaction| transaction.read_view(view_id)) .await?; - self.get_data_processor(view.data_type) + self.get_data_processor(view.data_format) } #[inline] - fn get_data_processor>( + fn get_data_processor>( &self, data_type: T, ) -> FlowyResult> { @@ -472,10 +475,10 @@ async fn handle_trash_event( .await?; for view in views { - let data_type = view.data_type.clone().into(); + let data_type = view.data_format.clone().into(); match get_data_processor(data_processors.clone(), &data_type) { Ok(processor) => { - let _ = processor.close_container(&view.id).await?; + let _ = processor.close_view(&view.id).await?; } Err(e) => { tracing::error!("{}", e) @@ -491,7 +494,7 @@ async fn handle_trash_event( fn get_data_processor( data_processors: ViewDataProcessorMap, - data_type: &ViewDataTypePB, + data_type: &ViewDataFormatPB, ) -> FlowyResult> { match data_processors.get(data_type) { None => Err(FlowyError::internal().context(format!( diff --git a/frontend/rust-lib/flowy-folder/src/services/view/event_handler.rs b/frontend/rust-lib/flowy-folder/src/services/view/event_handler.rs index 2a7736e05e..04f83b8132 100644 --- a/frontend/rust-lib/flowy-folder/src/services/view/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/services/view/event_handler.rs @@ -121,10 +121,10 @@ pub(crate) async fn move_item_handler( #[tracing::instrument(level = "debug", skip(data, controller), err)] pub(crate) async fn duplicate_view_handler( - data: Data, + data: Data, controller: AppData>, ) -> Result<(), FlowyError> { - let view_id: ViewIdPB = data.into_inner(); - let _ = controller.duplicate_view(&view_id.value).await?; + let view: ViewPB = data.into_inner(); + let _ = controller.duplicate_view(view).await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs b/frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs index 33564c923d..25d2387996 100644 --- a/frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs +++ b/frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs @@ -1,5 +1,5 @@ use crate::script::{invalid_workspace_name_test_case, FolderScript::*, FolderTest}; -use flowy_folder::entities::view::ViewDataTypePB; +use flowy_folder::entities::view::ViewDataFormatPB; use flowy_folder::entities::workspace::CreateWorkspacePayloadPB; use flowy_revision::disk::RevisionState; use flowy_test::{event_builder::*, FlowySDKTest}; @@ -133,12 +133,12 @@ async fn app_create_with_view() { CreateView { name: "View A".to_owned(), desc: "View A description".to_owned(), - data_type: ViewDataTypePB::Text, + data_type: ViewDataFormatPB::DeltaFormat, }, CreateView { name: "Grid".to_owned(), desc: "Grid description".to_owned(), - data_type: ViewDataTypePB::Database, + data_type: ViewDataFormatPB::DatabaseFormat, }, ReadApp(app.id), ]) @@ -197,12 +197,12 @@ async fn view_delete_all() { CreateView { name: "View A".to_owned(), desc: "View A description".to_owned(), - data_type: ViewDataTypePB::Text, + data_type: ViewDataFormatPB::DeltaFormat, }, CreateView { name: "Grid".to_owned(), desc: "Grid description".to_owned(), - data_type: ViewDataTypePB::Database, + data_type: ViewDataFormatPB::DatabaseFormat, }, ReadApp(app.id.clone()), ]) @@ -230,7 +230,7 @@ async fn view_delete_all_permanent() { CreateView { name: "View A".to_owned(), desc: "View A description".to_owned(), - data_type: ViewDataTypePB::Text, + data_type: ViewDataFormatPB::DeltaFormat, }, ReadApp(app.id.clone()), ]) @@ -329,7 +329,7 @@ async fn folder_sync_revision_with_new_view() { CreateView { name: view_name.clone(), desc: view_desc.clone(), - data_type: ViewDataTypePB::Text, + data_type: ViewDataFormatPB::DeltaFormat, }, AssertCurrentRevId(3), AssertNextSyncRevId(Some(3)), diff --git a/frontend/rust-lib/flowy-folder/tests/workspace/script.rs b/frontend/rust-lib/flowy-folder/tests/workspace/script.rs index 005b685a94..5725258bf2 100644 --- a/frontend/rust-lib/flowy-folder/tests/workspace/script.rs +++ b/frontend/rust-lib/flowy-folder/tests/workspace/script.rs @@ -10,7 +10,7 @@ use flowy_folder::entities::{ use flowy_folder::entities::{ app::{AppPB, RepeatedAppPB}, trash::TrashPB, - view::{RepeatedViewPB, ViewDataTypePB, ViewPB}, + view::{RepeatedViewPB, ViewDataFormatPB, ViewPB}, workspace::WorkspacePB, }; use flowy_folder::event_map::FolderEvent::*; @@ -52,7 +52,7 @@ pub enum FolderScript { CreateView { name: String, desc: String, - data_type: ViewDataTypePB, + data_type: ViewDataFormatPB, }, AssertView(ViewPB), ReadView(String), @@ -99,7 +99,7 @@ impl FolderTest { &app.id, "Folder View", "Folder test view", - ViewDataTypePB::Text, + ViewDataFormatPB::DeltaFormat, ViewLayoutTypePB::Document, ) .await; @@ -182,8 +182,9 @@ impl FolderTest { FolderScript::CreateView { name, desc, data_type } => { let layout = match data_type { - ViewDataTypePB::Text => ViewLayoutTypePB::Document, - ViewDataTypePB::Database => ViewLayoutTypePB::Grid, + ViewDataFormatPB::DeltaFormat => ViewLayoutTypePB::Document, + ViewDataFormatPB::TreeFormat => ViewLayoutTypePB::Document, + ViewDataFormatPB::DatabaseFormat => ViewLayoutTypePB::Grid, }; let view = create_view(sdk, &self.app.id, &name, &desc, data_type, layout).await; self.view = view; @@ -357,7 +358,7 @@ pub async fn create_view( app_id: &str, name: &str, desc: &str, - data_type: ViewDataTypePB, + data_type: ViewDataFormatPB, layout: ViewLayoutTypePB, ) -> ViewPB { let request = CreateViewPayloadPB { @@ -365,7 +366,7 @@ pub async fn create_view( name: name.to_string(), desc: desc.to_string(), thumbnail: None, - data_type, + data_format: data_type, layout, view_content_data: vec![], }; diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index 71799a22c3..7b9421ba10 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -589,14 +589,14 @@ pub fn make_grid_setting(view_pad: &GridViewRevisionPad, field_revs: &[Arc) -> FlowyResult { let pad = GridRevisionPad::from_revisions(revisions)?; let json = pad.json_str()?; - let bytes = DeltaBuilder::new().insert(&json).build().json_bytes(); + let bytes = GridOperationsBuilder::new().insert(&json).build().json_bytes(); Ok(bytes) } diff --git a/frontend/rust-lib/flowy-net/src/local_server/server.rs b/frontend/rust-lib/flowy-net/src/local_server/server.rs index 8d92efac76..af69d9072b 100644 --- a/frontend/rust-lib/flowy-net/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-net/src/local_server/server.rs @@ -307,7 +307,7 @@ impl FolderCouldServiceV1 for LocalServer { app_id: params.belong_to_id, name: params.name, desc: params.desc, - data_type: params.data_type.into(), + data_format: params.data_format.into(), version: 0, belongings: vec![], modified_time: time, diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/delta_document_impl.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/delta_document_impl.rs new file mode 100644 index 0000000000..9a3b4c69af --- /dev/null +++ b/frontend/rust-lib/flowy-revision/src/cache/disk/delta_document_impl.rs @@ -0,0 +1,305 @@ +use crate::cache::disk::RevisionDiskCache; +use crate::disk::{RevisionChangeset, RevisionRecord}; +use bytes::Bytes; +use diesel::{sql_types::Integer, update, SqliteConnection}; +use flowy_database::{ + impl_sql_integer_expression, insert_or_ignore_into, + prelude::*, + schema::{rev_table, rev_table::dsl}, + ConnectionPool, +}; +use flowy_error::{internal_error, FlowyError, FlowyResult}; +use flowy_sync::{ + entities::revision::{RevType, Revision, RevisionRange}, + util::md5, +}; +use std::collections::HashMap; +use std::sync::Arc; + +pub struct SQLiteDeltaDocumentRevisionPersistence { + user_id: String, + pub(crate) pool: Arc, +} + +impl RevisionDiskCache for SQLiteDeltaDocumentRevisionPersistence { + type Error = FlowyError; + + fn create_revision_records(&self, revision_records: Vec) -> Result<(), Self::Error> { + let conn = self.pool.get().map_err(internal_error)?; + let _ = DeltaRevisionSql::create(revision_records, &*conn)?; + Ok(()) + } + + fn read_revision_records( + &self, + object_id: &str, + rev_ids: Option>, + ) -> Result, Self::Error> { + let conn = self.pool.get().map_err(internal_error)?; + let records = DeltaRevisionSql::read(&self.user_id, object_id, rev_ids, &*conn)?; + Ok(records) + } + + fn read_revision_records_with_range( + &self, + object_id: &str, + range: &RevisionRange, + ) -> Result, Self::Error> { + let conn = &*self.pool.get().map_err(internal_error)?; + let revisions = DeltaRevisionSql::read_with_range(&self.user_id, object_id, range.clone(), conn)?; + Ok(revisions) + } + + fn update_revision_record(&self, changesets: Vec) -> FlowyResult<()> { + let conn = &*self.pool.get().map_err(internal_error)?; + let _ = conn.immediate_transaction::<_, FlowyError, _>(|| { + for changeset in changesets { + let _ = DeltaRevisionSql::update(changeset, conn)?; + } + Ok(()) + })?; + Ok(()) + } + + fn delete_revision_records(&self, object_id: &str, rev_ids: Option>) -> Result<(), Self::Error> { + let conn = &*self.pool.get().map_err(internal_error)?; + let _ = DeltaRevisionSql::delete(object_id, rev_ids, conn)?; + Ok(()) + } + + fn delete_and_insert_records( + &self, + object_id: &str, + deleted_rev_ids: Option>, + inserted_records: Vec, + ) -> Result<(), Self::Error> { + let conn = self.pool.get().map_err(internal_error)?; + conn.immediate_transaction::<_, FlowyError, _>(|| { + let _ = DeltaRevisionSql::delete(object_id, deleted_rev_ids, &*conn)?; + let _ = DeltaRevisionSql::create(inserted_records, &*conn)?; + Ok(()) + }) + } +} + +impl SQLiteDeltaDocumentRevisionPersistence { + pub fn new(user_id: &str, pool: Arc) -> Self { + Self { + user_id: user_id.to_owned(), + pool, + } + } +} + +pub struct DeltaRevisionSql {} + +impl DeltaRevisionSql { + fn create(revision_records: Vec, conn: &SqliteConnection) -> Result<(), FlowyError> { + // Batch insert: https://diesel.rs/guides/all-about-inserts.html + + let records = revision_records + .into_iter() + .map(|record| { + tracing::trace!( + "[TextRevisionSql] create revision: {}:{:?}", + record.revision.object_id, + record.revision.rev_id + ); + let rev_state: TextRevisionState = record.state.into(); + ( + dsl::doc_id.eq(record.revision.object_id), + dsl::base_rev_id.eq(record.revision.base_rev_id), + dsl::rev_id.eq(record.revision.rev_id), + dsl::data.eq(record.revision.bytes), + dsl::state.eq(rev_state), + dsl::ty.eq(RevTableType::Local), + ) + }) + .collect::>(); + + let _ = insert_or_ignore_into(dsl::rev_table).values(&records).execute(conn)?; + Ok(()) + } + + fn update(changeset: RevisionChangeset, conn: &SqliteConnection) -> Result<(), FlowyError> { + let state: TextRevisionState = changeset.state.clone().into(); + let filter = dsl::rev_table + .filter(dsl::rev_id.eq(changeset.rev_id.as_ref())) + .filter(dsl::doc_id.eq(changeset.object_id)); + let _ = update(filter).set(dsl::state.eq(state)).execute(conn)?; + tracing::debug!( + "[TextRevisionSql] update revision:{} state:to {:?}", + changeset.rev_id, + changeset.state + ); + Ok(()) + } + + fn read( + user_id: &str, + object_id: &str, + rev_ids: Option>, + conn: &SqliteConnection, + ) -> Result, FlowyError> { + let mut sql = dsl::rev_table.filter(dsl::doc_id.eq(object_id)).into_boxed(); + if let Some(rev_ids) = rev_ids { + sql = sql.filter(dsl::rev_id.eq_any(rev_ids)); + } + let rows = sql.order(dsl::rev_id.asc()).load::(conn)?; + let records = rows + .into_iter() + .map(|row| mk_revision_record_from_table(user_id, row)) + .collect::>(); + + Ok(records) + } + + fn read_with_range( + user_id: &str, + object_id: &str, + range: RevisionRange, + conn: &SqliteConnection, + ) -> Result, FlowyError> { + let rev_tables = dsl::rev_table + .filter(dsl::rev_id.ge(range.start)) + .filter(dsl::rev_id.le(range.end)) + .filter(dsl::doc_id.eq(object_id)) + .order(dsl::rev_id.asc()) + .load::(conn)?; + + let revisions = rev_tables + .into_iter() + .map(|table| mk_revision_record_from_table(user_id, table)) + .collect::>(); + Ok(revisions) + } + + fn delete(object_id: &str, rev_ids: Option>, conn: &SqliteConnection) -> Result<(), FlowyError> { + let mut sql = diesel::delete(dsl::rev_table).into_boxed(); + sql = sql.filter(dsl::doc_id.eq(object_id)); + + if let Some(rev_ids) = rev_ids { + tracing::trace!("[TextRevisionSql] Delete revision: {}:{:?}", object_id, rev_ids); + sql = sql.filter(dsl::rev_id.eq_any(rev_ids)); + } + + let affected_row = sql.execute(conn)?; + tracing::trace!("[TextRevisionSql] Delete {} rows", affected_row); + Ok(()) + } + + pub fn read_all_documents(user_id: &str, conn: &SqliteConnection) -> Result>, FlowyError> { + let rev_tables = dsl::rev_table.order(dsl::rev_id.asc()).load::(conn)?; + let mut document_map = HashMap::new(); + for rev_table in rev_tables { + document_map + .entry(rev_table.doc_id.clone()) + .or_insert_with(Vec::new) + .push(rev_table); + } + let mut documents = vec![]; + for rev_tables in document_map.into_values() { + let revisions = rev_tables + .into_iter() + .map(|table| { + let record = mk_revision_record_from_table(user_id, table); + record.revision + }) + .collect::>(); + documents.push(revisions); + } + + Ok(documents) + } +} + +#[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable, Associations)] +#[table_name = "rev_table"] +struct RevisionTable { + id: i32, + doc_id: String, + base_rev_id: i64, + rev_id: i64, + data: Vec, + state: TextRevisionState, + ty: RevTableType, // Deprecated +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, FromSqlRow, AsExpression)] +#[repr(i32)] +#[sql_type = "Integer"] +enum TextRevisionState { + Sync = 0, + Ack = 1, +} +impl_sql_integer_expression!(TextRevisionState); +impl_rev_state_map!(TextRevisionState); + +impl std::default::Default for TextRevisionState { + fn default() -> Self { + TextRevisionState::Sync + } +} + +fn mk_revision_record_from_table(user_id: &str, table: RevisionTable) -> RevisionRecord { + let md5 = md5(&table.data); + let revision = Revision::new( + &table.doc_id, + table.base_rev_id, + table.rev_id, + Bytes::from(table.data), + user_id, + md5, + ); + RevisionRecord { + revision, + state: table.state.into(), + write_to_disk: false, + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, FromSqlRow, AsExpression)] +#[repr(i32)] +#[sql_type = "Integer"] +pub enum RevTableType { + Local = 0, + Remote = 1, +} +impl_sql_integer_expression!(RevTableType); + +impl std::default::Default for RevTableType { + fn default() -> Self { + RevTableType::Local + } +} + +impl std::convert::From for RevTableType { + fn from(value: i32) -> Self { + match value { + 0 => RevTableType::Local, + 1 => RevTableType::Remote, + o => { + tracing::error!("Unsupported rev type {}, fallback to RevTableType::Local", o); + RevTableType::Local + } + } + } +} + +impl std::convert::From for RevTableType { + fn from(ty: RevType) -> Self { + match ty { + RevType::DeprecatedLocal => RevTableType::Local, + RevType::DeprecatedRemote => RevTableType::Remote, + } + } +} + +impl std::convert::From for RevType { + fn from(ty: RevTableType) -> Self { + match ty { + RevTableType::Local => RevType::DeprecatedLocal, + RevTableType::Remote => RevType::DeprecatedRemote, + } + } +} diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/document_impl.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/document_impl.rs index 5866df3523..9d3dc60dae 100644 --- a/frontend/rust-lib/flowy-revision/src/cache/disk/document_impl.rs +++ b/frontend/rust-lib/flowy-revision/src/cache/disk/document_impl.rs @@ -5,12 +5,12 @@ use diesel::{sql_types::Integer, update, SqliteConnection}; use flowy_database::{ impl_sql_integer_expression, insert_or_ignore_into, prelude::*, - schema::{rev_table, rev_table::dsl}, + schema::{document_rev_table, document_rev_table::dsl}, ConnectionPool, }; use flowy_error::{internal_error, FlowyError, FlowyResult}; use flowy_sync::{ - entities::revision::{RevType, Revision, RevisionRange}, + entities::revision::{Revision, RevisionRange}, util::md5, }; use std::sync::Arc; @@ -25,7 +25,7 @@ impl RevisionDiskCache for SQLiteDocumentRevisionPersistence { fn create_revision_records(&self, revision_records: Vec) -> Result<(), Self::Error> { let conn = self.pool.get().map_err(internal_error)?; - let _ = TextRevisionSql::create(revision_records, &*conn)?; + let _ = DocumentRevisionSql::create(revision_records, &*conn)?; Ok(()) } @@ -35,7 +35,7 @@ impl RevisionDiskCache for SQLiteDocumentRevisionPersistence { rev_ids: Option>, ) -> Result, Self::Error> { let conn = self.pool.get().map_err(internal_error)?; - let records = TextRevisionSql::read(&self.user_id, object_id, rev_ids, &*conn)?; + let records = DocumentRevisionSql::read(&self.user_id, object_id, rev_ids, &*conn)?; Ok(records) } @@ -45,7 +45,7 @@ impl RevisionDiskCache for SQLiteDocumentRevisionPersistence { range: &RevisionRange, ) -> Result, Self::Error> { let conn = &*self.pool.get().map_err(internal_error)?; - let revisions = TextRevisionSql::read_with_range(&self.user_id, object_id, range.clone(), conn)?; + let revisions = DocumentRevisionSql::read_with_range(&self.user_id, object_id, range.clone(), conn)?; Ok(revisions) } @@ -53,7 +53,7 @@ impl RevisionDiskCache for SQLiteDocumentRevisionPersistence { let conn = &*self.pool.get().map_err(internal_error)?; let _ = conn.immediate_transaction::<_, FlowyError, _>(|| { for changeset in changesets { - let _ = TextRevisionSql::update(changeset, conn)?; + let _ = DocumentRevisionSql::update(changeset, conn)?; } Ok(()) })?; @@ -62,7 +62,7 @@ impl RevisionDiskCache for SQLiteDocumentRevisionPersistence { fn delete_revision_records(&self, object_id: &str, rev_ids: Option>) -> Result<(), Self::Error> { let conn = &*self.pool.get().map_err(internal_error)?; - let _ = TextRevisionSql::delete(object_id, rev_ids, conn)?; + let _ = DocumentRevisionSql::delete(object_id, rev_ids, conn)?; Ok(()) } @@ -74,8 +74,8 @@ impl RevisionDiskCache for SQLiteDocumentRevisionPersistence { ) -> Result<(), Self::Error> { let conn = self.pool.get().map_err(internal_error)?; conn.immediate_transaction::<_, FlowyError, _>(|| { - let _ = TextRevisionSql::delete(object_id, deleted_rev_ids, &*conn)?; - let _ = TextRevisionSql::create(inserted_records, &*conn)?; + let _ = DocumentRevisionSql::delete(object_id, deleted_rev_ids, &*conn)?; + let _ = DocumentRevisionSql::create(inserted_records, &*conn)?; Ok(()) }) } @@ -90,44 +90,44 @@ impl SQLiteDocumentRevisionPersistence { } } -struct TextRevisionSql {} +struct DocumentRevisionSql {} -impl TextRevisionSql { +impl DocumentRevisionSql { fn create(revision_records: Vec, conn: &SqliteConnection) -> Result<(), FlowyError> { // Batch insert: https://diesel.rs/guides/all-about-inserts.html - let records = revision_records .into_iter() .map(|record| { tracing::trace!( - "[TextRevisionSql] create revision: {}:{:?}", + "[DocumentRevisionSql] create revision: {}:{:?}", record.revision.object_id, record.revision.rev_id ); - let rev_state: TextRevisionState = record.state.into(); + let rev_state: RevisionState = record.state.into(); ( - dsl::doc_id.eq(record.revision.object_id), + dsl::document_id.eq(record.revision.object_id), dsl::base_rev_id.eq(record.revision.base_rev_id), dsl::rev_id.eq(record.revision.rev_id), dsl::data.eq(record.revision.bytes), dsl::state.eq(rev_state), - dsl::ty.eq(RevTableType::Local), ) }) .collect::>(); - let _ = insert_or_ignore_into(dsl::rev_table).values(&records).execute(conn)?; + let _ = insert_or_ignore_into(dsl::document_rev_table) + .values(&records) + .execute(conn)?; Ok(()) } fn update(changeset: RevisionChangeset, conn: &SqliteConnection) -> Result<(), FlowyError> { - let state: TextRevisionState = changeset.state.clone().into(); - let filter = dsl::rev_table + let state: RevisionState = changeset.state.clone().into(); + let filter = dsl::document_rev_table .filter(dsl::rev_id.eq(changeset.rev_id.as_ref())) - .filter(dsl::doc_id.eq(changeset.object_id)); + .filter(dsl::document_id.eq(changeset.object_id)); let _ = update(filter).set(dsl::state.eq(state)).execute(conn)?; tracing::debug!( - "[TextRevisionSql] update revision:{} state:to {:?}", + "[DocumentRevisionSql] update revision:{} state:to {:?}", changeset.rev_id, changeset.state ); @@ -140,11 +140,13 @@ impl TextRevisionSql { rev_ids: Option>, conn: &SqliteConnection, ) -> Result, FlowyError> { - let mut sql = dsl::rev_table.filter(dsl::doc_id.eq(object_id)).into_boxed(); + let mut sql = dsl::document_rev_table + .filter(dsl::document_id.eq(object_id)) + .into_boxed(); if let Some(rev_ids) = rev_ids { sql = sql.filter(dsl::rev_id.eq_any(rev_ids)); } - let rows = sql.order(dsl::rev_id.asc()).load::(conn)?; + let rows = sql.order(dsl::rev_id.asc()).load::(conn)?; let records = rows .into_iter() .map(|row| mk_revision_record_from_table(user_id, row)) @@ -159,12 +161,12 @@ impl TextRevisionSql { range: RevisionRange, conn: &SqliteConnection, ) -> Result, FlowyError> { - let rev_tables = dsl::rev_table + let rev_tables = dsl::document_rev_table .filter(dsl::rev_id.ge(range.start)) .filter(dsl::rev_id.le(range.end)) - .filter(dsl::doc_id.eq(object_id)) + .filter(dsl::document_id.eq(object_id)) .order(dsl::rev_id.asc()) - .load::(conn)?; + .load::(conn)?; let revisions = rev_tables .into_iter() @@ -174,52 +176,51 @@ impl TextRevisionSql { } fn delete(object_id: &str, rev_ids: Option>, conn: &SqliteConnection) -> Result<(), FlowyError> { - let mut sql = diesel::delete(dsl::rev_table).into_boxed(); - sql = sql.filter(dsl::doc_id.eq(object_id)); + let mut sql = diesel::delete(dsl::document_rev_table).into_boxed(); + sql = sql.filter(dsl::document_id.eq(object_id)); if let Some(rev_ids) = rev_ids { - tracing::trace!("[TextRevisionSql] Delete revision: {}:{:?}", object_id, rev_ids); + tracing::trace!("[DocumentRevisionSql] Delete revision: {}:{:?}", object_id, rev_ids); sql = sql.filter(dsl::rev_id.eq_any(rev_ids)); } let affected_row = sql.execute(conn)?; - tracing::trace!("[TextRevisionSql] Delete {} rows", affected_row); + tracing::trace!("[DocumentRevisionSql] Delete {} rows", affected_row); Ok(()) } } #[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable, Associations)] -#[table_name = "rev_table"] -struct RevisionTable { +#[table_name = "document_rev_table"] +struct DocumentRevisionTable { id: i32, - doc_id: String, + document_id: String, base_rev_id: i64, rev_id: i64, data: Vec, - state: TextRevisionState, - ty: RevTableType, // Deprecated + state: RevisionState, } #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, FromSqlRow, AsExpression)] #[repr(i32)] #[sql_type = "Integer"] -enum TextRevisionState { +enum RevisionState { Sync = 0, Ack = 1, } -impl_sql_integer_expression!(TextRevisionState); -impl_rev_state_map!(TextRevisionState); +impl_sql_integer_expression!(RevisionState); +impl_rev_state_map!(RevisionState); -impl std::default::Default for TextRevisionState { +impl std::default::Default for RevisionState { fn default() -> Self { - TextRevisionState::Sync + RevisionState::Sync } } -fn mk_revision_record_from_table(user_id: &str, table: RevisionTable) -> RevisionRecord { +fn mk_revision_record_from_table(user_id: &str, table: DocumentRevisionTable) -> RevisionRecord { let md5 = md5(&table.data); let revision = Revision::new( - &table.doc_id, + &table.document_id, table.base_rev_id, table.rev_id, Bytes::from(table.data), @@ -232,49 +233,3 @@ fn mk_revision_record_from_table(user_id: &str, table: RevisionTable) -> Revisio write_to_disk: false, } } - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, FromSqlRow, AsExpression)] -#[repr(i32)] -#[sql_type = "Integer"] -pub enum RevTableType { - Local = 0, - Remote = 1, -} -impl_sql_integer_expression!(RevTableType); - -impl std::default::Default for RevTableType { - fn default() -> Self { - RevTableType::Local - } -} - -impl std::convert::From for RevTableType { - fn from(value: i32) -> Self { - match value { - 0 => RevTableType::Local, - 1 => RevTableType::Remote, - o => { - tracing::error!("Unsupported rev type {}, fallback to RevTableType::Local", o); - RevTableType::Local - } - } - } -} - -impl std::convert::From for RevTableType { - fn from(ty: RevType) -> Self { - match ty { - RevType::DeprecatedLocal => RevTableType::Local, - RevType::DeprecatedRemote => RevTableType::Remote, - } - } -} - -impl std::convert::From for RevType { - fn from(ty: RevTableType) -> Self { - match ty { - RevTableType::Local => RevType::DeprecatedLocal, - RevTableType::Remote => RevType::DeprecatedRemote, - } - } -} diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs index 501d1e591b..0cda25d421 100644 --- a/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs +++ b/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs @@ -1,8 +1,10 @@ +mod delta_document_impl; mod document_impl; mod grid_block_impl; mod grid_impl; mod grid_view_impl; +pub use delta_document_impl::*; pub use document_impl::*; pub use grid_block_impl::*; pub use grid_impl::*; diff --git a/frontend/rust-lib/flowy-revision/src/rev_manager.rs b/frontend/rust-lib/flowy-revision/src/rev_manager.rs index 8e48b90c03..04e8d470d6 100644 --- a/frontend/rust-lib/flowy-revision/src/rev_manager.rs +++ b/frontend/rust-lib/flowy-revision/src/rev_manager.rs @@ -86,7 +86,7 @@ impl RevisionManager { user_id: &str, object_id: &str, rev_persistence: RevisionPersistence, - rev_compactor: C, + rev_compress: C, snapshot_persistence: SP, ) -> Self where @@ -94,7 +94,7 @@ impl RevisionManager { C: 'static + RevisionCompress, { let rev_id_counter = RevIdCounter::new(0); - let rev_compactor = Arc::new(rev_compactor); + let rev_compress = Arc::new(rev_compress); let rev_persistence = Arc::new(rev_persistence); let rev_snapshot = Arc::new(RevisionSnapshotManager::new(user_id, object_id, snapshot_persistence)); #[cfg(feature = "flowy_unit_test")] @@ -106,7 +106,7 @@ impl RevisionManager { rev_id_counter, rev_persistence, rev_snapshot, - rev_compress: rev_compactor, + rev_compress, #[cfg(feature = "flowy_unit_test")] rev_ack_notifier: revision_ack_notifier, } @@ -130,6 +130,18 @@ impl RevisionManager { B::deserialize_revisions(&self.object_id, revisions) } + pub async fn load_revisions(&self) -> FlowyResult> { + let revisions = RevisionLoader { + object_id: self.object_id.clone(), + user_id: self.user_id.clone(), + cloud: None, + rev_persistence: self.rev_persistence.clone(), + } + .load_revisions() + .await?; + Ok(revisions) + } + #[tracing::instrument(level = "debug", skip(self, revisions), err)] pub async fn reset_object(&self, revisions: RepeatedRevision) -> FlowyResult<()> { let rev_id = pair_rev_id_from_revisions(&revisions).1; @@ -264,4 +276,10 @@ impl RevisionLoader { Ok((revisions, rev_id)) } + + pub async fn load_revisions(&self) -> Result, FlowyError> { + let records = self.rev_persistence.batch_get(&self.object_id)?; + let revisions = records.into_iter().map(|record| record.revision).collect::<_>(); + Ok(revisions) + } } diff --git a/frontend/rust-lib/flowy-revision/src/rev_persistence.rs b/frontend/rust-lib/flowy-revision/src/rev_persistence.rs index 0c0875d6a5..50e235aa70 100644 --- a/frontend/rust-lib/flowy-revision/src/rev_persistence.rs +++ b/frontend/rust-lib/flowy-revision/src/rev_persistence.rs @@ -1,5 +1,5 @@ use crate::cache::{ - disk::{RevisionChangeset, RevisionDiskCache, SQLiteDocumentRevisionPersistence}, + disk::{RevisionChangeset, RevisionDiskCache, SQLiteDeltaDocumentRevisionPersistence}, memory::RevisionMemoryCacheDelegate, }; use crate::disk::{RevisionRecord, RevisionState, SQLiteGridBlockRevisionPersistence}; @@ -228,7 +228,7 @@ pub fn mk_text_block_revision_disk_cache( user_id: &str, pool: Arc, ) -> Arc> { - Arc::new(SQLiteDocumentRevisionPersistence::new(user_id, pool)) + Arc::new(SQLiteDeltaDocumentRevisionPersistence::new(user_id, pool)) } pub fn mk_grid_block_revision_disk_cache( diff --git a/frontend/rust-lib/flowy-sdk/src/deps_resolve/document_deps.rs b/frontend/rust-lib/flowy-sdk/src/deps_resolve/document_deps.rs index 168db021be..346f4dadcb 100644 --- a/frontend/rust-lib/flowy-sdk/src/deps_resolve/document_deps.rs +++ b/frontend/rust-lib/flowy-sdk/src/deps_resolve/document_deps.rs @@ -2,7 +2,7 @@ use bytes::Bytes; use flowy_database::ConnectionPool; use flowy_document::{ errors::{internal_error, FlowyError}, - DocumentCloudService, DocumentConfig, DocumentManager, DocumentUser, + DocumentCloudService, DocumentConfig, DocumentDatabase, DocumentManager, DocumentUser, }; use flowy_net::ClientServerConfiguration; use flowy_net::{ @@ -25,16 +25,18 @@ impl DocumentDepsResolver { server_config: &ClientServerConfiguration, document_config: &DocumentConfig, ) -> Arc { - let user = Arc::new(BlockUserImpl(user_session)); + let user = Arc::new(BlockUserImpl(user_session.clone())); let rev_web_socket = Arc::new(DocumentRevisionWebSocket(ws_conn.clone())); let cloud_service: Arc = match local_server { None => Arc::new(DocumentCloudServiceImpl::new(server_config.clone())), Some(local_server) => local_server, }; + let database = Arc::new(DocumentDatabaseImpl(user_session)); let manager = Arc::new(DocumentManager::new( cloud_service, user, + database, rev_web_socket, document_config.clone(), )); @@ -64,7 +66,10 @@ impl DocumentUser for BlockUserImpl { fn token(&self) -> Result { self.0.token() } +} +struct DocumentDatabaseImpl(Arc); +impl DocumentDatabase for DocumentDatabaseImpl { fn db_pool(&self) -> Result, FlowyError> { self.0.db_pool() } diff --git a/frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs index 7935da0b8b..4625bfa6bf 100644 --- a/frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs @@ -1,7 +1,8 @@ use bytes::Bytes; use flowy_database::ConnectionPool; + use flowy_document::DocumentManager; -use flowy_folder::entities::{ViewDataTypePB, ViewLayoutTypePB}; +use flowy_folder::entities::{ViewDataFormatPB, ViewLayoutTypePB, ViewPB}; use flowy_folder::manager::{ViewDataProcessor, ViewDataProcessorMap}; use flowy_folder::{ errors::{internal_error, FlowyError}, @@ -63,16 +64,20 @@ impl FolderDepsResolver { } fn make_view_data_processor( - text_block_manager: Arc, + document_manager: Arc, grid_manager: Arc, ) -> ViewDataProcessorMap { - let mut map: HashMap> = HashMap::new(); + let mut map: HashMap> = HashMap::new(); - let block_data_impl = DocumentViewDataProcessor(text_block_manager); - map.insert(block_data_impl.data_type(), Arc::new(block_data_impl)); + let document_processor = Arc::new(DocumentViewDataProcessor(document_manager)); + document_processor.data_types().into_iter().for_each(|data_type| { + map.insert(data_type, document_processor.clone()); + }); - let grid_data_impl = GridViewDataProcessor(grid_manager); - map.insert(grid_data_impl.data_type(), Arc::new(grid_data_impl)); + let grid_data_impl = Arc::new(GridViewDataProcessor(grid_manager)); + grid_data_impl.data_types().into_iter().for_each(|data_type| { + map.insert(data_type, grid_data_impl.clone()); + }); Arc::new(map) } @@ -137,30 +142,26 @@ impl WSMessageReceiver for FolderWSMessageReceiverImpl { struct DocumentViewDataProcessor(Arc); impl ViewDataProcessor for DocumentViewDataProcessor { - fn initialize(&self) -> FutureResult<(), FlowyError> { - let manager = self.0.clone(); - FutureResult::new(async move { manager.init() }) - } - - fn create_container( + fn create_view( &self, user_id: &str, view_id: &str, layout: ViewLayoutTypePB, - delta_data: Bytes, + view_data: Bytes, ) -> FutureResult<(), FlowyError> { // Only accept Document type debug_assert_eq!(layout, ViewLayoutTypePB::Document); - let repeated_revision: RepeatedRevision = Revision::initial_revision(user_id, view_id, delta_data).into(); + let repeated_revision: RepeatedRevision = Revision::initial_revision(user_id, view_id, view_data).into(); let view_id = view_id.to_string(); let manager = self.0.clone(); + FutureResult::new(async move { let _ = manager.create_document(view_id, repeated_revision).await?; Ok(()) }) } - fn close_container(&self, view_id: &str) -> FutureResult<(), FlowyError> { + fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError> { let manager = self.0.clone(); let view_id = view_id.to_string(); FutureResult::new(async move { @@ -169,13 +170,13 @@ impl ViewDataProcessor for DocumentViewDataProcessor { }) } - fn get_view_data(&self, view_id: &str) -> FutureResult { - let view_id = view_id.to_string(); + fn get_view_data(&self, view: &ViewPB) -> FutureResult { + let view_id = view.id.clone(); let manager = self.0.clone(); FutureResult::new(async move { let editor = manager.open_document_editor(view_id).await?; - let delta_bytes = Bytes::from(editor.export().await?); - Ok(delta_bytes) + let document_data = Bytes::from(editor.duplicate().await?); + Ok(document_data) }) } @@ -184,14 +185,15 @@ impl ViewDataProcessor for DocumentViewDataProcessor { user_id: &str, view_id: &str, layout: ViewLayoutTypePB, + _data_format: ViewDataFormatPB, ) -> FutureResult { debug_assert_eq!(layout, ViewLayoutTypePB::Document); let user_id = user_id.to_string(); let view_id = view_id.to_string(); let manager = self.0.clone(); - let view_data = self.0.initial_document_content(); + let document_content = self.0.initial_document_content(); FutureResult::new(async move { - let delta_data = Bytes::from(view_data); + let delta_data = Bytes::from(document_content); let repeated_revision: RepeatedRevision = Revision::initial_revision(&user_id, &view_id, delta_data.clone()).into(); let _ = manager.create_document(view_id, repeated_revision).await?; @@ -210,18 +212,14 @@ impl ViewDataProcessor for DocumentViewDataProcessor { FutureResult::new(async move { Ok(Bytes::from(data)) }) } - fn data_type(&self) -> ViewDataTypePB { - ViewDataTypePB::Text + fn data_types(&self) -> Vec { + vec![ViewDataFormatPB::DeltaFormat, ViewDataFormatPB::TreeFormat] } } struct GridViewDataProcessor(Arc); impl ViewDataProcessor for GridViewDataProcessor { - fn initialize(&self) -> FutureResult<(), FlowyError> { - FutureResult::new(async { Ok(()) }) - } - - fn create_container( + fn create_view( &self, user_id: &str, view_id: &str, @@ -237,7 +235,7 @@ impl ViewDataProcessor for GridViewDataProcessor { }) } - fn close_container(&self, view_id: &str) -> FutureResult<(), FlowyError> { + fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError> { let grid_manager = self.0.clone(); let view_id = view_id.to_string(); FutureResult::new(async move { @@ -246,9 +244,9 @@ impl ViewDataProcessor for GridViewDataProcessor { }) } - fn get_view_data(&self, view_id: &str) -> FutureResult { - let view_id = view_id.to_string(); + fn get_view_data(&self, view: &ViewPB) -> FutureResult { let grid_manager = self.0.clone(); + let view_id = view.id.clone(); FutureResult::new(async move { let editor = grid_manager.open_grid(view_id).await?; let delta_bytes = editor.duplicate_grid().await?; @@ -261,7 +259,9 @@ impl ViewDataProcessor for GridViewDataProcessor { user_id: &str, view_id: &str, layout: ViewLayoutTypePB, + data_format: ViewDataFormatPB, ) -> FutureResult { + debug_assert_eq!(data_format, ViewDataFormatPB::DatabaseFormat); let (build_context, layout) = match layout { ViewLayoutTypePB::Grid => (make_default_grid(), GridLayout::Table), ViewLayoutTypePB::Board => (make_default_board(), GridLayout::Board), @@ -308,7 +308,7 @@ impl ViewDataProcessor for GridViewDataProcessor { }) } - fn data_type(&self) -> ViewDataTypePB { - ViewDataTypePB::Database + fn data_types(&self) -> Vec { + vec![ViewDataFormatPB::DatabaseFormat] } } diff --git a/frontend/rust-lib/flowy-sdk/src/lib.rs b/frontend/rust-lib/flowy-sdk/src/lib.rs index fd26292bf1..58bae2594d 100644 --- a/frontend/rust-lib/flowy-sdk/src/lib.rs +++ b/frontend/rust-lib/flowy-sdk/src/lib.rs @@ -3,7 +3,10 @@ pub mod module; pub use flowy_net::get_client_server_configuration; use crate::deps_resolve::*; + +use flowy_document::entities::DocumentVersionPB; use flowy_document::{DocumentConfig, DocumentManager}; +use flowy_folder::entities::ViewDataFormatPB; use flowy_folder::{errors::FlowyError, manager::FolderManager}; use flowy_grid::manager::GridManager; use flowy_net::ClientServerConfiguration; @@ -34,7 +37,7 @@ pub struct FlowySDKConfig { root: String, log_filter: String, server_config: ClientServerConfiguration, - document_config: DocumentConfig, + pub document: DocumentConfig, } impl fmt::Debug for FlowySDKConfig { @@ -42,23 +45,27 @@ impl fmt::Debug for FlowySDKConfig { f.debug_struct("FlowySDKConfig") .field("root", &self.root) .field("server-config", &self.server_config) - .field("document-config", &self.document_config) + .field("document-config", &self.document) .finish() } } impl FlowySDKConfig { - pub fn new(root: &str, name: &str, server_config: ClientServerConfiguration, use_new_editor: bool) -> Self { - let document_config = DocumentConfig { use_new_editor }; + pub fn new(root: &str, name: &str, server_config: ClientServerConfiguration) -> Self { FlowySDKConfig { name: name.to_owned(), root: root.to_owned(), log_filter: crate_log_filter("info".to_owned()), server_config, - document_config, + document: DocumentConfig::default(), } } + pub fn with_document_version(mut self, version: DocumentVersionPB) -> Self { + self.document.version = version; + self + } + pub fn log_filter(mut self, level: &str) -> Self { self.log_filter = crate_log_filter(level.to_owned()); self @@ -91,7 +98,7 @@ fn crate_log_filter(level: String) -> String { #[derive(Clone)] pub struct FlowySDK { #[allow(dead_code)] - config: FlowySDKConfig, + pub config: FlowySDKConfig, pub user_session: Arc, pub document_manager: Arc, pub folder_manager: Arc, @@ -108,14 +115,14 @@ impl FlowySDK { tracing::debug!("🔥 {:?}", config); let runtime = tokio_default_runtime().unwrap(); let (local_server, ws_conn) = mk_local_server(&config.server_config); - let (user_session, text_block_manager, folder_manager, local_server, grid_manager) = runtime.block_on(async { + let (user_session, document_manager, folder_manager, local_server, grid_manager) = runtime.block_on(async { let user_session = mk_user_session(&config, &local_server, &config.server_config); let document_manager = DocumentDepsResolver::resolve( local_server.clone(), ws_conn.clone(), user_session.clone(), &config.server_config, - &config.document_config, + &config.document, ); let grid_manager = GridDepsResolver::resolve(ws_conn.clone(), user_session.clone()).await; @@ -149,16 +156,24 @@ impl FlowySDK { &folder_manager, &grid_manager, &user_session, - &text_block_manager, + &document_manager, ) })); - _start_listening(&dispatcher, &ws_conn, &user_session, &folder_manager, &grid_manager); + _start_listening( + &config, + &dispatcher, + &ws_conn, + &user_session, + &document_manager, + &folder_manager, + &grid_manager, + ); Self { config, user_session, - document_manager: text_block_manager, + document_manager, folder_manager, grid_manager, dispatcher, @@ -173,9 +188,11 @@ impl FlowySDK { } fn _start_listening( + config: &FlowySDKConfig, dispatch: &EventDispatcher, ws_conn: &Arc, user_session: &Arc, + document_manager: &Arc, folder_manager: &Arc, grid_manager: &Arc, ) { @@ -186,15 +203,19 @@ fn _start_listening( let cloned_folder_manager = folder_manager.clone(); let ws_conn = ws_conn.clone(); let user_session = user_session.clone(); + let document_manager = document_manager.clone(); + let config = config.clone(); dispatch.spawn(async move { user_session.init(); listen_on_websocket(ws_conn.clone()); _listen_user_status( + config, ws_conn.clone(), subscribe_user_status, - folder_manager.clone(), - grid_manager.clone(), + document_manager, + folder_manager, + grid_manager, ) .await; }); @@ -220,8 +241,10 @@ fn mk_local_server( } async fn _listen_user_status( + config: FlowySDKConfig, ws_conn: Arc, mut subscribe: broadcast::Receiver, + document_manager: Arc, folder_manager: Arc, grid_manager: Arc, ) { @@ -231,6 +254,7 @@ async fn _listen_user_status( UserStatus::Login { token, user_id } => { tracing::trace!("User did login"); let _ = folder_manager.initialize(&user_id, &token).await?; + let _ = document_manager.initialize(&user_id).await?; let _ = grid_manager.initialize(&user_id, &token).await?; let _ = ws_conn.start(token, user_id).await?; } @@ -246,7 +270,15 @@ async fn _listen_user_status( } UserStatus::SignUp { profile, ret } => { tracing::trace!("User did sign up"); + + let view_data_type = match config.document.version { + DocumentVersionPB::V0 => ViewDataFormatPB::DeltaFormat, + DocumentVersionPB::V1 => ViewDataFormatPB::TreeFormat, + }; let _ = folder_manager + .initialize_with_new_user(&profile.id, &profile.token, view_data_type) + .await?; + let _ = document_manager .initialize_with_new_user(&profile.id, &profile.token) .await?; diff --git a/frontend/rust-lib/flowy-test/src/helper.rs b/frontend/rust-lib/flowy-test/src/helper.rs index 9a055c4571..3d9a9d06ac 100644 --- a/frontend/rust-lib/flowy-test/src/helper.rs +++ b/frontend/rust-lib/flowy-test/src/helper.rs @@ -25,11 +25,16 @@ pub struct ViewTest { impl ViewTest { #[allow(dead_code)] - pub async fn new(sdk: &FlowySDKTest, data_type: ViewDataTypePB, layout: ViewLayoutTypePB, data: Vec) -> Self { + pub async fn new( + sdk: &FlowySDKTest, + data_format: ViewDataFormatPB, + layout: ViewLayoutTypePB, + data: Vec, + ) -> Self { let workspace = create_workspace(sdk, "Workspace", "").await; open_workspace(sdk, &workspace.id).await; let app = create_app(sdk, "App", "AppFlowy GitHub Project", &workspace.id).await; - let view = create_view(sdk, &app.id, data_type, layout, data).await; + let view = create_view(sdk, &app.id, data_format, layout, data).await; Self { sdk: sdk.clone(), workspace, @@ -39,15 +44,19 @@ impl ViewTest { } pub async fn new_grid_view(sdk: &FlowySDKTest, data: Vec) -> Self { - Self::new(sdk, ViewDataTypePB::Database, ViewLayoutTypePB::Grid, data).await + Self::new(sdk, ViewDataFormatPB::DatabaseFormat, ViewLayoutTypePB::Grid, data).await } pub async fn new_board_view(sdk: &FlowySDKTest, data: Vec) -> Self { - Self::new(sdk, ViewDataTypePB::Database, ViewLayoutTypePB::Board, data).await + Self::new(sdk, ViewDataFormatPB::DatabaseFormat, ViewLayoutTypePB::Board, data).await } pub async fn new_document_view(sdk: &FlowySDKTest) -> Self { - Self::new(sdk, ViewDataTypePB::Text, ViewLayoutTypePB::Document, vec![]).await + let view_data_format = match sdk.document_version() { + DocumentVersionPB::V0 => ViewDataFormatPB::DeltaFormat, + DocumentVersionPB::V1 => ViewDataFormatPB::TreeFormat, + }; + Self::new(sdk, view_data_format, ViewLayoutTypePB::Document, vec![]).await } } @@ -97,7 +106,7 @@ async fn create_app(sdk: &FlowySDKTest, name: &str, desc: &str, workspace_id: &s async fn create_view( sdk: &FlowySDKTest, app_id: &str, - data_type: ViewDataTypePB, + data_format: ViewDataFormatPB, layout: ViewLayoutTypePB, data: Vec, ) -> ViewPB { @@ -106,7 +115,7 @@ async fn create_view( name: "View A".to_string(), desc: "".to_string(), thumbnail: Some("http://1.png".to_string()), - data_type, + data_format, layout, view_content_data: data, }; diff --git a/frontend/rust-lib/flowy-test/src/lib.rs b/frontend/rust-lib/flowy-test/src/lib.rs index 3c4f60e391..68e9834abf 100644 --- a/frontend/rust-lib/flowy-test/src/lib.rs +++ b/frontend/rust-lib/flowy-test/src/lib.rs @@ -3,6 +3,7 @@ pub mod helper; use crate::helper::*; +use flowy_document::entities::DocumentVersionPB; use flowy_net::get_client_server_configuration; use flowy_sdk::{FlowySDK, FlowySDKConfig}; use flowy_user::entities::UserProfilePB; @@ -28,14 +29,16 @@ impl std::ops::Deref for FlowySDKTest { impl std::default::Default for FlowySDKTest { fn default() -> Self { - Self::new(false) + Self::new(DocumentVersionPB::V0) } } impl FlowySDKTest { - pub fn new(use_new_editor: bool) -> Self { + pub fn new(document_version: DocumentVersionPB) -> Self { let server_config = get_client_server_configuration().unwrap(); - let config = FlowySDKConfig::new(&root_dir(), &nanoid!(6), server_config, use_new_editor).log_filter("info"); + let config = FlowySDKConfig::new(&root_dir(), &nanoid!(6), server_config) + .with_document_version(document_version) + .log_filter("info"); let sdk = std::thread::spawn(|| FlowySDK::new(config)).join().unwrap(); std::mem::forget(sdk.dispatcher()); Self { inner: sdk } @@ -51,4 +54,8 @@ impl FlowySDKTest { init_user_setting(self.inner.dispatcher()).await; context.user_profile } + + pub fn document_version(&self) -> DocumentVersionPB { + self.inner.config.document.version.clone() + } } diff --git a/shared-lib/Cargo.lock b/shared-lib/Cargo.lock index e519a829cd..e20285ac8e 100644 --- a/shared-lib/Cargo.lock +++ b/shared-lib/Cargo.lock @@ -815,6 +815,7 @@ dependencies = [ "bytes", "dashmap", "derive_more", + "indexmap", "indextree", "lazy_static", "log", diff --git a/shared-lib/flowy-folder-data-model/src/revision/view_rev.rs b/shared-lib/flowy-folder-data-model/src/revision/view_rev.rs index e5ee15f7c9..f618a58d96 100644 --- a/shared-lib/flowy-folder-data-model/src/revision/view_rev.rs +++ b/shared-lib/flowy-folder-data-model/src/revision/view_rev.rs @@ -17,7 +17,8 @@ pub struct ViewRevision { pub desc: String, #[serde(default)] - pub data_type: ViewDataTypeRevision, + #[serde(rename = "data_type")] + pub data_format: ViewDataFormatRevision, pub version: i64, // Deprecated @@ -55,14 +56,15 @@ impl std::convert::From for TrashRevision { #[derive(Eq, PartialEq, Debug, Clone, Serialize_repr, Deserialize_repr)] #[repr(u8)] -pub enum ViewDataTypeRevision { - Text = 0, - Database = 1, +pub enum ViewDataFormatRevision { + DeltaFormat = 0, + DatabaseFormat = 1, + TreeFormat = 2, } -impl std::default::Default for ViewDataTypeRevision { +impl std::default::Default for ViewDataFormatRevision { fn default() -> Self { - ViewDataTypeRevision::Text + ViewDataFormatRevision::DeltaFormat } } diff --git a/shared-lib/flowy-folder-data-model/src/user_default.rs b/shared-lib/flowy-folder-data-model/src/user_default.rs index bb4eacc4dc..ac85367229 100644 --- a/shared-lib/flowy-folder-data-model/src/user_default.rs +++ b/shared-lib/flowy-folder-data-model/src/user_default.rs @@ -1,6 +1,6 @@ use crate::revision::{ - gen_app_id, gen_view_id, gen_workspace_id, AppRevision, ViewDataTypeRevision, ViewLayoutTypeRevision, ViewRevision, - WorkspaceRevision, + gen_app_id, gen_view_id, gen_workspace_id, AppRevision, ViewDataFormatRevision, ViewLayoutTypeRevision, + ViewRevision, WorkspaceRevision, }; use chrono::Utc; @@ -50,7 +50,7 @@ fn create_default_view(app_id: String, time: chrono::DateTime) -> ViewRevis app_id, name, desc: "".to_string(), - data_type: ViewDataTypeRevision::Text, + data_format: ViewDataFormatRevision::DeltaFormat, version: 0, belongings: vec![], modified_time: time.timestamp(), diff --git a/shared-lib/flowy-sync/src/READ_ME.json b/shared-lib/flowy-sync/src/READ_ME.json deleted file mode 100644 index 624802720c..0000000000 --- a/shared-lib/flowy-sync/src/READ_ME.json +++ /dev/null @@ -1 +0,0 @@ -[{"insert":"\n👋 Welcome to AppFlowy!"},{"insert":"\n","attributes":{"header":1}},{"insert":"\nHere are the basics"},{"insert":"\n","attributes":{"header":2}},{"insert":"Click anywhere and just start typing"},{"insert":"\n","attributes":{"list":"unchecked"}},{"insert":"Highlight","attributes":{"background":"#fff2cd"}},{"insert":" any text, and use the menu at the bottom to "},{"insert":"style","attributes":{"italic":true}},{"insert":" "},{"insert":"your","attributes":{"bold":true}},{"insert":" "},{"insert":"writing","attributes":{"underline":true}},{"insert":" "},{"insert":"however","attributes":{"code":true}},{"insert":" "},{"insert":"you","attributes":{"strike":true}},{"insert":" "},{"insert":"like","attributes":{"background":"#e8e0ff"}},{"insert":"\n","attributes":{"list":"unchecked"}},{"insert":"Click "},{"insert":"+ New Page","attributes":{"background":"#defff1","bold":true}},{"insert":" button at the bottom of your sidebar to add a new page"},{"insert":"\n","attributes":{"list":"unchecked"}},{"insert":"Click the "},{"insert":"'","attributes":{"background":"#defff1"}},{"insert":"+","attributes":{"background":"#defff1","bold":true}},{"insert":"'","attributes":{"background":"#defff1"}},{"insert":" next to any page title in the sidebar to quickly add a new subpage"},{"insert":"\n","attributes":{"list":"unchecked"}},{"insert":"\nHave a question? "},{"insert":"\n","attributes":{"header":2}},{"insert":"Click the "},{"insert":"'?'","attributes":{"background":"#defff1","bold":true}},{"insert":" at the bottom right for help and support.\n\nLike AppFlowy? Follow us:"},{"insert":"\n","attributes":{"header":2}},{"insert":"GitHub: https://github.com/AppFlowy-IO/appflowy"},{"insert":"\n","attributes":{"blockquote":true}},{"insert":"Twitter: https://twitter.com/appflowy"},{"insert":"\n","attributes":{"blockquote":true}},{"insert":"Newsletter: https://www.appflowy.io/blog"},{"insert":"\n","attributes":{"blockquote":true}}] \ No newline at end of file diff --git a/shared-lib/flowy-sync/src/client_document/default/READ_ME.json b/shared-lib/flowy-sync/src/client_document/default/READ_ME.json deleted file mode 100644 index 624802720c..0000000000 --- a/shared-lib/flowy-sync/src/client_document/default/READ_ME.json +++ /dev/null @@ -1 +0,0 @@ -[{"insert":"\n👋 Welcome to AppFlowy!"},{"insert":"\n","attributes":{"header":1}},{"insert":"\nHere are the basics"},{"insert":"\n","attributes":{"header":2}},{"insert":"Click anywhere and just start typing"},{"insert":"\n","attributes":{"list":"unchecked"}},{"insert":"Highlight","attributes":{"background":"#fff2cd"}},{"insert":" any text, and use the menu at the bottom to "},{"insert":"style","attributes":{"italic":true}},{"insert":" "},{"insert":"your","attributes":{"bold":true}},{"insert":" "},{"insert":"writing","attributes":{"underline":true}},{"insert":" "},{"insert":"however","attributes":{"code":true}},{"insert":" "},{"insert":"you","attributes":{"strike":true}},{"insert":" "},{"insert":"like","attributes":{"background":"#e8e0ff"}},{"insert":"\n","attributes":{"list":"unchecked"}},{"insert":"Click "},{"insert":"+ New Page","attributes":{"background":"#defff1","bold":true}},{"insert":" button at the bottom of your sidebar to add a new page"},{"insert":"\n","attributes":{"list":"unchecked"}},{"insert":"Click the "},{"insert":"'","attributes":{"background":"#defff1"}},{"insert":"+","attributes":{"background":"#defff1","bold":true}},{"insert":"'","attributes":{"background":"#defff1"}},{"insert":" next to any page title in the sidebar to quickly add a new subpage"},{"insert":"\n","attributes":{"list":"unchecked"}},{"insert":"\nHave a question? "},{"insert":"\n","attributes":{"header":2}},{"insert":"Click the "},{"insert":"'?'","attributes":{"background":"#defff1","bold":true}},{"insert":" at the bottom right for help and support.\n\nLike AppFlowy? Follow us:"},{"insert":"\n","attributes":{"header":2}},{"insert":"GitHub: https://github.com/AppFlowy-IO/appflowy"},{"insert":"\n","attributes":{"blockquote":true}},{"insert":"Twitter: https://twitter.com/appflowy"},{"insert":"\n","attributes":{"blockquote":true}},{"insert":"Newsletter: https://www.appflowy.io/blog"},{"insert":"\n","attributes":{"blockquote":true}}] \ No newline at end of file diff --git a/shared-lib/flowy-sync/src/client_document/default/mod.rs b/shared-lib/flowy-sync/src/client_document/default/mod.rs deleted file mode 100644 index e187a259c6..0000000000 --- a/shared-lib/flowy-sync/src/client_document/default/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -use lib_ot::text_delta::TextOperations; - -#[inline] -pub fn initial_read_me() -> TextOperations { - let json = include_str!("READ_ME.json"); - TextOperations::from_json(json).unwrap() -} - -#[cfg(test)] -mod tests { - use crate::client_document::default::initial_read_me; - - #[test] - fn load_read_me() { - println!("{}", initial_read_me().json_str()); - } -} diff --git a/shared-lib/flowy-sync/src/client_document/document_pad.rs b/shared-lib/flowy-sync/src/client_document/document_pad.rs index 4273c05f9f..be438dca25 100644 --- a/shared-lib/flowy-sync/src/client_document/document_pad.rs +++ b/shared-lib/flowy-sync/src/client_document/document_pad.rs @@ -6,8 +6,8 @@ use crate::{ errors::CollaborateError, }; use bytes::Bytes; -use lib_ot::text_delta::TextOperationBuilder; -use lib_ot::{core::*, text_delta::TextOperations}; +use lib_ot::text_delta::DeltaTextOperationBuilder; +use lib_ot::{core::*, text_delta::DeltaTextOperations}; use tokio::sync::mpsc; pub trait InitialDocument { @@ -17,23 +17,23 @@ pub trait InitialDocument { pub struct EmptyDocument(); impl InitialDocument for EmptyDocument { fn json_str() -> String { - TextOperations::default().json_str() + DeltaTextOperations::default().json_str() } } pub struct NewlineDocument(); impl InitialDocument for NewlineDocument { fn json_str() -> String { - initial_old_document_content() + initial_delta_document_content() } } -pub fn initial_old_document_content() -> String { - TextOperationBuilder::new().insert("\n").build().json_str() +pub fn initial_delta_document_content() -> String { + DeltaTextOperationBuilder::new().insert("\n").build().json_str() } pub struct ClientDocument { - operations: TextOperations, + operations: DeltaTextOperations, history: History, view: ViewExtensions, last_edit_time: usize, @@ -46,7 +46,7 @@ impl ClientDocument { Self::from_json(&content).unwrap() } - pub fn from_operations(operations: TextOperations) -> Self { + pub fn from_operations(operations: DeltaTextOperations) -> Self { ClientDocument { operations, history: History::new(), @@ -57,7 +57,7 @@ impl ClientDocument { } pub fn from_json(json: &str) -> Result { - let operations = TextOperations::from_json(json)?; + let operations = DeltaTextOperations::from_json(json)?; Ok(Self::from_operations(operations)) } @@ -73,7 +73,7 @@ impl ClientDocument { self.operations.content().unwrap() } - pub fn get_operations(&self) -> &TextOperations { + pub fn get_operations(&self) -> &DeltaTextOperations { &self.operations } @@ -86,7 +86,7 @@ impl ClientDocument { self.notify = Some(notify); } - pub fn set_operations(&mut self, operations: TextOperations) { + pub fn set_operations(&mut self, operations: DeltaTextOperations) { tracing::trace!("document: {}", operations.json_str()); self.operations = operations; @@ -98,7 +98,7 @@ impl ClientDocument { } } - pub fn compose_operations(&mut self, operations: TextOperations) -> Result<(), CollaborateError> { + pub fn compose_operations(&mut self, operations: DeltaTextOperations) -> Result<(), CollaborateError> { tracing::trace!("{} compose {}", &self.operations.json_str(), operations.json_str()); let composed_operations = self.operations.compose(&operations)?; let mut undo_operations = operations.invert(&self.operations); @@ -124,7 +124,7 @@ impl ClientDocument { Ok(()) } - pub fn insert(&mut self, index: usize, data: T) -> Result { + pub fn insert(&mut self, index: usize, data: T) -> Result { let text = data.to_string(); let interval = Interval::new(index, index); let _ = validate_interval(&self.operations, &interval)?; @@ -133,7 +133,7 @@ impl ClientDocument { Ok(operations) } - pub fn delete(&mut self, interval: Interval) -> Result { + pub fn delete(&mut self, interval: Interval) -> Result { let _ = validate_interval(&self.operations, &interval)?; debug_assert!(!interval.is_empty()); let operations = self.view.delete(&self.operations, interval)?; @@ -147,7 +147,7 @@ impl ClientDocument { &mut self, interval: Interval, attribute: AttributeEntry, - ) -> Result { + ) -> Result { let _ = validate_interval(&self.operations, &interval)?; tracing::trace!("format {} with {:?}", interval, attribute); let operations = self.view.format(&self.operations, attribute, interval).unwrap(); @@ -155,9 +155,13 @@ impl ClientDocument { Ok(operations) } - pub fn replace(&mut self, interval: Interval, data: T) -> Result { + pub fn replace( + &mut self, + interval: Interval, + data: T, + ) -> Result { let _ = validate_interval(&self.operations, &interval)?; - let mut operations = TextOperations::default(); + let mut operations = DeltaTextOperations::default(); let text = data.to_string(); if !text.is_empty() { operations = self.view.insert(&self.operations, &text, interval)?; @@ -215,7 +219,10 @@ impl ClientDocument { } impl ClientDocument { - fn invert(&self, operations: &TextOperations) -> Result<(TextOperations, TextOperations), CollaborateError> { + fn invert( + &self, + operations: &DeltaTextOperations, + ) -> Result<(DeltaTextOperations, DeltaTextOperations), CollaborateError> { // c = a.compose(b) // d = b.invert(a) // a = c.compose(d) @@ -225,7 +232,7 @@ impl ClientDocument { } } -fn validate_interval(operations: &TextOperations, interval: &Interval) -> Result<(), CollaborateError> { +fn validate_interval(operations: &DeltaTextOperations, interval: &Interval) -> Result<(), CollaborateError> { if operations.utf16_target_len < interval.end { log::error!( "{:?} out of bounds. should 0..{}", diff --git a/shared-lib/flowy-sync/src/client_document/extensions/delete/default_delete.rs b/shared-lib/flowy-sync/src/client_document/extensions/delete/default_delete.rs index 5fcf100299..208786a61d 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/delete/default_delete.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/delete/default_delete.rs @@ -1,7 +1,7 @@ use crate::client_document::DeleteExt; use lib_ot::{ - core::{Interval, OperationBuilder}, - text_delta::TextOperations, + core::{DeltaOperationBuilder, Interval}, + text_delta::DeltaTextOperations, }; pub struct DefaultDelete {} @@ -10,9 +10,9 @@ impl DeleteExt for DefaultDelete { "DefaultDelete" } - fn apply(&self, _delta: &TextOperations, interval: Interval) -> Option { + fn apply(&self, _delta: &DeltaTextOperations, interval: Interval) -> Option { Some( - OperationBuilder::new() + DeltaOperationBuilder::new() .retain(interval.start) .delete(interval.size()) .build(), diff --git a/shared-lib/flowy-sync/src/client_document/extensions/delete/preserve_line_format_merge.rs b/shared-lib/flowy-sync/src/client_document/extensions/delete/preserve_line_format_merge.rs index 0f30102616..2cf75187c3 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/delete/preserve_line_format_merge.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/delete/preserve_line_format_merge.rs @@ -1,7 +1,7 @@ use crate::{client_document::DeleteExt, util::is_newline}; use lib_ot::{ - core::{Interval, OperationAttributes, OperationBuilder, OperationIterator, Utf16CodeUnitMetric, NEW_LINE}, - text_delta::{empty_attributes, TextOperations}, + core::{DeltaOperationBuilder, Interval, OperationAttributes, OperationIterator, Utf16CodeUnitMetric, NEW_LINE}, + text_delta::{empty_attributes, DeltaTextOperations}, }; pub struct PreserveLineFormatOnMerge {} @@ -10,7 +10,7 @@ impl DeleteExt for PreserveLineFormatOnMerge { "PreserveLineFormatOnMerge" } - fn apply(&self, delta: &TextOperations, interval: Interval) -> Option { + fn apply(&self, delta: &DeltaTextOperations, interval: Interval) -> Option { if interval.is_empty() { return None; } @@ -25,7 +25,7 @@ impl DeleteExt for PreserveLineFormatOnMerge { } iter.seek::(interval.size() - 1); - let mut new_delta = OperationBuilder::new() + let mut new_delta = DeltaOperationBuilder::new() .retain(interval.start) .delete(interval.size()) .build(); diff --git a/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_block_format.rs b/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_block_format.rs index c2c2006e3e..680db108f4 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_block_format.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_block_format.rs @@ -1,8 +1,8 @@ use lib_ot::core::AttributeEntry; use lib_ot::text_delta::is_block; use lib_ot::{ - core::{Interval, OperationBuilder, OperationIterator}, - text_delta::{empty_attributes, AttributeScope, TextOperations}, + core::{DeltaOperationBuilder, Interval, OperationIterator}, + text_delta::{empty_attributes, AttributeScope, DeltaTextOperations}, }; use crate::{ @@ -16,12 +16,17 @@ impl FormatExt for ResolveBlockFormat { "ResolveBlockFormat" } - fn apply(&self, delta: &TextOperations, interval: Interval, attribute: &AttributeEntry) -> Option { + fn apply( + &self, + delta: &DeltaTextOperations, + interval: Interval, + attribute: &AttributeEntry, + ) -> Option { if !is_block(&attribute.key) { return None; } - let mut new_delta = OperationBuilder::new().retain(interval.start).build(); + let mut new_delta = DeltaOperationBuilder::new().retain(interval.start).build(); let mut iter = OperationIterator::from_offset(delta, interval.start); let mut start = 0; let end = interval.size(); diff --git a/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_inline_format.rs b/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_inline_format.rs index 7ee82dcadc..6a3c0d23fa 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_inline_format.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_inline_format.rs @@ -1,8 +1,8 @@ use lib_ot::core::AttributeEntry; use lib_ot::text_delta::is_inline; use lib_ot::{ - core::{Interval, OperationBuilder, OperationIterator}, - text_delta::{AttributeScope, TextOperations}, + core::{DeltaOperationBuilder, Interval, OperationIterator}, + text_delta::{AttributeScope, DeltaTextOperations}, }; use crate::{ @@ -16,11 +16,16 @@ impl FormatExt for ResolveInlineFormat { "ResolveInlineFormat" } - fn apply(&self, delta: &TextOperations, interval: Interval, attribute: &AttributeEntry) -> Option { + fn apply( + &self, + delta: &DeltaTextOperations, + interval: Interval, + attribute: &AttributeEntry, + ) -> Option { if !is_inline(&attribute.key) { return None; } - let mut new_delta = OperationBuilder::new().retain(interval.start).build(); + let mut new_delta = DeltaOperationBuilder::new().retain(interval.start).build(); let mut iter = OperationIterator::from_offset(delta, interval.start); let mut start = 0; let end = interval.size(); diff --git a/shared-lib/flowy-sync/src/client_document/extensions/helper.rs b/shared-lib/flowy-sync/src/client_document/extensions/helper.rs index 6e267f0cd0..4ecb23dcf6 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/helper.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/helper.rs @@ -1,9 +1,13 @@ use crate::util::find_newline; use lib_ot::core::AttributeEntry; -use lib_ot::text_delta::{empty_attributes, AttributeScope, TextOperation, TextOperations}; +use lib_ot::text_delta::{empty_attributes, AttributeScope, DeltaTextOperation, DeltaTextOperations}; -pub(crate) fn line_break(op: &TextOperation, attribute: &AttributeEntry, scope: AttributeScope) -> TextOperations { - let mut new_delta = TextOperations::new(); +pub(crate) fn line_break( + op: &DeltaTextOperation, + attribute: &AttributeEntry, + scope: AttributeScope, +) -> DeltaTextOperations { + let mut new_delta = DeltaTextOperations::new(); let mut start = 0; let end = op.len(); let mut s = op.get_data(); diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_exit_block.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_exit_block.rs index 52ed6165cd..31f40545f9 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_exit_block.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_exit_block.rs @@ -1,6 +1,6 @@ use crate::{client_document::InsertExt, util::is_newline}; -use lib_ot::core::{is_empty_line_at_index, OperationBuilder, OperationIterator}; -use lib_ot::text_delta::{attributes_except_header, BuildInTextAttributeKey, TextOperations}; +use lib_ot::core::{is_empty_line_at_index, DeltaOperationBuilder, OperationIterator}; +use lib_ot::text_delta::{attributes_except_header, BuildInTextAttributeKey, DeltaTextOperations}; pub struct AutoExitBlock {} @@ -9,7 +9,13 @@ impl InsertExt for AutoExitBlock { "AutoExitBlock" } - fn apply(&self, delta: &TextOperations, replace_len: usize, text: &str, index: usize) -> Option { + fn apply( + &self, + delta: &DeltaTextOperations, + replace_len: usize, + text: &str, + index: usize, + ) -> Option { // Auto exit block will be triggered by enter two new lines if !is_newline(text) { return None; @@ -45,7 +51,7 @@ impl InsertExt for AutoExitBlock { attributes.retain_values(&[BuildInTextAttributeKey::Header.as_ref()]); Some( - OperationBuilder::new() + DeltaOperationBuilder::new() .retain(index + replace_len) .retain_with_attributes(1, attributes) .build(), diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_format.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_format.rs index 2fe7b70b0e..e62fcff287 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_format.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_format.rs @@ -1,8 +1,8 @@ use crate::{client_document::InsertExt, util::is_whitespace}; use lib_ot::core::AttributeHashMap; use lib_ot::{ - core::{count_utf16_code_units, OperationBuilder, OperationIterator}, - text_delta::{empty_attributes, BuildInTextAttribute, TextOperations}, + core::{count_utf16_code_units, DeltaOperationBuilder, OperationIterator}, + text_delta::{empty_attributes, BuildInTextAttribute, DeltaTextOperations}, }; use std::cmp::min; use url::Url; @@ -13,7 +13,13 @@ impl InsertExt for AutoFormatExt { "AutoFormatExt" } - fn apply(&self, delta: &TextOperations, replace_len: usize, text: &str, index: usize) -> Option { + fn apply( + &self, + delta: &DeltaTextOperations, + replace_len: usize, + text: &str, + index: usize, + ) -> Option { // enter whitespace to trigger auto format if !is_whitespace(text) { return None; @@ -42,7 +48,7 @@ impl InsertExt for AutoFormatExt { }; return Some( - OperationBuilder::new() + DeltaOperationBuilder::new() .retain(index + replace_len - min(index, format_len)) .retain_with_attributes(format_len, format_attributes) .insert_with_attributes(text, next_attributes) diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/default_insert.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/default_insert.rs index 948cf7f994..78248b6877 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/default_insert.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/default_insert.rs @@ -1,8 +1,8 @@ use crate::client_document::InsertExt; use lib_ot::core::AttributeHashMap; use lib_ot::{ - core::{OperationAttributes, OperationBuilder, OperationIterator, NEW_LINE}, - text_delta::{BuildInTextAttributeKey, TextOperations}, + core::{DeltaOperationBuilder, OperationAttributes, OperationIterator, NEW_LINE}, + text_delta::{BuildInTextAttributeKey, DeltaTextOperations}, }; pub struct DefaultInsertAttribute {} @@ -11,7 +11,13 @@ impl InsertExt for DefaultInsertAttribute { "DefaultInsertAttribute" } - fn apply(&self, delta: &TextOperations, replace_len: usize, text: &str, index: usize) -> Option { + fn apply( + &self, + delta: &DeltaTextOperations, + replace_len: usize, + text: &str, + index: usize, + ) -> Option { let iter = OperationIterator::new(delta); let mut attributes = AttributeHashMap::new(); @@ -35,7 +41,7 @@ impl InsertExt for DefaultInsertAttribute { } Some( - OperationBuilder::new() + DeltaOperationBuilder::new() .retain(index + replace_len) .insert_with_attributes(text, attributes) .build(), diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/mod.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/mod.rs index 19661006e6..3ec97f37d8 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/mod.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/mod.rs @@ -2,7 +2,7 @@ use crate::client_document::InsertExt; pub use auto_exit_block::*; pub use auto_format::*; pub use default_insert::*; -use lib_ot::text_delta::TextOperations; +use lib_ot::text_delta::DeltaTextOperations; pub use preserve_block_format::*; pub use preserve_inline_format::*; pub use reset_format_on_new_line::*; @@ -22,11 +22,11 @@ impl InsertExt for InsertEmbedsExt { fn apply( &self, - _delta: &TextOperations, + _delta: &DeltaTextOperations, _replace_len: usize, _text: &str, _index: usize, - ) -> Option { + ) -> Option { None } } @@ -39,11 +39,11 @@ impl InsertExt for ForceNewlineForInsertsAroundEmbedExt { fn apply( &self, - _delta: &TextOperations, + _delta: &DeltaTextOperations, _replace_len: usize, _text: &str, _index: usize, - ) -> Option { + ) -> Option { None } } diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_block_format.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_block_format.rs index 3ce1d1f9f0..46859cdb1f 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_block_format.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_block_format.rs @@ -1,8 +1,8 @@ use crate::{client_document::InsertExt, util::is_newline}; use lib_ot::core::AttributeHashMap; use lib_ot::{ - core::{OperationBuilder, OperationIterator, NEW_LINE}, - text_delta::{attributes_except_header, empty_attributes, BuildInTextAttributeKey, TextOperations}, + core::{DeltaOperationBuilder, OperationIterator, NEW_LINE}, + text_delta::{attributes_except_header, empty_attributes, BuildInTextAttributeKey, DeltaTextOperations}, }; pub struct PreserveBlockFormatOnInsert {} @@ -11,7 +11,13 @@ impl InsertExt for PreserveBlockFormatOnInsert { "PreserveBlockFormatOnInsert" } - fn apply(&self, delta: &TextOperations, replace_len: usize, text: &str, index: usize) -> Option { + fn apply( + &self, + delta: &DeltaTextOperations, + replace_len: usize, + text: &str, + index: usize, + ) -> Option { if !is_newline(text) { return None; } @@ -32,7 +38,7 @@ impl InsertExt for PreserveBlockFormatOnInsert { } let lines: Vec<_> = text.split(NEW_LINE).collect(); - let mut new_delta = OperationBuilder::new().retain(index + replace_len).build(); + let mut new_delta = DeltaOperationBuilder::new().retain(index + replace_len).build(); lines.iter().enumerate().for_each(|(i, line)| { if !line.is_empty() { new_delta.insert(line, empty_attributes()); diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_inline_format.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_inline_format.rs index f8bd30dc9b..d7f238a21e 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_inline_format.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_inline_format.rs @@ -3,8 +3,8 @@ use crate::{ util::{contain_newline, is_newline}, }; use lib_ot::{ - core::{OpNewline, OperationBuilder, OperationIterator, NEW_LINE}, - text_delta::{empty_attributes, BuildInTextAttributeKey, TextOperations}, + core::{DeltaOperationBuilder, OpNewline, OperationIterator, NEW_LINE}, + text_delta::{empty_attributes, BuildInTextAttributeKey, DeltaTextOperations}, }; pub struct PreserveInlineFormat {} @@ -13,7 +13,13 @@ impl InsertExt for PreserveInlineFormat { "PreserveInlineFormat" } - fn apply(&self, delta: &TextOperations, replace_len: usize, text: &str, index: usize) -> Option { + fn apply( + &self, + delta: &DeltaTextOperations, + replace_len: usize, + text: &str, + index: usize, + ) -> Option { if contain_newline(text) { return None; } @@ -27,7 +33,7 @@ impl InsertExt for PreserveInlineFormat { let mut attributes = prev.get_attributes(); if attributes.is_empty() || !attributes.contains_key(BuildInTextAttributeKey::Link.as_ref()) { return Some( - OperationBuilder::new() + DeltaOperationBuilder::new() .retain(index + replace_len) .insert_with_attributes(text, attributes) .build(), @@ -44,7 +50,7 @@ impl InsertExt for PreserveInlineFormat { } } - let new_delta = OperationBuilder::new() + let new_delta = DeltaOperationBuilder::new() .retain(index + replace_len) .insert_with_attributes(text, attributes) .build(); @@ -59,7 +65,13 @@ impl InsertExt for PreserveLineFormatOnSplit { "PreserveLineFormatOnSplit" } - fn apply(&self, delta: &TextOperations, replace_len: usize, text: &str, index: usize) -> Option { + fn apply( + &self, + delta: &DeltaTextOperations, + replace_len: usize, + text: &str, + index: usize, + ) -> Option { if !is_newline(text) { return None; } @@ -76,7 +88,7 @@ impl InsertExt for PreserveLineFormatOnSplit { return None; } - let mut new_delta = TextOperations::new(); + let mut new_delta = DeltaTextOperations::new(); new_delta.retain(index + replace_len, empty_attributes()); if newline_status.is_contain() { diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/reset_format_on_new_line.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/reset_format_on_new_line.rs index 3a15e9dba0..067e373212 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/reset_format_on_new_line.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/reset_format_on_new_line.rs @@ -1,8 +1,8 @@ use crate::{client_document::InsertExt, util::is_newline}; use lib_ot::core::AttributeHashMap; use lib_ot::{ - core::{OperationBuilder, OperationIterator, Utf16CodeUnitMetric, NEW_LINE}, - text_delta::{BuildInTextAttributeKey, TextOperations}, + core::{DeltaOperationBuilder, OperationIterator, Utf16CodeUnitMetric, NEW_LINE}, + text_delta::{BuildInTextAttributeKey, DeltaTextOperations}, }; pub struct ResetLineFormatOnNewLine {} @@ -11,7 +11,13 @@ impl InsertExt for ResetLineFormatOnNewLine { "ResetLineFormatOnNewLine" } - fn apply(&self, delta: &TextOperations, replace_len: usize, text: &str, index: usize) -> Option { + fn apply( + &self, + delta: &DeltaTextOperations, + replace_len: usize, + text: &str, + index: usize, + ) -> Option { if !is_newline(text) { return None; } @@ -33,7 +39,7 @@ impl InsertExt for ResetLineFormatOnNewLine { let len = index + replace_len; Some( - OperationBuilder::new() + DeltaOperationBuilder::new() .retain(len) .insert_with_attributes(NEW_LINE, next_op.get_attributes()) .retain_with_attributes(1, reset_attribute) diff --git a/shared-lib/flowy-sync/src/client_document/extensions/mod.rs b/shared-lib/flowy-sync/src/client_document/extensions/mod.rs index a2cd2dc9d7..6cfc1f48ac 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/mod.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/mod.rs @@ -2,7 +2,7 @@ pub use delete::*; pub use format::*; pub use insert::*; use lib_ot::core::AttributeEntry; -use lib_ot::{core::Interval, text_delta::TextOperations}; +use lib_ot::{core::Interval, text_delta::DeltaTextOperations}; mod delete; mod format; @@ -15,15 +15,26 @@ pub type DeleteExtension = Box; pub trait InsertExt { fn ext_name(&self) -> &str; - fn apply(&self, delta: &TextOperations, replace_len: usize, text: &str, index: usize) -> Option; + fn apply( + &self, + delta: &DeltaTextOperations, + replace_len: usize, + text: &str, + index: usize, + ) -> Option; } pub trait FormatExt { fn ext_name(&self) -> &str; - fn apply(&self, delta: &TextOperations, interval: Interval, attribute: &AttributeEntry) -> Option; + fn apply( + &self, + delta: &DeltaTextOperations, + interval: Interval, + attribute: &AttributeEntry, + ) -> Option; } pub trait DeleteExt { fn ext_name(&self) -> &str; - fn apply(&self, delta: &TextOperations, interval: Interval) -> Option; + fn apply(&self, delta: &DeltaTextOperations, interval: Interval) -> Option; } diff --git a/shared-lib/flowy-sync/src/client_document/history.rs b/shared-lib/flowy-sync/src/client_document/history.rs index 4d08a8238d..07930f3973 100644 --- a/shared-lib/flowy-sync/src/client_document/history.rs +++ b/shared-lib/flowy-sync/src/client_document/history.rs @@ -1,18 +1,18 @@ -use lib_ot::text_delta::TextOperations; +use lib_ot::text_delta::DeltaTextOperations; const MAX_UNDOES: usize = 20; #[derive(Debug, Clone)] pub struct UndoResult { - pub operations: TextOperations, + pub operations: DeltaTextOperations, } #[derive(Debug, Clone)] pub struct History { #[allow(dead_code)] cur_undo: usize, - undoes: Vec, - redoes: Vec, + undoes: Vec, + redoes: Vec, capacity: usize, } @@ -40,15 +40,15 @@ impl History { !self.redoes.is_empty() } - pub fn add_undo(&mut self, delta: TextOperations) { + pub fn add_undo(&mut self, delta: DeltaTextOperations) { self.undoes.push(delta); } - pub fn add_redo(&mut self, delta: TextOperations) { + pub fn add_redo(&mut self, delta: DeltaTextOperations) { self.redoes.push(delta); } - pub fn record(&mut self, delta: TextOperations) { + pub fn record(&mut self, delta: DeltaTextOperations) { if delta.ops.is_empty() { return; } @@ -61,7 +61,7 @@ impl History { } } - pub fn undo(&mut self) -> Option { + pub fn undo(&mut self) -> Option { if !self.can_undo() { return None; } @@ -69,7 +69,7 @@ impl History { Some(delta) } - pub fn redo(&mut self) -> Option { + pub fn redo(&mut self) -> Option { if !self.can_redo() { return None; } diff --git a/shared-lib/flowy-sync/src/client_document/mod.rs b/shared-lib/flowy-sync/src/client_document/mod.rs index 7e52b8f9e8..d571d5447b 100644 --- a/shared-lib/flowy-sync/src/client_document/mod.rs +++ b/shared-lib/flowy-sync/src/client_document/mod.rs @@ -4,7 +4,6 @@ pub use document_pad::*; pub(crate) use extensions::*; pub use view::*; -pub mod default; mod document_pad; mod extensions; pub mod history; diff --git a/shared-lib/flowy-sync/src/client_document/view.rs b/shared-lib/flowy-sync/src/client_document/view.rs index 7c06e36bb8..e009f1ae45 100644 --- a/shared-lib/flowy-sync/src/client_document/view.rs +++ b/shared-lib/flowy-sync/src/client_document/view.rs @@ -3,7 +3,7 @@ use lib_ot::core::AttributeEntry; use lib_ot::{ core::{trim, Interval}, errors::{ErrorBuilder, OTError, OTErrorCode}, - text_delta::TextOperations, + text_delta::DeltaTextOperations, }; pub const RECORD_THRESHOLD: usize = 400; // in milliseconds @@ -25,10 +25,10 @@ impl ViewExtensions { pub(crate) fn insert( &self, - operations: &TextOperations, + operations: &DeltaTextOperations, text: &str, interval: Interval, - ) -> Result { + ) -> Result { let mut new_operations = None; for ext in &self.insert_exts { if let Some(mut operations) = ext.apply(operations, interval.size(), text, interval.start) { @@ -45,7 +45,11 @@ impl ViewExtensions { } } - pub(crate) fn delete(&self, delta: &TextOperations, interval: Interval) -> Result { + pub(crate) fn delete( + &self, + delta: &DeltaTextOperations, + interval: Interval, + ) -> Result { let mut new_delta = None; for ext in &self.delete_exts { if let Some(mut delta) = ext.apply(delta, interval) { @@ -64,10 +68,10 @@ impl ViewExtensions { pub(crate) fn format( &self, - operations: &TextOperations, + operations: &DeltaTextOperations, attribute: AttributeEntry, interval: Interval, - ) -> Result { + ) -> Result { let mut new_operations = None; for ext in &self.format_exts { if let Some(mut operations) = ext.apply(operations, interval, &attribute) { diff --git a/shared-lib/flowy-sync/src/client_folder/folder_pad.rs b/shared-lib/flowy-sync/src/client_folder/folder_pad.rs index 6b3e27a9b2..2c67900fa3 100644 --- a/shared-lib/flowy-sync/src/client_folder/folder_pad.rs +++ b/shared-lib/flowy-sync/src/client_folder/folder_pad.rs @@ -1,5 +1,5 @@ use crate::errors::internal_error; -use crate::server_folder::FolderOperations; +use crate::server_folder::{FolderOperations, FolderOperationsBuilder}; use crate::util::cal_diff; use crate::{ client_folder::builder::FolderPadBuilder, @@ -12,8 +12,6 @@ use lib_ot::core::*; use serde::Deserialize; use std::sync::Arc; -pub type FolderOperationsBuilder = DeltaBuilder; - #[derive(Debug, Clone, Eq, PartialEq)] pub struct FolderPad { folder_rev: FolderRevision, @@ -464,15 +462,13 @@ pub struct FolderChangeset { mod tests { #![allow(clippy::all)] use crate::client_folder::folder_pad::FolderPad; + use crate::server_folder::{FolderOperations, FolderOperationsBuilder}; use chrono::Utc; - use serde::Deserialize; - - use crate::client_folder::FolderOperationsBuilder; - use crate::server_folder::FolderOperations; use flowy_folder_data_model::revision::{ AppRevision, FolderRevision, TrashRevision, ViewRevision, WorkspaceRevision, }; use lib_ot::core::OperationTransform; + use serde::Deserialize; #[test] fn folder_add_workspace() { diff --git a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs index 1e1a68a136..f27c46d250 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs @@ -7,11 +7,11 @@ use flowy_grid_data_model::revision::{ GridRevision, }; use lib_infra::util::move_vec_element; -use lib_ot::core::{DeltaBuilder, DeltaOperations, EmptyAttributes, OperationTransform}; +use lib_ot::core::{DeltaOperationBuilder, DeltaOperations, EmptyAttributes, OperationTransform}; use std::collections::HashMap; use std::sync::Arc; pub type GridOperations = DeltaOperations; -pub type GridOperationsBuilder = DeltaBuilder; +pub type GridOperationsBuilder = DeltaOperationBuilder; pub struct GridRevisionPad { grid_rev: Arc, diff --git a/shared-lib/flowy-sync/src/entities/document.rs b/shared-lib/flowy-sync/src/entities/document.rs index f351d95677..1751bf70a4 100644 --- a/shared-lib/flowy-sync/src/entities/document.rs +++ b/shared-lib/flowy-sync/src/entities/document.rs @@ -3,7 +3,7 @@ use crate::{ errors::CollaborateError, }; use flowy_derive::ProtoBuf; -use lib_ot::text_delta::TextOperations; +use lib_ot::text_delta::DeltaTextOperations; #[derive(ProtoBuf, Default, Debug, Clone)] pub struct CreateDocumentParams { @@ -38,7 +38,7 @@ impl std::convert::TryFrom for DocumentPayloadPB { .context("Revision's rev_id should be 0 when creating the document")); } - let delta = TextOperations::from_bytes(&revision.bytes)?; + let delta = DeltaTextOperations::from_bytes(&revision.bytes)?; let doc_json = delta.json_str(); Ok(DocumentPayloadPB { @@ -59,27 +59,6 @@ pub struct ResetDocumentParams { pub revisions: RepeatedRevision, } -#[derive(ProtoBuf, Default, Debug, Clone)] -pub struct DocumentOperationsPB { - #[pb(index = 1)] - pub doc_id: String, - - #[pb(index = 2)] - pub operations_str: String, -} - -#[derive(ProtoBuf, Default, Debug, Clone)] -pub struct NewDocUserPB { - #[pb(index = 1)] - pub user_id: String, - - #[pb(index = 2)] - pub rev_id: i64, - - #[pb(index = 3)] - pub doc_id: String, -} - #[derive(ProtoBuf, Default, Debug, Clone)] pub struct DocumentIdPB { #[pb(index = 1)] diff --git a/shared-lib/flowy-sync/src/entities/revision.rs b/shared-lib/flowy-sync/src/entities/revision.rs index 4f7cc7a148..39fb71a2e6 100644 --- a/shared-lib/flowy-sync/src/entities/revision.rs +++ b/shared-lib/flowy-sync/src/entities/revision.rs @@ -2,7 +2,7 @@ use bytes::Bytes; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use std::{convert::TryFrom, fmt::Formatter, ops::RangeInclusive}; -pub type RevisionObject = lib_ot::text_delta::TextOperations; +pub type RevisionObject = lib_ot::text_delta::DeltaTextOperations; #[derive(PartialEq, Eq, Clone, Default, ProtoBuf)] pub struct Revision { diff --git a/shared-lib/flowy-sync/src/lib.rs b/shared-lib/flowy-sync/src/lib.rs index b21023c903..1746d20aad 100644 --- a/shared-lib/flowy-sync/src/lib.rs +++ b/shared-lib/flowy-sync/src/lib.rs @@ -9,4 +9,4 @@ pub mod server_folder; pub mod synchronizer; pub mod util; -pub use lib_ot::text_delta::TextOperations; +pub use lib_ot::text_delta::DeltaTextOperations; diff --git a/shared-lib/flowy-sync/src/server_document/document_manager.rs b/shared-lib/flowy-sync/src/server_document/document_manager.rs index f6851e2d66..4d909b50f7 100644 --- a/shared-lib/flowy-sync/src/server_document/document_manager.rs +++ b/shared-lib/flowy-sync/src/server_document/document_manager.rs @@ -12,7 +12,7 @@ use dashmap::DashMap; use futures::stream::StreamExt; use lib_infra::future::BoxResultFuture; use lib_ot::core::AttributeHashMap; -use lib_ot::text_delta::TextOperations; +use lib_ot::text_delta::DeltaTextOperations; use std::{collections::HashMap, fmt::Debug, sync::Arc}; use tokio::{ sync::{mpsc, oneshot, RwLock}, @@ -216,7 +216,7 @@ impl OpenDocumentHandler { let (sender, receiver) = mpsc::channel(1000); let users = DashMap::new(); - let operations = TextOperations::from_bytes(&doc.content)?; + let operations = DeltaTextOperations::from_bytes(&doc.content)?; let sync_object = ServerDocument::from_operations(&doc_id, operations); let synchronizer = Arc::new(DocumentRevisionSynchronizer::new(doc.rev_id, sync_object, persistence)); diff --git a/shared-lib/flowy-sync/src/server_document/document_pad.rs b/shared-lib/flowy-sync/src/server_document/document_pad.rs index ff2b567955..46e82e4fa7 100644 --- a/shared-lib/flowy-sync/src/server_document/document_pad.rs +++ b/shared-lib/flowy-sync/src/server_document/document_pad.rs @@ -1,20 +1,20 @@ use crate::synchronizer::RevisionOperations; use crate::{client_document::InitialDocument, errors::CollaborateError, synchronizer::RevisionSyncObject}; -use lib_ot::{core::*, text_delta::TextOperations}; +use lib_ot::{core::*, text_delta::DeltaTextOperations}; pub struct ServerDocument { document_id: String, - operations: TextOperations, + operations: DeltaTextOperations, } impl ServerDocument { #[allow(dead_code)] pub fn new(doc_id: &str) -> Self { - let operations = TextOperations::from_json(&C::json_str()).unwrap(); + let operations = DeltaTextOperations::from_json(&C::json_str()).unwrap(); Self::from_operations(doc_id, operations) } - pub fn from_operations(document_id: &str, operations: TextOperations) -> Self { + pub fn from_operations(document_id: &str, operations: DeltaTextOperations) -> Self { let document_id = document_id.to_owned(); ServerDocument { document_id, @@ -32,13 +32,16 @@ impl RevisionSyncObject for ServerDocument { self.operations.json_str() } - fn compose(&mut self, other: &TextOperations) -> Result<(), CollaborateError> { + fn compose(&mut self, other: &DeltaTextOperations) -> Result<(), CollaborateError> { let operations = self.operations.compose(other)?; self.operations = operations; Ok(()) } - fn transform(&self, other: &TextOperations) -> Result<(TextOperations, TextOperations), CollaborateError> { + fn transform( + &self, + other: &DeltaTextOperations, + ) -> Result<(DeltaTextOperations, DeltaTextOperations), CollaborateError> { let value = self.operations.transform(other)?; Ok(value) } diff --git a/shared-lib/flowy-sync/src/server_folder/folder_pad.rs b/shared-lib/flowy-sync/src/server_folder/folder_pad.rs index f192cc7f3d..b979b95f87 100644 --- a/shared-lib/flowy-sync/src/server_folder/folder_pad.rs +++ b/shared-lib/flowy-sync/src/server_folder/folder_pad.rs @@ -1,9 +1,10 @@ use crate::synchronizer::{RevisionOperations, RevisionSynchronizer}; use crate::{errors::CollaborateError, synchronizer::RevisionSyncObject}; -use lib_ot::core::{DeltaOperations, EmptyAttributes, OperationTransform}; +use lib_ot::core::{DeltaOperationBuilder, DeltaOperations, EmptyAttributes, OperationTransform}; pub type FolderRevisionSynchronizer = RevisionSynchronizer; pub type FolderOperations = DeltaOperations; +pub type FolderOperationsBuilder = DeltaOperationBuilder; pub struct ServerFolder { folder_id: String, diff --git a/shared-lib/flowy-sync/src/util.rs b/shared-lib/flowy-sync/src/util.rs index 225598c752..10e977919f 100644 --- a/shared-lib/flowy-sync/src/util.rs +++ b/shared-lib/flowy-sync/src/util.rs @@ -8,10 +8,10 @@ use crate::{ errors::{CollaborateError, CollaborateResult}, }; use dissimilar::Chunk; -use lib_ot::core::{OTString, OperationAttributes, OperationBuilder}; +use lib_ot::core::{DeltaOperationBuilder, OTString, OperationAttributes}; use lib_ot::{ core::{DeltaOperations, OperationTransform, NEW_LINE, WHITESPACE}, - text_delta::TextOperations, + text_delta::DeltaTextOperations, }; use serde::de::DeserializeOwned; use std::sync::atomic::{AtomicI64, Ordering::SeqCst}; @@ -155,7 +155,7 @@ pub fn make_document_from_revision_pbs( return Ok(None); } - let mut delta = TextOperations::new(); + let mut delta = DeltaTextOperations::new(); let mut base_rev_id = 0; let mut rev_id = 0; for revision in revisions { @@ -166,7 +166,7 @@ pub fn make_document_from_revision_pbs( tracing::warn!("revision delta_data is empty"); } - let new_delta = TextOperations::from_bytes(revision.bytes)?; + let new_delta = DeltaTextOperations::from_bytes(revision.bytes)?; delta = delta.compose(&new_delta)?; } @@ -191,7 +191,7 @@ pub fn rev_id_from_str(s: &str) -> Result { pub fn cal_diff(old: String, new: String) -> Option> { let chunks = dissimilar::diff(&old, &new); - let mut delta_builder = OperationBuilder::::new(); + let mut delta_builder = DeltaOperationBuilder::::new(); for chunk in &chunks { match chunk { Chunk::Equal(s) => { diff --git a/shared-lib/lib-ot/Cargo.toml b/shared-lib/lib-ot/Cargo.toml index a2fdf443bb..c654a00235 100644 --- a/shared-lib/lib-ot/Cargo.toml +++ b/shared-lib/lib-ot/Cargo.toml @@ -17,6 +17,7 @@ thiserror = "1.0" serde_json = { version = "1.0" } serde_repr = { version = "0.1" } derive_more = { version = "0.99", features = ["display"] } +indexmap = {version = "1.9.1", features = ["serde"]} log = "0.4" tracing = { version = "0.1", features = ["log"] } lazy_static = "1.4.0" diff --git a/shared-lib/lib-ot/src/core/attributes/attribute.rs b/shared-lib/lib-ot/src/core/attributes/attribute.rs index 1f75b5ac08..0acabc1209 100644 --- a/shared-lib/lib-ot/src/core/attributes/attribute.rs +++ b/shared-lib/lib-ot/src/core/attributes/attribute.rs @@ -1,7 +1,7 @@ use crate::core::{OperationAttributes, OperationTransform}; use crate::errors::OTError; +use indexmap::IndexMap; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use std::fmt; use std::fmt::Display; @@ -27,10 +27,10 @@ impl std::convert::From for AttributeHashMap { } #[derive(Default, Clone, Serialize, Deserialize, Eq, PartialEq, Debug)] -pub struct AttributeHashMap(HashMap); +pub struct AttributeHashMap(IndexMap); impl std::ops::Deref for AttributeHashMap { - type Target = HashMap; + type Target = IndexMap; fn deref(&self) -> &Self::Target { &self.0 @@ -45,16 +45,16 @@ impl std::ops::DerefMut for AttributeHashMap { impl AttributeHashMap { pub fn new() -> AttributeHashMap { - AttributeHashMap(HashMap::new()) + AttributeHashMap(IndexMap::new()) } - pub fn into_inner(self) -> HashMap { + pub fn into_inner(self) -> IndexMap { self.0 } - pub fn from_value(attribute_map: HashMap) -> Self { - Self(attribute_map) - } + // pub fn from_value(attribute_map: HashMap) -> Self { + // Self(attribute_map) + // } pub fn insert>(&mut self, key: K, value: V) { self.0.insert(key.to_string(), value.into()); @@ -219,10 +219,10 @@ impl AttributeValue { } pub fn from_bool(val: bool) -> Self { - let value = if val { Some(val.to_string()) } else { None }; + // let value = if val { Some(val.to_string()) } else { None }; Self { ty: Some(ValueType::BoolType), - value, + value: Some(val.to_string()), } } pub fn from_string(s: &str) -> Self { diff --git a/shared-lib/lib-ot/src/core/delta/builder.rs b/shared-lib/lib-ot/src/core/delta/builder.rs index 624be9b287..75014e1ec1 100644 --- a/shared-lib/lib-ot/src/core/delta/builder.rs +++ b/shared-lib/lib-ot/src/core/delta/builder.rs @@ -16,11 +16,11 @@ use crate::core::DeltaOperation; /// .build(); /// assert_eq!(delta.content().unwrap(), "AppFlowy"); /// ``` -pub struct OperationBuilder { +pub struct DeltaOperationBuilder { delta: DeltaOperations, } -impl std::default::Default for OperationBuilder +impl std::default::Default for DeltaOperationBuilder where T: OperationAttributes, { @@ -31,16 +31,16 @@ where } } -impl OperationBuilder +impl DeltaOperationBuilder where T: OperationAttributes, { pub fn new() -> Self { - OperationBuilder::default() + DeltaOperationBuilder::default() } pub fn from_operations(operations: Vec>) -> DeltaOperations { - let mut delta = OperationBuilder::default().build(); + let mut delta = DeltaOperationBuilder::default().build(); operations.into_iter().for_each(|operation| { delta.add(operation); }); @@ -52,10 +52,10 @@ where /// # Examples /// /// ``` - /// use lib_ot::text_delta::{BuildInTextAttribute, TextOperations, TextOperationBuilder}; + /// use lib_ot::text_delta::{BuildInTextAttribute, DeltaTextOperations, DeltaTextOperationBuilder}; /// /// let mut attribute = BuildInTextAttribute::Bold(true); - /// let delta = TextOperationBuilder::new().retain_with_attributes(7, attribute.into()).build(); + /// let delta = DeltaTextOperationBuilder::new().retain_with_attributes(7, attribute.into()).build(); /// /// assert_eq!(delta.json_str(), r#"[{"retain":7,"attributes":{"bold":true}}]"#); /// ``` @@ -111,14 +111,14 @@ where /// /// ``` /// use lib_ot::core::{OperationTransform, DeltaBuilder}; - /// use lib_ot::text_delta::{BuildInTextAttribute, TextOperationBuilder}; + /// use lib_ot::text_delta::{BuildInTextAttribute, DeltaTextOperationBuilder}; /// let delta = DeltaBuilder::new() /// .retain(3) /// .trim() /// .build(); /// assert_eq!(delta.ops.len(), 0); /// - /// let delta = TextOperationBuilder::new() + /// let delta = DeltaTextOperationBuilder::new() /// .retain_with_attributes(3, BuildInTextAttribute::Bold(true).into()) /// .trim() /// .build(); diff --git a/shared-lib/lib-ot/src/core/delta/cursor.rs b/shared-lib/lib-ot/src/core/delta/cursor.rs index de2ae4304e..42c28dae10 100644 --- a/shared-lib/lib-ot/src/core/delta/cursor.rs +++ b/shared-lib/lib-ot/src/core/delta/cursor.rs @@ -30,8 +30,8 @@ where /// /// ``` /// use lib_ot::core::{OperationsCursor, OperationIterator, Interval, DeltaOperation}; - /// use lib_ot::text_delta::TextOperations; - /// let mut delta = TextOperations::default(); + /// use lib_ot::text_delta::DeltaTextOperations; + /// let mut delta = DeltaTextOperations::default(); /// delta.add(DeltaOperation::insert("123")); /// delta.add(DeltaOperation::insert("4")); /// diff --git a/shared-lib/lib-ot/src/core/delta/iterator.rs b/shared-lib/lib-ot/src/core/delta/iterator.rs index bd4d3ee55a..c0b3aaa3cb 100644 --- a/shared-lib/lib-ot/src/core/delta/iterator.rs +++ b/shared-lib/lib-ot/src/core/delta/iterator.rs @@ -15,8 +15,8 @@ pub(crate) const MAX_IV_LEN: usize = i32::MAX as usize; /// /// ``` /// use lib_ot::core::{OperationIterator, Interval, DeltaOperation}; -/// use lib_ot::text_delta::TextOperations; -/// let mut delta = TextOperations::default(); +/// use lib_ot::text_delta::DeltaTextOperations; +/// let mut delta = DeltaTextOperations::default(); /// delta.add(DeltaOperation::insert("123")); /// delta.add(DeltaOperation::insert("4")); /// assert_eq!( diff --git a/shared-lib/lib-ot/src/core/delta/ops.rs b/shared-lib/lib-ot/src/core/delta/ops.rs index df697f845e..32bca15859 100644 --- a/shared-lib/lib-ot/src/core/delta/ops.rs +++ b/shared-lib/lib-ot/src/core/delta/ops.rs @@ -1,10 +1,9 @@ -use crate::errors::{ErrorBuilder, OTError, OTErrorCode}; - use crate::core::delta::operation::{DeltaOperation, EmptyAttributes, OperationAttributes, OperationTransform}; use crate::core::delta::{OperationIterator, MAX_IV_LEN}; use crate::core::interval::Interval; use crate::core::ot_str::OTString; -use crate::core::OperationBuilder; +use crate::core::DeltaOperationBuilder; +use crate::errors::{ErrorBuilder, OTError, OTErrorCode}; use bytes::Bytes; use serde::de::DeserializeOwned; use std::{ @@ -15,8 +14,7 @@ use std::{ str::FromStr, }; -pub type Delta = DeltaOperations; -pub type DeltaBuilder = OperationBuilder; +pub type DeltaBuilder = DeltaOperationBuilder; /// A [Delta] contains list of operations that consists of 'Retain', 'Delete' and 'Insert' operation. /// Check out the [Operation] for more details. It describes the document as a sequence of @@ -571,12 +569,12 @@ where /// # Examples /// /// ``` - /// use lib_ot::core::OperationBuilder; - /// use lib_ot::text_delta::{TextOperations}; + /// use lib_ot::core::DeltaOperationBuilder; + /// use lib_ot::text_delta::{DeltaTextOperations}; /// let json = r#"[ /// {"retain":7,"attributes":{"bold":null}} /// ]"#; - /// let delta = TextOperations::from_json(json).unwrap(); + /// let delta = DeltaTextOperations::from_json(json).unwrap(); /// assert_eq!(delta.json_str(), r#"[{"retain":7,"attributes":{"bold":null}}]"#); /// ``` pub fn from_json(json: &str) -> Result { diff --git a/shared-lib/lib-ot/src/core/node_tree/mod.rs b/shared-lib/lib-ot/src/core/node_tree/mod.rs index 417c4af03f..a05819bbe3 100644 --- a/shared-lib/lib-ot/src/core/node_tree/mod.rs +++ b/shared-lib/lib-ot/src/core/node_tree/mod.rs @@ -3,7 +3,6 @@ mod node; mod node_serde; mod operation; -mod operation_serde; mod path; mod transaction; mod transaction_serde; diff --git a/shared-lib/lib-ot/src/core/node_tree/node.rs b/shared-lib/lib-ot/src/core/node_tree/node.rs index 23c342d631..556d3617bb 100644 --- a/shared-lib/lib-ot/src/core/node_tree/node.rs +++ b/shared-lib/lib-ot/src/core/node_tree/node.rs @@ -3,7 +3,7 @@ use crate::core::attributes::{AttributeHashMap, AttributeKey, AttributeValue}; use crate::core::Body::Delta; use crate::core::OperationTransform; use crate::errors::OTError; -use crate::text_delta::TextOperations; +use crate::text_delta::DeltaTextOperations; use serde::{Deserialize, Serialize}; #[derive(Default, Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] @@ -97,7 +97,7 @@ impl NodeDataBuilder { #[derive(Debug, Clone, PartialEq, Eq)] pub enum Body { Empty, - Delta(TextOperations), + Delta(DeltaTextOperations), } impl std::default::Default for Body { @@ -120,6 +120,7 @@ impl OperationTransform for Body { { match (self, other) { (Delta(a), Delta(b)) => a.compose(b).map(Delta), + (Body::Empty, Delta(b)) => Ok(Delta(b.clone())), (Body::Empty, Body::Empty) => Ok(Body::Empty), (l, r) => { let msg = format!("{:?} can not compose {:?}", l, r); @@ -163,8 +164,8 @@ impl OperationTransform for Body { #[serde(rename_all = "snake_case")] pub enum Changeset { Delta { - delta: TextOperations, - inverted: TextOperations, + delta: DeltaTextOperations, + inverted: DeltaTextOperations, }, Attributes { new: AttributeHashMap, diff --git a/shared-lib/lib-ot/src/core/node_tree/node_serde.rs b/shared-lib/lib-ot/src/core/node_tree/node_serde.rs index df754d2620..8e360b8937 100644 --- a/shared-lib/lib-ot/src/core/node_tree/node_serde.rs +++ b/shared-lib/lib-ot/src/core/node_tree/node_serde.rs @@ -1,5 +1,5 @@ use super::Body; -use crate::text_delta::TextOperations; +use crate::text_delta::DeltaTextOperations; use serde::de::{self, MapAccess, Visitor}; use serde::ser::SerializeMap; use serde::{Deserializer, Serializer}; @@ -37,7 +37,7 @@ where where V: MapAccess<'de>, { - let mut delta: Option = None; + let mut delta: Option = None; while let Some(key) = map.next_key()? { match key { "delta" => { diff --git a/shared-lib/lib-ot/src/core/node_tree/operation.rs b/shared-lib/lib-ot/src/core/node_tree/operation.rs index f5388b1847..8698d26d28 100644 --- a/shared-lib/lib-ot/src/core/node_tree/operation.rs +++ b/shared-lib/lib-ot/src/core/node_tree/operation.rs @@ -1,4 +1,3 @@ -use super::operation_serde::{deserialize_changeset, serialize_changeset}; use crate::core::{Changeset, NodeData, Path}; use crate::errors::OTError; use serde::{Deserialize, Serialize}; @@ -11,8 +10,6 @@ pub enum NodeOperation { Insert { path: Path, nodes: Vec }, #[serde(rename = "update")] - #[serde(serialize_with = "serialize_changeset")] - #[serde(deserialize_with = "deserialize_changeset")] Update { path: Path, changeset: Changeset }, #[serde(rename = "delete")] @@ -112,9 +109,15 @@ impl NodeOperations { self.operations } - pub fn add_op(&mut self, operation: NodeOperation) { + pub fn push_op(&mut self, operation: NodeOperation) { self.operations.push(Arc::new(operation)); } + + pub fn extend(&mut self, other: NodeOperations) { + for operation in other.operations { + self.operations.push(operation); + } + } } impl std::ops::Deref for NodeOperations { diff --git a/shared-lib/lib-ot/src/core/node_tree/operation_serde.rs b/shared-lib/lib-ot/src/core/node_tree/operation_serde.rs index 04f7bab7c5..5135ca5d38 100644 --- a/shared-lib/lib-ot/src/core/node_tree/operation_serde.rs +++ b/shared-lib/lib-ot/src/core/node_tree/operation_serde.rs @@ -1,4 +1,4 @@ -use crate::core::{Changeset, Path}; +use crate::core::{AttributeHashMap, Changeset, Path}; use crate::text_delta::TextOperations; use serde::de::{self, MapAccess, Visitor}; use serde::ser::SerializeMap; @@ -7,6 +7,7 @@ use std::convert::TryInto; use std::fmt; use std::marker::PhantomData; +#[allow(dead_code)] pub fn serialize_changeset(path: &Path, changeset: &Changeset, serializer: S) -> Result where S: Serializer, @@ -33,6 +34,7 @@ where } } +#[allow(dead_code)] pub fn deserialize_changeset<'de, D>(deserializer: D) -> Result<(Path, Changeset), D::Error> where D: Deserializer<'de>, @@ -53,6 +55,7 @@ where { let mut path: Option = None; let mut delta_changeset = DeltaChangeset::::new(); + let mut attribute_changeset = AttributeChangeset::new(); while let Some(key) = map.next_key()? { match key { "delta" => { @@ -73,8 +76,21 @@ where } path = Some(map.next_value::()?) } + "new" => { + if attribute_changeset.new.is_some() { + return Err(de::Error::duplicate_field("new")); + } + attribute_changeset.new = Some(map.next_value()?); + } + "old" => { + if attribute_changeset.old.is_some() { + return Err(de::Error::duplicate_field("old")); + } + attribute_changeset.old = Some(map.next_value()?); + } other => { - panic!("Unexpected key: {}", other); + tracing::warn!("Unexpected key: {}", other); + panic!() } } } @@ -82,7 +98,12 @@ where return Err(de::Error::missing_field("path")); } - let changeset = delta_changeset.try_into()?; + let mut changeset: Changeset; + if !delta_changeset.is_empty() { + changeset = delta_changeset.try_into()? + } else { + changeset = attribute_changeset.try_into()?; + } Ok((path.unwrap(), changeset)) } @@ -104,6 +125,10 @@ impl DeltaChangeset { error: PhantomData, } } + + fn is_empty(&self) -> bool { + self.delta.is_none() && self.inverted.is_none() + } } impl std::convert::TryInto for DeltaChangeset @@ -128,3 +153,44 @@ where Ok(changeset) } } +struct AttributeChangeset { + new: Option, + old: Option, + error: PhantomData, +} + +impl AttributeChangeset { + fn new() -> Self { + Self { + new: Default::default(), + old: Default::default(), + error: PhantomData, + } + } + + fn is_empty(&self) -> bool { + self.new.is_none() && self.old.is_none() + } +} + +impl std::convert::TryInto for AttributeChangeset +where + E: de::Error, +{ + type Error = E; + + fn try_into(self) -> Result { + if self.new.is_none() { + return Err(de::Error::missing_field("new")); + } + + if self.old.is_none() { + return Err(de::Error::missing_field("old")); + } + + Ok(Changeset::Attributes { + new: self.new.unwrap(), + old: self.old.unwrap(), + }) + } +} diff --git a/shared-lib/lib-ot/src/core/node_tree/path.rs b/shared-lib/lib-ot/src/core/node_tree/path.rs index d3fd301753..6963a661f3 100644 --- a/shared-lib/lib-ot/src/core/node_tree/path.rs +++ b/shared-lib/lib-ot/src/core/node_tree/path.rs @@ -31,11 +31,11 @@ impl Path { if self.is_empty() { return false; } - return true; + true } pub fn is_root(&self) -> bool { - return self.0.len() == 1 && self.0[0] == 0; + self.0.len() == 1 && self.0[0] == 0 } } diff --git a/shared-lib/lib-ot/src/core/node_tree/transaction.rs b/shared-lib/lib-ot/src/core/node_tree/transaction.rs index be060831d4..6c0e8bd0b5 100644 --- a/shared-lib/lib-ot/src/core/node_tree/transaction.rs +++ b/shared-lib/lib-ot/src/core/node_tree/transaction.rs @@ -2,18 +2,15 @@ use super::{Changeset, NodeOperations}; use crate::core::attributes::AttributeHashMap; use crate::core::{NodeData, NodeOperation, NodeTree, Path}; use crate::errors::OTError; - use indextree::NodeId; use serde::{Deserialize, Serialize}; use std::sync::Arc; - #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Transaction { #[serde(flatten)] pub operations: NodeOperations, #[serde(default)] - #[serde(flatten)] #[serde(skip_serializing_if = "Extension::is_empty")] pub extension: Extension, } @@ -48,28 +45,31 @@ impl Transaction { self.operations.into_inner() } + pub fn push_operation>(&mut self, operation: T) { + let operation = operation.into(); + self.operations.push_op(operation); + } + /// Make the `other` can be applied to the version after applying the `self` transaction. /// /// The semantics of transform is used when editing conflicts occur, which is often determined by the version id。 /// the operations of the transaction will be transformed into the conflict operations. pub fn transform(&self, other: &Transaction) -> Result { - let mut new_transaction = other.clone(); - new_transaction.extension = self.extension.clone(); - for other_operation in new_transaction.iter_mut() { + let mut other = other.clone(); + other.extension = self.extension.clone(); + for other_operation in other.iter_mut() { let other_operation = Arc::make_mut(other_operation); for operation in self.operations.iter() { operation.transform(other_operation); } } - Ok(new_transaction) + Ok(other) } pub fn compose(&mut self, other: Transaction) -> Result<(), OTError> { // For the moment, just append `other` operations to the end of `self`. let Transaction { operations, extension } = other; - for operation in operations.into_inner().into_iter() { - self.operations.push(operation); - } + self.operations.extend(operations); self.extension = extension; Ok(()) } @@ -199,7 +199,7 @@ impl<'a> TransactionBuilder<'a> { } } - self.operations.add_op(NodeOperation::Update { + self.operations.push_op(NodeOperation::Update { path: path.clone(), changeset: Changeset::Attributes { new: attributes, @@ -215,7 +215,7 @@ impl<'a> TransactionBuilder<'a> { pub fn update_body_at_path(mut self, path: &Path, changeset: Changeset) -> Self { match self.node_tree.node_id_at_path(path) { Some(_) => { - self.operations.add_op(NodeOperation::Update { + self.operations.push_op(NodeOperation::Update { path: path.clone(), changeset, }); @@ -243,7 +243,7 @@ impl<'a> TransactionBuilder<'a> { node_id = self.node_tree.following_siblings(node_id).next().unwrap(); } - self.operations.add_op(NodeOperation::Delete { + self.operations.push_op(NodeOperation::Delete { path: path.clone(), nodes: deleted_nodes, }); @@ -270,7 +270,7 @@ impl<'a> TransactionBuilder<'a> { } pub fn push(mut self, op: NodeOperation) -> Self { - self.operations.add_op(op); + self.operations.push_op(op); self } diff --git a/shared-lib/lib-ot/src/core/node_tree/tree.rs b/shared-lib/lib-ot/src/core/node_tree/tree.rs index 2a4fce4c14..2028451ad7 100644 --- a/shared-lib/lib-ot/src/core/node_tree/tree.rs +++ b/shared-lib/lib-ot/src/core/node_tree/tree.rs @@ -38,7 +38,8 @@ impl NodeTree { Self::from_operations(operations, context) } - pub fn from_operations(operations: NodeOperations, context: NodeTreeContext) -> Result { + pub fn from_operations>(operations: T, context: NodeTreeContext) -> Result { + let operations = operations.into(); let mut node_tree = NodeTree::new(context); for operation in operations.into_inner().into_iter() { let _ = node_tree.apply_op(operation)?; @@ -46,6 +47,13 @@ impl NodeTree { Ok(node_tree) } + pub fn from_transaction>(transaction: T, context: NodeTreeContext) -> Result { + let transaction = transaction.into(); + let mut tree = Self::new(context); + let _ = tree.apply_transaction(transaction)?; + Ok(tree) + } + pub fn get_node(&self, node_id: NodeId) -> Option<&Node> { if node_id.is_removed(&self.arena) { return None; diff --git a/shared-lib/lib-ot/src/errors.rs b/shared-lib/lib-ot/src/errors.rs index 8786cdedfb..92dc116fb0 100644 --- a/shared-lib/lib-ot/src/errors.rs +++ b/shared-lib/lib-ot/src/errors.rs @@ -42,7 +42,7 @@ impl OTError { impl fmt::Display for OTError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "incompatible lengths") + write!(f, "{:?}: {}", self.code, self.msg) } } diff --git a/shared-lib/lib-ot/src/text_delta/attributes.rs b/shared-lib/lib-ot/src/text_delta/attributes.rs index b695d8a805..574130b214 100644 --- a/shared-lib/lib-ot/src/text_delta/attributes.rs +++ b/shared-lib/lib-ot/src/text_delta/attributes.rs @@ -1,6 +1,6 @@ #![allow(non_snake_case)] use crate::core::{AttributeEntry, AttributeHashMap, AttributeKey}; -use crate::text_delta::TextOperation; +use crate::text_delta::DeltaTextOperation; use crate::{inline_attribute_entry, inline_list_attribute_entry}; use lazy_static::lazy_static; use std::str::FromStr; @@ -12,7 +12,7 @@ pub fn empty_attributes() -> AttributeHashMap { AttributeHashMap::default() } -pub fn attributes_except_header(op: &TextOperation) -> AttributeHashMap { +pub fn attributes_except_header(op: &DeltaTextOperation) -> AttributeHashMap { let mut attributes = op.get_attributes(); attributes.remove_key(BuildInTextAttributeKey::Header); attributes diff --git a/shared-lib/lib-ot/src/text_delta/delta.rs b/shared-lib/lib-ot/src/text_delta/delta.rs index 526a27713d..dcb98f80cc 100644 --- a/shared-lib/lib-ot/src/text_delta/delta.rs +++ b/shared-lib/lib-ot/src/text_delta/delta.rs @@ -1,8 +1,8 @@ -use crate::core::{AttributeHashMap, DeltaOperation, DeltaOperations, OperationBuilder}; +use crate::core::{AttributeHashMap, DeltaOperation, DeltaOperationBuilder, DeltaOperations}; -pub type TextOperations = DeltaOperations; -pub type TextOperationBuilder = OperationBuilder; -pub type TextOperation = DeltaOperation; +pub type DeltaTextOperations = DeltaOperations; +pub type DeltaTextOperationBuilder = DeltaOperationBuilder; +pub type DeltaTextOperation = DeltaOperation; // pub trait TextOperation2: Default + Debug + OperationTransform {} // diff --git a/shared-lib/lib-ot/tests/node/operation_test.rs b/shared-lib/lib-ot/tests/node/operation_test.rs index e62b166b5d..bed011d918 100644 --- a/shared-lib/lib-ot/tests/node/operation_test.rs +++ b/shared-lib/lib-ot/tests/node/operation_test.rs @@ -103,7 +103,7 @@ fn operation_insert_with_multiple_level_path_test() { }, AssertNode { path: 2.into(), - expected: Some(node_data_3.into()), + expected: Some(node_data_3), }, ]; test.run_scripts(scripts); diff --git a/shared-lib/lib-ot/tests/node/script.rs b/shared-lib/lib-ot/tests/node/script.rs index a1f6a5ad99..ec8f101293 100644 --- a/shared-lib/lib-ot/tests/node/script.rs +++ b/shared-lib/lib-ot/tests/node/script.rs @@ -3,7 +3,7 @@ use lib_ot::core::{NodeTreeContext, Transaction}; use lib_ot::{ core::attributes::AttributeHashMap, core::{Body, Changeset, NodeData, NodeTree, Path, TransactionBuilder}, - text_delta::TextOperations, + text_delta::DeltaTextOperations, }; use std::collections::HashMap; @@ -48,7 +48,7 @@ pub enum NodeScript { }, AssertNodeDelta { path: Path, - expected: TextOperations, + expected: DeltaTextOperations, }, #[allow(dead_code)] AssertTreeJSON { diff --git a/shared-lib/lib-ot/tests/node/serde_test.rs b/shared-lib/lib-ot/tests/node/serde_test.rs index c9c20afdc6..9d4c9a8d70 100644 --- a/shared-lib/lib-ot/tests/node/serde_test.rs +++ b/shared-lib/lib-ot/tests/node/serde_test.rs @@ -1,8 +1,7 @@ use lib_ot::core::{ - AttributeBuilder, Changeset, Extension, NodeData, NodeDataBuilder, NodeOperation, NodeTree, Path, Selection, - Transaction, + AttributeBuilder, Changeset, NodeData, NodeDataBuilder, NodeOperation, NodeTree, Path, Transaction, }; -use lib_ot::text_delta::TextOperationBuilder; +use lib_ot::text_delta::DeltaTextOperationBuilder; #[test] fn operation_insert_node_serde_test() { @@ -42,13 +41,13 @@ fn operation_update_node_attributes_serde_test() { let result = serde_json::to_string(&operation).unwrap(); assert_eq!( result, - r#"{"op":"update","path":[0,1],"new":{"bold":true},"old":{"bold":null}}"# + r#"{"op":"update","path":[0,1],"changeset":{"attributes":{"new":{"bold":true},"old":{"bold":false}}}}"# ); } #[test] fn operation_update_node_body_serialize_test() { - let delta = TextOperationBuilder::new().insert("AppFlowy...").build(); + let delta = DeltaTextOperationBuilder::new().insert("AppFlowy...").build(); let inverted = delta.invert_str(""); let changeset = Changeset::Delta { delta, inverted }; let insert = NodeOperation::Update { @@ -58,13 +57,13 @@ fn operation_update_node_body_serialize_test() { let result = serde_json::to_string(&insert).unwrap(); assert_eq!( result, - r#"{"op":"update","path":[0,1],"delta":[{"insert":"AppFlowy..."}],"inverted":[{"delete":11}]}"# + r#"{"op":"update","path":[0,1],"changeset":{"delta":{"delta":[{"insert":"AppFlowy..."}],"inverted":[{"delete":11}]}}}"# ); } #[test] fn operation_update_node_body_deserialize_test() { - let json_1 = r#"{"op":"update","path":[0,1],"delta":[{"insert":"AppFlowy..."}],"inverted":[{"delete":11}]}"#; + let json_1 = r#"{"op":"update","path":[0,1],"changeset":{"delta":{"delta":[{"insert":"AppFlowy..."}],"inverted":[{"delete":11}]}}}"#; let operation: NodeOperation = serde_json::from_str(json_1).unwrap(); let json_2 = serde_json::to_string(&operation).unwrap(); assert_eq!(json_1, json_2); @@ -76,15 +75,11 @@ fn transaction_serialize_test() { path: Path(vec![0, 1]), nodes: vec![NodeData::new("text".to_owned())], }; - let mut transaction = Transaction::from_operations(vec![insert]); - transaction.extension = Extension::TextSelection { - before_selection: Selection::default(), - after_selection: Selection::default(), - }; + let transaction = Transaction::from_operations(vec![insert]); let json = serde_json::to_string(&transaction).unwrap(); assert_eq!( json, - r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}],"TextSelection":{"before_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}},"after_selection":{"start":{"path":[],"offset":0},"end":{"path":[],"offset":0}}}}"# + r#"{"operations":[{"op":"insert","path":[0,1],"nodes":[{"type":"text"}]}]}"# ); } diff --git a/shared-lib/lib-ot/tests/node/tree_test.rs b/shared-lib/lib-ot/tests/node/tree_test.rs index de4c551e51..b508e194f6 100644 --- a/shared-lib/lib-ot/tests/node/tree_test.rs +++ b/shared-lib/lib-ot/tests/node/tree_test.rs @@ -4,7 +4,7 @@ use lib_ot::core::Body; use lib_ot::core::Changeset; use lib_ot::core::OperationTransform; use lib_ot::core::{NodeData, NodeDataBuilder, Path}; -use lib_ot::text_delta::{TextOperationBuilder, TextOperations}; +use lib_ot::text_delta::{DeltaTextOperationBuilder, DeltaTextOperations}; #[test] fn node_insert_test() { @@ -64,10 +64,10 @@ fn node_insert_out_of_bound_test() { let image_a = NodeData::new("image_a"); let image_b = NodeData::new("image_b"); let image = NodeDataBuilder::new("image_1") - .add_node_data(image_a.clone()) - .add_node_data(image_b.clone()) + .add_node_data(image_a) + .add_node_data(image_b) .build(); - let text_node = NodeDataBuilder::new("text_1").add_node_data(image.clone()).build(); + let text_node = NodeDataBuilder::new("text_1").add_node_data(image).build(); let image_c = NodeData::new("image_c"); let scripts = vec![ @@ -309,19 +309,19 @@ fn node_insert_nested_nodes_test() { // 1:text_2_2 AssertNode { path: vec![0, 0].into(), - expected: Some(node_data_1_1.into()), + expected: Some(node_data_1_1), }, AssertNode { path: vec![0, 1].into(), - expected: Some(node_data_1_2.into()), + expected: Some(node_data_1_2), }, AssertNode { path: vec![1, 0].into(), - expected: Some(node_data_2_1.into()), + expected: Some(node_data_2_1), }, AssertNode { path: vec![1, 1].into(), - expected: Some(node_data_2_2.into()), + expected: Some(node_data_2_2), }, ]; test.run_scripts(scripts); @@ -357,11 +357,11 @@ fn node_insert_node_before_existing_nested_nodes_test() { // 1:text_1_2 AssertNode { path: vec![1, 0].into(), - expected: Some(node_data_1_1.into()), + expected: Some(node_data_1_1), }, AssertNode { path: vec![1, 1].into(), - expected: Some(node_data_1_2.into()), + expected: Some(node_data_1_2), }, ]; test.run_scripts(scripts); @@ -422,10 +422,10 @@ fn node_delete_node_from_list_test() { .add_node_data(image_a.clone()) .add_node_data(image_b.clone()) .build(); - let text_node_1 = NodeDataBuilder::new("text_1").add_node_data(image_1.clone()).build(); + let text_node_1 = NodeDataBuilder::new("text_1").add_node_data(image_1).build(); let image_2 = NodeDataBuilder::new("image_2") - .add_node_data(image_a.clone()) - .add_node_data(image_b.clone()) + .add_node_data(image_a) + .add_node_data(image_b) .build(); let text_node_2 = NodeDataBuilder::new("text_2").add_node_data(image_2.clone()).build(); @@ -470,13 +470,13 @@ fn node_delete_nested_node_test() { .add_node_data(image_a.clone()) .add_node_data(image_b.clone()) .build(); - let text_node_1 = NodeDataBuilder::new("text_1").add_node_data(image_1.clone()).build(); + let text_node_1 = NodeDataBuilder::new("text_1").add_node_data(image_1).build(); let image_2 = NodeDataBuilder::new("image_2") .add_node_data(image_a.clone()) .add_node_data(image_b.clone()) .build(); - let text_node_2 = NodeDataBuilder::new("text_2").add_node_data(image_2.clone()).build(); + let text_node_2 = NodeDataBuilder::new("text_2").add_node_data(image_2).build(); let scripts = vec![ InsertNode { @@ -486,7 +486,7 @@ fn node_delete_nested_node_test() { }, InsertNode { path: 1.into(), - node_data: text_node_2.clone(), + node_data: text_node_2, rev_id: 1, }, // 0:text_1 @@ -531,11 +531,11 @@ fn node_delete_nested_node_test() { }, AssertNode { path: vec![1, 0, 0].into(), - expected: Some(image_a.clone()), + expected: Some(image_a), }, AssertNode { path: vec![1, 0, 1].into(), - expected: Some(image_b.clone()), + expected: Some(image_b), }, ]; test.run_scripts(scripts); @@ -598,7 +598,7 @@ fn node_reorder_sub_nodes_test() { .add_node_data(image_a.clone()) .add_node_data(image_b.clone()) .build(); - let text_node_1 = NodeDataBuilder::new("text_1").add_node_data(child_1.clone()).build(); + let text_node_1 = NodeDataBuilder::new("text_1").add_node_data(child_1).build(); let scripts = vec![ InsertNode { path: 0.into(), @@ -627,11 +627,11 @@ fn node_reorder_sub_nodes_test() { // 1:image_a AssertNode { path: vec![0, 0, 0].into(), - expected: Some(image_b.clone()), + expected: Some(image_b), }, AssertNode { path: vec![0, 0, 1].into(), - expected: Some(image_a.clone()), + expected: Some(image_a), }, ]; test.run_scripts(scripts); @@ -693,27 +693,27 @@ fn node_reorder_nodes_test() { // 1:image_b AssertNode { path: vec![0].into(), - expected: Some(text_node_2.clone()), + expected: Some(text_node_2), }, AssertNode { path: vec![0, 0].into(), - expected: Some(image_2.clone()), + expected: Some(image_2), }, AssertNode { path: vec![0, 0, 0].into(), - expected: Some(image_a.clone()), + expected: Some(image_a), }, AssertNode { path: vec![1].into(), - expected: Some(text_node_1.clone()), + expected: Some(text_node_1), }, AssertNode { path: vec![1, 0].into(), - expected: Some(image_1.clone()), + expected: Some(image_1), }, AssertNode { path: vec![1, 0, 1].into(), - expected: Some(image_b.clone()), + expected: Some(image_b), }, ]; test.run_scripts(scripts); @@ -778,10 +778,10 @@ fn node_inverted_body_changeset_test() { fn make_node_delta_changeset( initial_content: &str, insert_str: &str, -) -> (TextOperations, Changeset, Changeset, TextOperations) { +) -> (DeltaTextOperations, Changeset, Changeset, DeltaTextOperations) { let initial_content = initial_content.to_owned(); - let initial_delta = TextOperationBuilder::new().insert(&initial_content).build(); - let delta = TextOperationBuilder::new() + let initial_delta = DeltaTextOperationBuilder::new().insert(&initial_content).build(); + let delta = DeltaTextOperationBuilder::new() .retain(initial_content.len()) .insert(insert_str) .build();