Skip to content
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

Do not store webview if there is no corresponding serializer #8680

Merged

Conversation

vitaliy-guliy
Copy link
Contributor

@vitaliy-guliy vitaliy-guliy commented Oct 28, 2020

Signed-off-by: Vitaliy Gulyy vgulyy@redhat.com

This PR is a duplicate of previously closed #8473

What it does

  • Disables saving of the webview state if the plugin don't register an apropriate theia.WebviewPanelSerializer.
  • Fixes an error with restoring the editor area when a webview and several editors are opened.

Video demonstrating bug https://www.youtube.com/watch?v=9ir1hTov350

Fixes #8620 and #6877

This is another approach in comparing with #8621
In #8621 Theia stores the empty state for the webview and tries to find the serializer when restoring the state.
In this PR, Theia checks for the theia.WebviewPanelSerializer, and does not store the webview to the layout at all if the serializer is not found.

How to test

To test it you need to have a plugin which opens a webview, but don't register vscode.WebviewPanelSerializer for this webview.
I used this one https://github.com/vitaliy-guliy/greeting-extension, which opens a Greeting webview each time when Theia starts.

Steps to reproduce:

  • run Theia with the plugin, open the browser. Greeting plugin will open the webview after startup.
  • open few files, Greeting tab must be stay first
  • reload the browser

Opened flies must be restored, the order of files must be saved. As the webview is not persisted, after reloading it will be opened again next to active tab.

Follow 'Steps to reproduce' in the original issue #8620

Review checklist

Reminder for reviewers

@vitaliy-guliy
Copy link
Contributor Author

@akosyakov what do you think about not to store the webview at all in a case when there is no webview serializer. Instead of saving it with {} inner state and then disposing the widget when restoring the layout.

Copy link
Contributor

@tsmaeder tsmaeder left a comment

Choose a reason for hiding this comment

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

I think we should handle the case where widgets cannot be recreated. For example, what if a plugin is updated and no longer contributes a webview serializer?
ShellLayoutRestorer#convertToWidget can return undefined and we need to handle that.

@vitaliy-guliy
Copy link
Contributor Author

I think we should handle the case where widgets cannot be recreated. For example, what if a plugin is updated and no longer contributes a webview serializer?
ShellLayoutRestorer#convertToWidget can return undefined and we need to handle that.

Agree with you. I think thew we need to handle both cases:

  • do not store the webview state if there is no serializer
  • handle undefined when converting JSON to widget

@vitaliy-guliy
Copy link
Contributor Author

@tsmaeder done. I've handled a case when the widget cannot be recreated.

@@ -636,6 +636,12 @@ export class ApplicationShell extends Widget {
// Proceed with the main panel once all others are set up
await this.bottomPanelState.pendingUpdate;
if (mainPanel) {
// Array of widgets must not contain items with `undefined` value as it breaks the restoring of the layout
Copy link
Contributor

Choose a reason for hiding this comment

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

T.b.h I liked the previous version better: can't we just filter the array in ShellLayoutRestorer#parse() after we've created all the widgets?

Copy link
Contributor

Choose a reason for hiding this comment

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

And I like the old version better because the filtering is done closer to where the widget is created (or not created). Simpler to understand when looking at ShellLayoutRestorere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This what I wanted to do first, but in this case it will not work.
In ShellLayoutRestorer#parse() each widget is created in a separate promise, which is called already after returning array of widgets.

const widgets: (Widget | undefined)[] = [];
const descs = (value as WidgetDescription[]);
for (let i = 0; i < descs.length; i++) {
parseContext.push(async context => {
widgets[i] = await this.convertToWidget(descs[i], context);
});
}
return widgets;

Copy link
Contributor

Choose a reason for hiding this comment

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

We wait can for all promises to be fullfilled: we should not return before the widgets array is fully populated anyway:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In previous version we had a problem with order of opened editors. It's because the time for restoring/creating widgets for different files is different and the created widget was always inserted the first.

Layout data is created in ShellLayoutRestorer#inflate function.
At the beginning of this function we parse layout string representation to have LayoutData.

const layout = this.parse<ApplicationShell.LayoutData>(layoutData, parseContext);

But at that moment arrays of widgets are empty for all panels.

At the end we call parseContext.inflate(context). It runs all the promises, which create the widgets and insert them in the array.

await parseContext.inflate(context);

Copy link
Contributor Author

@vitaliy-guliy vitaliy-guliy Nov 10, 2020

Choose a reason for hiding this comment

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

@tsmaeder @spoenemann I have another idea is to move filtering at the end of ShellLayoutRestorer.ParseContext#inflate.

ParseContext has all the arrays to be filtered. So, it will work for all the widget panels.

    export class ParseContext {
        protected readonly toInflate: Inflate[] = [];
        protected readonly toFilter: Widgets[] = [];

        newArray(): Widgets {
            const array: Widgets = [];
            this.toFilter.push(array);
            return array;
        }

        async inflate(context: InflateContext): Promise<void> {
            const pending: Promise<void>[] = [];
            while (this.toInflate.length) {
                pending.push(this.toInflate.pop()!(context));
            }
            await Promise.all(pending);

            if (this.toFilter.length) {
                this.toFilter.forEach(array => {
                    const filtered = array.filter(value => value !== undefined);
                    array.splice(0, array.length, ...filtered);
                });
            }
        }
    }

    export type WidgetArray = (Widget | undefined)[];

Then ShellLayoutRestorer#parse can ask for a widget array, which will be filtered after creating the widgets

    const widgets = parseContext.newArray();
    const descs = (value as WidgetDescription[]);
    for (let i = 0; i < descs.length; i++) {
        parseContext.push(async context => {
            widgets[i] = await this.convertToWidget(descs[i], context);
        });
    }
    return widgets;

I have just checked this approach, it works well.
If it's OK for you, I will update the PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reworked

Copy link
Contributor

Choose a reason for hiding this comment

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

I really don't understand why this needs to be so complicated: just wait for all the promises to resolve and then filter out the undefined values from the array in ShellLayoutRestorer#parse(). It's better code-quality-wise because it does the filtering closer to where the nulls are created and there is no if conditional expression around the filtering. Please convince me!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's such confusing because promises for the widgets are created in one place and then resolved in another.
Why do we need so?
It could happen that your Theia is updated or downgraded and the layout cannot be restored properly.
That requires to do some checks for the layout version and probably transform the layout or update the state of some widgets before creating their instances.
Just take a look at ShellLayoutRestorer#inflate()

Copy link
Member

@azatsarynnyy azatsarynnyy left a comment

Choose a reason for hiding this comment

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

I've tested both use-cases on the provided plug-in https://github.com/vitaliy-guliy/greeting-extension:

  • when a contributed WebView has a related registered serializer
  • when a contributed WebView without a registered serializer

All widgets restore works well. In both use cases, the behavior is the same as in VS Code 👍
So, I'm +1 for this patch.

It would be great to have another pair of eyes look at it as the proposed changes affect one of the essential part - the application shell.
cc @eclipse-theia/core

if (this.toFilter.length) {
this.toFilter.forEach(array => {
const filtered = array.filter(value => value !== undefined);
array.splice(0, array.length, ...filtered);
Copy link
Contributor

Choose a reason for hiding this comment

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

Performance: Do this splice only if filtered.length is smaller than array.length

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Performance: Do this splice only if filtered.length is smaller than array.length

Interesting, I didn't know about it before. In fact the array will not have so much items to work very slow.
But thinking about performance. To avoid checking for an array length, maybe it's better to reset the array at all and then push all the items..

array.length = 0;
array.push(...filtered);

WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

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

That might be an alternative to splice, but checking length for sure has lower cost than resetting the array. And resetting is only necessary if the length has changed.

Another alternative would be this:

for (let i = 0; i < array.length; i++) {
    if (array[i] === undefined) {
        array.splice(i--, 1);
    }
}

If there are only few undefined elements, this variant might be even better because it does not duplicate the array.

Copy link
Contributor Author

@vitaliy-guliy vitaliy-guliy Nov 13, 2020

Choose a reason for hiding this comment

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

In 99% array will not contain undefined elements, so this alternative looks much better than rebuilding the array each time when restoring the layout.
Thanks for this.
I've updated and squashed the PR.

@ericwill ericwill mentioned this pull request Nov 12, 2020
34 tasks
Signed-off-by: Vitaliy Gulyy <vgulyy@redhat.com>
@vitaliy-guliy
Copy link
Contributor Author

I'm going to merge this PR tomorrow if there are no objections.

@vitaliy-guliy vitaliy-guliy merged commit 56c2274 into eclipse-theia:master Nov 17, 2020
@vitaliy-guliy vitaliy-guliy deleted the serialize-webview-copy branch November 17, 2020 10:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Error when restoring editor area if webview is opened
4 participants