Skip to content

Implement base custom editor support #9812

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

Merged
merged 18 commits into from
Jan 30, 2020

Conversation

rchiodo
Copy link

@rchiodo rchiodo commented Jan 28, 2020

For #9255 and #9514

First step in implementing the custom editor support.

Split the nativeEditor into a View model (nativeEditor.ts) and a model (nativeEditorStorage.ts).
Communication between them now uses custom commands.

Fixes #9510, #9511, #9512, #9515, #9513

@rchiodo
Copy link
Author

rchiodo commented Jan 28, 2020

Still have to do undo/redo and more manual testing

But opening notebooks and creating blank ones should work.

Copy link

@DonJayamanne DonJayamanne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will continue the review, 1/2 way through

package.json Outdated
"webviewEditors": [
{
"viewType": "NativeEditorProvider.ipynb",
"displayName": "Jupyter Notebook Editor",
Copy link

@DonJayamanne DonJayamanne Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the Editor part?
I think we should drop that. #Resolved

Copy link

@DonJayamanne DonJayamanne Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion. #Resolved

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably don't need the editor part.


In reply to: 372156351 [](ancestors = 372156351)

/**
* Opens a file with a custom editor
*/
openEditor(file: Uri): Thenable<void | undefined>;
Copy link

@DonJayamanne DonJayamanne Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not Promise<void> Why void | undefined?

Suggested change
openEditor(file: Uri): Thenable<void | undefined>;
openEditor(file: Uri): Promise<void>;
``` #Resolved

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it maps internally to calling something else that returns that.


In reply to: 372156630 [](ancestors = 372156630)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I can just await inside. You're right, it makes more sense to return a promise.


In reply to: 372479925 [](ancestors = 372479925,372156630)

}

// Set these contents into the storage before the file opens
await this.getStorage(uri, contents);
Copy link

@DonJayamanne DonJayamanne Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably good to rename the method from getStorage to something else...,
We're performing a get operation however we're expecting a side effect (implied by the words Set these contents....).
Hence the get is not really a get, but does more... #Resolved

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadStorage probably sounds better.


In reply to: 372158169 [](ancestors = 372158169)


export const ILoadableNotebookStorage = Symbol('ILoadableNotebookStorage');

export interface ILoadableNotebookStorage extends INotebookStorage {
Copy link

@DonJayamanne DonJayamanne Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't these two be separate interfaces and let a class implement both if required?

I.e. one that provides the operations to read/write from a file/disc and the other that provides operations to manage the content (INotebookStorage).

E.g.

interface I..... {
    load(file: Uri): Promise<INotebookStorage>;
    save(storage: INotebookStorage, file: Uri): Promise<void>;
    saveAs(storage: INotebookStorage): Promise<void>;
}
``` #Resolved

Copy link

@DonJayamanne DonJayamanne Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separating into two interfaces make make help get rid of the promise to in getCells, getContents and getJson, as INotebookStorage would have been resolved.
#Resolved

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the promises would go away. Contents and json are after the fact. Load does not determine them.

Have to think about separating them. Maybe two classes even. NotebookStorage and NotebookModel


In reply to: 372520118 [](ancestors = 372520118)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although the NotebookModel couldn't be 'loaded' from the service container because it would need a 'load' model then too.


In reply to: 372522427 [](ancestors = 372522427,372520118)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'll keep them the same for now.


In reply to: 372522852 [](ancestors = 372522852,372522427,372520118)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds more reasonable. I'll see about decoupling the interfaces.


In reply to: 372525975 [](ancestors = 372525975)

Copy link

@DonJayamanne DonJayamanne Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that the case only because we're trying to save the notebook with the updated metadata information?
I.e. if we had the metadata loaded, then getJson would be sync, hence save would also be sync (in the sense we won't need to await on anything for save to complete).
This is something we can address later. #WontFix

Copy link

@DonJayamanne DonJayamanne Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though I'd still like to see two separate interfaces, even if the same class implements the two. #Resolved

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the metadata is async because it updates when we run a cell.


In reply to: 372531329 [](ancestors = 372531329)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a cell isn't run, then we need to look for the usable jupyter and use that as the kernel information.


In reply to: 372537166 [](ancestors = 372537166,372531329)


export interface INotebookStorage {
readonly file: Uri;
readonly isDirty: boolean;
Copy link

@DonJayamanne DonJayamanne Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably best to rename this to INotebookContents or INotebookModel
This doesn't deal with any storage, but the raw contents of the ipynb file. #Resolved

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INotebookModel sounds good


In reply to: 372491590 [](ancestors = 372491590)

disposableRegistry.push(commandManager.registerCommand(Commands.NotebookStorage_ModifyCells, NativeEditorStorage.handleModifyCells));
disposableRegistry.push(commandManager.registerCommand(Commands.NotebookStorage_RemoveCell, NativeEditorStorage.handleRemoveCell));
disposableRegistry.push(commandManager.registerCommand(Commands.NotebookStorage_SwapCells, NativeEditorStorage.handleSwapCells));
disposableRegistry.push(commandManager.registerCommand(Commands.NotebookStorage_UpdateVersion, NativeEditorStorage.handleUpdateVersionInfo));
Copy link

@DonJayamanne DonJayamanne Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could instead have a wrapper handler (handler that handles the command, checks if there's a notebook and if there is then call the callback with that notebook)

E.g.

type PromiseFunctionWithFirstArgOfResource = (...any: [Uri | undefined, ...any[]]) => Promise<any>;
async function callback(handler, resource) {
    const args = Array.prototype.slice.call(arguments).slice(1);
    const storage = await NativeEditorStorage.getStorage(resource);
    if (storage) {
        handler(storage, ...args);
    }
}

commandManager.registerCommand(....InsiderCell, callback.bind(undefined, NativeEditorStorage.handleDeleteAllCells);

// finally
private static handleDeleteAllCells(storage: INotebookStorage, resource: Uri): Promise<void> {
``` #Resolved

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure that's what I was essentially doing, but then you just said callbacks were bad.


In reply to: 372513072 [](ancestors = 372513072)

Copy link

@DonJayamanne DonJayamanne Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It avoids the callback in the handler and makes that cleaner.
But thats only a suggestion, no need to change. Personally I'd prefer to avoid callbacks. #Resolved

@@ -62,6 +62,17 @@ export namespace Commands {
export const ScrollToCell = 'python.datascience.scrolltocell';
export const CreateNewNotebook = 'python.datascience.createnewnotebook';
export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput';

// Make sure to put these into the package .json
Copy link
Member

@IanMatthewHuff IanMatthewHuff Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to happen now? #WontFix

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only if we need to expose them externally.


In reply to: 372540362 [](ancestors = 372540362)

Copy link

@DonJayamanne DonJayamanne Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users would need to run VSCode with a special CLI argument as well. At least that's what I thought. #ByDesign

Copy link
Member

@IanMatthewHuff IanMatthewHuff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were a couple of comments that I had open, but overall looks good. Glad we are cutting out lots of that old custom code.

@rchiodo rchiodo closed this Jan 29, 2020
@rchiodo rchiodo reopened this Jan 29, 2020
@rchiodo rchiodo closed this Jan 29, 2020
@rchiodo rchiodo reopened this Jan 29, 2020
@codecov-io
Copy link

codecov-io commented Jan 29, 2020

Codecov Report

Merging #9812 into ds/custom_editor will decrease coverage by 0.22%.
The diff coverage is 53.01%.

Impacted file tree graph

@@                 Coverage Diff                  @@
##           ds/custom_editor    #9812      +/-   ##
====================================================
- Coverage             60.58%   60.35%   -0.23%     
====================================================
  Files                   552      551       -1     
  Lines                 29852    29536     -316     
  Branches               4527     4451      -76     
====================================================
- Hits                  18086    17827     -259     
+ Misses                10748    10717      -31     
+ Partials               1018      992      -26
Impacted Files Coverage Δ
src/client/telemetry/index.ts 85.58% <ø> (ø) ⬆️
src/client/datascience/webViewHost.ts 9.48% <0%> (-51.55%) ⬇️
...ractive-window/interactiveWindowCommandListener.ts 57.82% <0%> (ø) ⬆️
.../datascience/interactive-common/interactiveBase.ts 5.66% <0%> (-11.48%) ⬇️
...atascience/interactive-window/interactiveWindow.ts 18.38% <0%> (ø) ⬆️
src/client/datascience/constants.ts 99.69% <100%> (ø) ⬆️
src/client/common/application/types.ts 100% <100%> (ø) ⬆️
src/client/common/serviceRegistry.ts 100% <100%> (ø) ⬆️
src/client/datascience/types.ts 100% <100%> (ø) ⬆️
...ience/interactive-common/interactiveWindowTypes.ts 100% <100%> (ø) ⬆️
... and 22 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d5f153e...7049d5a. Read the comment docs.

@sonarqubecloud
Copy link

Kudos, SonarCloud Quality Gate passed!

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities (and Security Hotspot 0 Security Hotspots to review)
Code Smell A 3 Code Smells

No Coverage information No Coverage information
0.0% 0.0% Duplication

@rchiodo rchiodo merged commit 80cf8af into ds/custom_editor Jan 30, 2020
@rchiodo rchiodo deleted the rchiodo/base_custom_editor branch January 30, 2020 16:13
@lock lock bot locked as resolved and limited conversation to collaborators Feb 6, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants