Closed adrian-moisa closed 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.
TextInputConnection
between the system's TextInput
and a TextInputClient
(visual editor).## 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.
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:
DeltaM
- List of operations as stored in the json document (insert, delete, retain). Nodes
- List of nodes resulted from the operations (including styles). 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.
The new Document Model, down from 450 lines of code.
Document model contains the raw data representation:
DeltaM
- List of operations as stored in the json document (insert, delete, retain).HistoryM
- Stacks of undo and redo operations (each doc has it's own history).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.
This is round 2 of refactoring (with far better awareness of the codebase architecture):
defaultToggleStyleButtonBuilder
fromToggleStyleButton
and fromToggleCheckListButton
.DocumentEditingService
, MovedformatText
formatTextStyle
toTextStylesService
.SelectionActionsService
toSelectionHandlesService
.keepStyleOnNewLine
to the state store inEditorConfigM
. There was no need to separate this property from the main config.onSelectionChanged
.onBuildComplete
callback. Since the rectangles data is stale we decided to remove it rather than refactor the whole code flow.selection
,highlights
,markerTypes
,editorWidget
from state no longer requiredCoordinatesService
fromLinesAndBlocsService
.StylesService
&DocumentService
editorController
to controllereditorWidgetState
towidget
CursorService
andCaretService
toolbarButtonToggler
andcopiedImageUrl
to the state storeconfig.config
even if it brakes the state store pattern. It's simply too much nesting repetition in the code base for no benefit.refs.controller.selection
with the state/service versionselection.selection
TextValueService
toPreBuildService
to better reflect what it really does.value
inTextEditingValue
value toplainText
.refreshEditor
torunBuild
.DocumentRenderService
intoDocTreeService
.setState
to voidcacheStateStore
to avoid confusion with the widgetsetState
.userUpdateTextEditingValue
toremoveSpecialCharsAndUpdateDocTextAndStyle
.EditorTextService
intoDocumentService
(defragmentation).runBuild$L
.refs.controller
with service calls.TextSelectionService
toSelectionService
EmbedsService
, has methods for inserting embeds programatically.InputConnectionService
RenameupdateEditingValue
todiffPlainTextAndUpdateDocumentModel
InputState
to cache temporarily theplainText
value duringInputConnectionService
and theKeyboardConnectionService
.PreBuildService
toGuiService
to better indicate it's role.runBuild
andrunBuildIfMounted
EditorKeyboardListener
intoVisualEditor
. It had nothing useful to add in the widget tree.EditorService
intoGuiService
.EditorService
toRunBuildService
_emitPressedKeyHandler
,state.pressedKeys.emitPressedKeys()
._pressedKeysChanged
. It is useless since we use the state store.metaOrControlPressed
can be read directly sync from the state store.TextGesturesService
OnSelectionChangedCallback
. They are outdated because the callback is invoked before the build cycle can compute the new rectangles.TextGestures
service and widget to /inputsStylesService
. One of them was renamed toStylesCfgService
.other
innew
,this
incurr
HistoryM
. It appears we have support for coop but it is not exposed/enabled publicly.DocumentM
. Extracted most methods in a dedicated serviceDocumentNodesService
. Improved doc comments.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.DocumentService
toEditorService
. RenameDocumentNodesService
toDocumentService
. This reflects the reality of what happens in codebase.EditorService
mutates doc, updates all systems and triggers build.DocumentService
only performs the mutations on the doc.DeltaM
andOperationM
. Extracted most methods in a dedicated serviceDeltaService
. Improved doc comments.HistoryM
intoHistoryService
, since this is an internal model never exposed in the public.DocumentM
in theEditorController
. Changes are not operated and broadcasted from an idle document without the user being aware via the GUI. Therefore it does not make sense to keep the changes stream in the document model. We want to have the document model pure data.HistoryService
andDocumentService
in the controller. This means we can start migrating the document models to pure data. Also it means all services are now state store aware. TheHistoryService
andDocumentService
were document aware only. We did not want to pass the state store to the document to avoid complicated architecture. Now that we are migrating all data from the models means we no longer risk exposing the state store in the public. More explanations about our architecture choices in editor.md .DocumentService
toDocumentController
,DeltaService
toDeltaUtils
. This enables us to keep the models as pure data. It also enables advanced client developers to edit documents that are not cached in theEditorController
.DocumentM
toDocumentController
. MovedcustomRules
tostateStore
(one list per editor).DocumentUtils
to segregate some simple utils that are used also when initialising the Document.EditorState
when passing it to theToolbar
buttons meant that we lost the reference to theDocumentController
. Fixed theCusrsorController.dispose()
issue the correct way by caching the prev instance in the state store. Detailed explanation in state-store.md. Now the state store is back again a simple object, easy to understand.HistoryService
toHistoryController
. We need a controller independent of the state store to be able to process changes also when the document model manipulated from outside.HistoryController
inDocumentController
. Edits running outside of the editor will update history as well.main.dart
.SelectionActionsController
toSelectionHandlesController
.DeltaService
toDeltaUtils
.length
intocharsNum
.adjust
intomergeSimilarNodes
.EditableTextPaintService
.EditableTextLineBoxRenderer
has many overrides concerned with computing the layout dimensions. Therefore the painting logic for selection/highlight boxes is better separated here. Separating the layout dimensions logic and painting logic helps improves readability and maintainability.EditorRendererInner
toEditorTextAreaRenderer
following the conventions for editable text line.