-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Implement undo support for the custom editor #9946
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
Conversation
…ustom_editor_undo
.vscode/launch.json
Outdated
@@ -187,7 +187,7 @@ | |||
"--ui=tdd", | |||
"--recursive", | |||
"--colors", | |||
//"--grep", "<suite name>", | |||
"--grep", "Edit/delete cells", |
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.
cells [](start = 39, length = 5)
Oops #Resolved
@@ -374,19 +331,37 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor { | |||
} | |||
} | |||
|
|||
protected sendCellsToWebView(cells: ICell[]) { | |||
protected async sendCellsToWebView(cells: ICell[]) { |
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.
async [](start = 14, length = 5)
Actually this doesn't need to be async. #Resolved
Codecov Report
@@ Coverage Diff @@
## ds/custom_editor #9946 +/- ##
===================================================
Coverage ? 60.52%
===================================================
Files ? 556
Lines ? 29788
Branches ? 4487
===================================================
Hits ? 18029
Misses ? 10769
Partials ? 990
Continue to review full report at Codecov.
|
@@ -280,6 +281,7 @@ export class NativeCell extends React.Component<INativeCellProps> { | |||
this.props.sendCommand(NativeCommandType.ToggleOutput, 'keyboard'); | |||
} | |||
break; | |||
case 'NumpadEnter': |
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.
Thought I'd fix this while I was changing it :) #Resolved
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.
All good, except for some state stuff..
if (this.state.model) { | ||
return this.state.model.getValue().replace(/\r/g, ''); | ||
if (this.monacoRef.current) { | ||
return this.monacoRef.current.getContents(); |
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.
Oh good, I never understood why we removed /r/
#Resolved
if (this.state.editor && this.state.model && this.monacoRef && this.monacoRef.current) { | ||
const cursor = this.state.editor.getPosition(); | ||
if (this.monacoRef.current) { | ||
const cursor = this.monacoRef.current.getPosition(); |
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.
If we're not using state.model
or state.editor
, do we still need to add them into the state object.
I'm glad we're getting rid of them, much cleaner (clean = not confusing why it was in state).
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.
I.e. please review what's in state and remove, I think we can remove them.
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.
They are removed? Not sure what you're asking. IEditorState doesn't exist anymore.
In reply to: 376519201 [](ancestors = 376519201)
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.
this.setState({ editor, model: editor.getModel() });
Do we need these states anymore? I don't think model
is being used.
@@ -28,7 +28,7 @@ export namespace Effects { | |||
|
|||
if (typeof removeFocusIndex === 'number') { | |||
const oldFocusCell = prevState.cellVMs[removeFocusIndex]; | |||
const oldCode = oldFocusCell.uncomittedText || oldFocusCell.inputBlockText; | |||
const oldCode = oldFocusCell.uncommittedText || oldFocusCell.inputBlockText; |
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.
With the new changes, do we still need uncommittedText
?
Feels hacky to me
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.
Yes. For now. Until we make it really update the text on every type.
In reply to: 376522239 [](ancestors = 376522239)
|
||
public getContents(): string { | ||
if (this.state.model) { | ||
return this.state.model.getValue().replace(/\r/g, ''); |
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.
Please could you add some comments around the need for this. (the need to remove /r
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.
Sure. It's so it's consistent with what jupyter expects. It doesn't put in /r ever.
In reply to: 376523346 [](ancestors = 376523346)
// Spec exists, just update name and display_name | ||
s._state.notebookJson.metadata.kernelspec.name = kernelSpec.name || kernelSpec.display_name || ''; | ||
s._state.notebookJson.metadata.kernelspec.display_name = kernelSpec.display_name || kernelSpec.name || ''; | ||
if (changed) { |
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.
I'm a bit confused as to why this doesn't use the newDirty from the change as Undo does. I get this is handling user and redo, but in the redo case I would assume that it would be using that.
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.
Can you redo a change and have it not be dirty? I don't think so? Not 100% sure. That's why its like this. The newDirty state from the redo may be off.
In reply to: 376528654 [](ancestors = 376528654)
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.
Ok, I think I get it better now. So the new / old Dirty are always reflecting just reality of the change at the time that it was made. Right?
In reply to: 376529516 [](ancestors = 376529516,376528654)
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.
private swapCells(firstCellId: string, secondCellId: string) { | ||
const first = this.cells.findIndex(v => v.id === firstCellId); | ||
const second = this.cells.findIndex(v => v.id === secondCellId); | ||
if (first >= 0 && second >= 0) { |
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.
first !== second check as well? Would keep us from returning changed on a no op. #Resolved
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.
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.
Code flow isn't showing the change here for some reason, but I did what you suggested.
In reply to: 376530953 [](ancestors = 376530953,376529606)
if (storage) { | ||
return handler(storage, ...args); | ||
private clearOutputs(): boolean { | ||
this._state.cells = this.cells.map(c => this.asCell({ ...c, data: { ...c.data, execution_count: null, outputs: [] } })); |
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.
It does make it a bit more complex, but I'm thinking that this could also use a possible false state. If you have a cell or two that is not run, this is going to return changed when nothing has actually happened. If you wanted to be super safe modify and remove could also have false states (no cells) but since we never expect that UI state I don't think it's needed as much for them.
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.
Yeah I was thinking it wasn't worth it for the clear cells (cause of the cost of the compare), but maybe. It would be weird if you cleared, saved, cleared again and it was dirty
In reply to: 376535431 [](ancestors = 376535431)
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.
It's not a bad bug, but this did seem like the only situation where this would be wrong given a valid input state. I get that it does add a bit more. Since you are clearing everything it's not like you need to check each cell. Just if any cell has an execution count or an output this will do something for changed.
In reply to: 376538081 [](ancestors = 376538081,376535431)
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.
I think I need to rethink the dirty state. Right now this doesn't work:
Open notebook
Clear all outputs - editor tab and react ui think dirty
Save - editor tab and react ui think clean
Undo - editor tab says dirty, ui says otherwise.
I think the dirty state should just be a diff between the saved state and the current state. That's what makes it dirty. And I don't want to compare the entire thing on every update. So have to do smart comparisons.
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.
@DonJayamanne did you still have concerns? (Other than the save problem I'm working on) Was going to submit this and work on the save problem separately. |
Kudos, SonarCloud Quality Gate passed!
|
For #9821
Implement undo support by creating a new NotebookModelChange type that we send to both VS code and to ourselves to keep track of changes to the underlying model of a notebook editor.
Edits that come from the UI are tagged with 'user' and are forwarded onto VS code.
Edits that come from VS code (either undo or redo) are tagged with 'redo' and 'undo' and are forwarded onto the UI.
Also eliminated undo/redo support in the monaco editor so we could control the undo stack. This didn't turn out perfect though for a number of reasons: