visual-space / visual-editor

Rich text editor for Flutter based on Delta format (Quill fork)
MIT License
283 stars 44 forks source link

Sys - Improve file/folders structure #174

Closed adrian-moisa closed 1 year ago

adrian-moisa commented 1 year ago

This is round 2 of refactoring (with far better awareness of the codebase architecture):

adrian-moisa commented 1 year ago

Added a new doc for explaining how inputing text works:

# Inputs

## Architecture Overview The current editor is composed of two major parts, the remote input and the document model.

## Code Flow When a user start typing, new characters are inserted by the remote input. The remote input is the input used by the system to synchronize the content of the input with the state of the software keyboard or other input devices. The remote input stores only plain text. The actual rich text is stored in the editor state store as a DocumentM.

The main VisualEditor class implements several mixins as required by Flutter. One of the expected overrides is updateEditingValue(). This in turn invokes _inputConnectionService.diffPlainTextAndUpdateDocumentModel(). This method will run a series of checks to figure out if the plain text or text selection have changed. Depending on the result we invoke further methods to either updateSelection() or replaceText(). Depending on these changes the next setp in the code flow will be to runBuild() and update the widgets tree. This process is explained in great details in Documents and Controller.

## Keyboard Service The Keyboard service handles requesting the software keyboard and connecting to the remote input. If the software keyboard is enable it triggers the build. This service is not used to stream the keystrokes to the document itself. The document gets updates from the remote input via the InputConnectionService (Read inputs.md). This service is used for other needs such as figuring out if meta keys are pressed (ex: when ctrl clicking links).

This was a very difficult part to understand in the early refactoring process after forking Quill.

adrian-moisa commented 1 year ago

DocumentM is more or less almost a full fledged controller itself (not a pure data model). We've separated most of the methods to a dedicated service to make it easier to grasp the overview of the model. Document model contains two data representations of the same document:

When a document is initialised, the delta operations are converted to nodes and attached to the root node. The build() process maps the document nodes to styled text spans in the widget tree. All document editing operations are computed using the rules middleware. The rules are split in three categories: insert, delete, retain. Rules identify patterns in the document and perform mutations (ex: closing bullet if if enter is pressed twice). After the rules are executed a new updated delta is generated. This delta is then passed to document.compose() which in turn maps it to nodes. If successful compose then maps again the bodes to delta and stores this last one in the document for later use. All document editing methods return change deltas to be streamed for coop editing. The complete history of changes is stored in memory during the editing process.

One of the challenges of understanding how DocumentM works was recognising if methods are pure or impure. I've updated the comments to reflect if methods are pure or impure (purity indicators). Having increased awareness of the purity of code improves the developers ability to predict what will happen. Thus makes the debugging process far easier.

adrian-moisa commented 1 year ago

The new Document Model, down from 450 lines of code.

image

Document model contains the raw data representation:

Document models can be initialised empty or from json data or delta models. Documents can be edited outside of the editor by a DocumentController class. We decided to use this approach to avoid create monster models with excessive data and models in one scope. We prefer the pure functional approach for the sake of keeping the models easy to comprehend by new contributors. Also another reason to keep the api in a DocumentController class was the simple fact that not many developers will be using the document API directly for manipulating the doc in memory. Those experienced enough to require such operations will be able to do so using a new instance of the DocumentController. The net gain is that we have far easier code to read and comprehend compared to the forked Quill repo. And this is what we care the most right now, being highly accessible for new developers not slightly more accessible for experienced devs. Read editor.md for a full breakdown of the editor architecture.