-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Allow empty notebook #13141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow empty notebook #13141
Conversation
|
Thanks for making a pull request to jupyterlab! |
|
We also create an empty cell if there is no cell in jupyter_ydoc. Maybe the solution is to only keep the logic there and remove it from the front-end in collaborative mode? There would be no need for the button. |
|
Yes, but we add it only when opening/creating a new notebook. We don't check if the client removed all the cells. Besides that, now that we have the ystore, we only add the cell from the backend when loading the document from the disk and not when the client opens a document that was already on the ystore. |
Can we just change the implementation of the restriction in JupyterLab? What if instead we disallow deleting the last cell and if user asks us to delete it we would just clean it rather than re-insert an empty cell? I slightly prefer keeping the old behaviour over adding a button, as in my view it will add a new user interaction that we need to maintain with no clear user benefit (mind you some users are already annoyed by changes which subtly broke their workflows due to the way RTC was introduced, and only a subset of users will take advantage of RTC). |
|
I also think that we should not make the notebook one more click away from entering some code. It would be like in a text editor, you needed to click a button to add a first line. |
|
Note that Observable notebooks appear to not allow empty notebooks either (you cannot delete the cell if it's the only one). |
|
Another solution could be to
However, this would work if most users start by using a code cell first. Which I am not sure of. It can be annoying to have to change the type of your cell every time you create a notebook because the default one does not fit your need. I think the root question here is: does having a code cell by default meet our users' needs? If yes, which users? Do they represent a majority? The answers to these questions should drive the decision of the solution to this issue. |
It is better than re-inserting an empty cell, but we will still deal with specific corner cases, which we are trying to avoid.
I believe we do it for selecting a kernel. When opening/creating a new notebook, Instead of defaulting to the first available kernel, we show a dialog to the user to choose a kernel. How many notebooks are out there that start with a code cell? Most notebooks I have seen start with a markdown cell with a title or a description of the work done in that notebook. In my case, when I create a new notebook, I first change the cell to a markdown cell and write down a description of what I'm going to do in that notebook. |
That will work. |
The default cell type could be configurable in the settings. Observable starts with a Markdown cell, probably because it should be a title for your notebook. But it's true that in Jupyter it has been a code cell for a long time, and people are used to it.
If it's justified yes, why not changing this behavior. But I don't recall people complaining about that. And if other successful notebooks like Observable also disable empty notebooks, maybe we should not make that change? |
|
@hbcarlos Clearing a cell is not different from anything we already do in RTC, it's just equivalent to replacing a content with a new one. |
|
FYI, there probably are assumptions through the code that assume there is at least on cell. For example, the notebook active cell I think is always assumed to point to some cell. |
|
@williamstein mentioned in the dev call:
|
This will be hard because there is gonna be edge cases like if a notebook has two cells that get deleted simultaneously by two users (like user A delete cell 1 and user B delete cell 2). You still end up in an empty notebook. |
No because we can make sure that in the back-end we never end up with no cell. |
|
If there is a way to preserve the user experience of always having at least one cell in a notebook (e.g., creating a cell automatically so that the user perception is that a cell always exists), I think that will make a huge difference to users. If I ever am in the situation where all the cells are deleted, I think there is one reasonable path forward, and making me click something to get back to a working state is annoying friction. |
|
"New View for Notebook" seems broken with this PR ( |
This is not related to this PR: #12965 |
…NotebookAdapter and NotebookTrustStatus
|
Reading up on this PR a bit more. I am trying to wrap my head around the different cases. Seems like there are two situations:
In both cases, it seems like the backend should own this decision and handle the edge case. The frontend would need to be updated to allow for no cells for a short period or time, but otherwise, things would remain the same from the user's perspective. |
|
Note that @dmonad suggested not inserting a new cell from the backend:
|
fcollonval
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @hbcarlos
I left some comments. And the test failures in js-notebook are relevant.
|
I believe that if we want to preserve the UX of Jupyter, then this amounts to a leader election problem. @dmonad do you have recommendations on how to do leader election using only Yjs data structures? Picking the server as the leader always is one way of solving the problem, but there may be others. |
fcollonval
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @hbcarlos
Let's move forward with this PR and see what testers think of this curated handling of the last cell deletion.
* Allows empty notebook * Modify command insertBelow and insertAbove, allow activeCell None in NotebookAdapter and NotebookTrustStatus * Fixes integrity check and linting * Add cell when the notebook is empty in non collaborative mode * Fixes notebook tests * Improves adding a new cell when the notebook is empty in non collaborative mode * Adds a message to the notebook, just in case we end with an empty notebook * Review * Fixes
* Allows empty notebook * Modify command insertBelow and insertAbove, allow activeCell None in NotebookAdapter and NotebookTrustStatus * Fixes integrity check and linting * Add cell when the notebook is empty in non collaborative mode * Fixes notebook tests * Improves adding a new cell when the notebook is empty in non collaborative mode * Adds a message to the notebook, just in case we end with an empty notebook * Review * Fixes
* Allow empty notebook (#13141) * Allows empty notebook * Modify command insertBelow and insertAbove, allow activeCell None in NotebookAdapter and NotebookTrustStatus * Fixes integrity check and linting * Add cell when the notebook is empty in non collaborative mode * Fixes notebook tests * Improves adding a new cell when the notebook is empty in non collaborative mode * Adds a message to the notebook, just in case we end with an empty notebook * Review * Fixes * Linter * Remove unused import Co-authored-by: Carlos Herrero <contact@carloshb.com>
Even though the notebook format doesn't specify that a notebook should have at least one cell, JupyterLab imposes this restriction for a better UX by adding a new empty cell every time the Notebook is blank.
This restriction conflicts with RTC because it is done in the front-end. When a client detects the Notebook is empty, it adds a new cell. We could end up in a situation where we add multiple empty cells because, after a removal operation, multiple clients detect that the Notebook is empty. To prevent this situation, at the moment, we check if the Notebook is empty after each operation which could lead to an empty notebook and creating an empty cell in the same transaction. For example, the action
deleteCells:jupyterlab/packages/notebook/src/actions.tsx
Lines 2433 to 2449 in d46f100
Another example is the YNotebook, where when instantiating the Notebook, we need to create the initial cell as a Yjs update message to prevent multiple clients create the same initial cell.
jupyterlab/packages/shared-models/src/ymodels.ts
Lines 286 to 291 in d46f100
jupyterlab/packages/shared-models/src/ymodels.ts
Lines 330 to 344 in d46f100
By forcing the Notebook to have at least one cell, we are spreading this logic to multiple places, NotebookActions, StaticNotebook, NotebookModel, and YNotebook, plus adding the complexity of dealing with Yjs messages like in the case of the YNotebook.
References
Code changes
1- To make sure the Notebook is not empty when we open it in non-collaborative mode, we add an empty cell during the initialization of the Notebook here:
jupyterlab/packages/notebook/src/widget.ts
Lines 556 to 563 in 92b57bb
because at this point, the model is not initialized yet (it is empty) and here:
jupyterlab/packages/notebook/src/model.ts
Lines 264 to 268 in 92b57bb
in case we are initializing the model without cells.
2- In collaborative mode, we add the cell from the backend while loading the document here:
https://github.com/jupyter-server/jupyter_ydoc/blob/7d67a75a386521445794e3958747c07cb0e79ebb/jupyter_ydoc/ydoc.py#L169-L178
3- Once the Notebook is initialized, to improve re-adding the cell, in both cases (collaborative mode or not), we re-add the cell in the same transaction we are removing all the cells, here:
https://github.com/hbcarlos/jupyterlab/blob/edef2f45afd83bc057eb2af739791db600fae8b3/packages/notebook/src/actions.tsx#L2451-L2462
this way, re-adding the cell is an atomic operation with the deletion, and we mitigate propagating an update where the Notebook is empty. This will prevent the user from removing all the cells, but extensions will be able to remove them by interacting directly with the shared model.
The reason to re-add the cell in the notebook action "deleteCells" instead of the shared model is that re-adding the cell in the shared model is an unexpected behavior since extension developers might use the delete operation in conjunction with other operations like inserting a cell. For example, the notebook action "changeCellType" doesn't modify the cell, it removes the cell and creates a new one of the desired type (markdown, raw, or code). By re-adding the cell in the delete operation of the shared model, the developer needs to be aware that deleting the last cell will create a new one. Otherwise, they will end with two cells. This is weird behavior for a list that breaks multiple notebook actions, "changeCellType", "mergeCells", "splitCells", etc., and I'm sure it will break some extensions.
4- There is one corner case where the Notebook could end in an empty state. If we have N clients and N cells, and each client removes one cell simultaneously, we can have a blank notebook once all the updates are synced. We can solve this corner case by adding a cell from the backend (here jupyter-server/jupyter_ydoc#62).
5- In addition, in case the notebook end empty, we are adding a message to the Notebook here:
jupyterlab/packages/notebook/src/widget.ts
Lines 615 to 617 in 92b57bb
In my opinion, we should not implement 4 because modifying a document automatically by reacting to a change in the document is dangerous and could create future issues. At the same time, if we don't implement 4, we should not implement 3. Otherwise, if the situation described in 4 occurs, it will look like a bug. Instead, we should allow the user to remove all the cells and add a button to the Notebook so the user knows the Notebook is empty and can easily add a new cell (this is the approach used by collab and vscode).
User-facing changes
Backwards-incompatible changes