Prevent code editing for specific file extension

In our scenario, we have a custom editor for files with a specific extension (.wf). The editor can be opened via “Open” or “Open with…”. In case of “Open with…”, we see 2 options: “WF viewer” and “Code Editor”. The “WF viewer” is our custom editor. The “Code Editor” is the default code editor from theia. Since .wf should not be edited through code editor, is there a way to prevent the “Code Editor” option in “Open with…”? Or else, is there a way the code editor opens in read-only mode to prevent user from editing?
I saw ticket What is the best approach to prevent opening some file types which seems to be related. The solution there requires a change in ‘standard’ theia. However, I would prefer to stick to ‘standard’ theia.
Any suggestion is welcome

@hbank since the default behavior is not what you require for your use-case, you will need to extend the base functionality of the framework to adjust. You can do so by providing your own custom extension which extends the base EditorManager (and subsequently rebinds it), to provide a canHandle which takes into account the uri and determines if the file extension should be opened (in your case you can prevent opening .wf files).

At the moment, the canHandle of EditorManager is as follows (permits opening all files) - (0 will denote it cannot open the file resource):

canHandle(uri: URI, options?: WidgetOpenerOptions): number {
    return 100;
}

The markdown preview open handler is as follows (for reference):

canHandle(uri: URI): number {
        return uri.scheme === 'file'
            && (
                uri.path.ext.toLowerCase() === '.md' ||
                uri.path.ext.toLowerCase() === '.markdown'
        ) ? 500 : 0;
}

We declare that only such file extensions can actually be opened for this type of widget.

1 Like

Hi @vince-fugnitto, thanks for your quick and extensive reply. Extending it (WfEditorManager extends EditorManager) and rebinding it as EditorManager will solve it for me.

Assume the WF modeler is implemented as a theia extension, that comes with its own WfEditorManager (extends EditorManager). Suppose there is another theia extension with different modeler, say FW modeler, which comes with FwEditorManager. (extends EditorManager) and follows the same rebind approach.

In case there is a user that installs both theia extensions, only one of the EditorManagers can be active. Any thoughts/suggestions on how to deal with this?

I would think that generic extensions should NOT rebind things to avoid this kind of clash. Although, when making an application you can create extensions specific to it in order to fine-tune your IDE, then it makes sense to rebind core components without too much worrying.

Regarding what @vince-fugnitto suggested, maybe don’t rebind the EditorManager directly but rather contribute some OpenHandler that would have higher priority than the default one?

1 Like

@marechal-p, regarding “rather contribute some OpenHandler that would have higher priority than the default one”: I could create an OpenHandler for my .wf files, and give it a higher priority (> 100), so that by default WF modeler is opened. Then still, the user has the possibility to open the .wf file via “Open with… > Code Editor”. And that’s what I want to prevent: the user should not edit a .wf model because with manual editing you could easily corrupt the model.

You can participate in text document creation via MonacoTextModelService.onDidCreate and for certain models override readonly flag to always be true.

@akosyakov, not exactly sure what you mean here, but I guess with this approach the code editor will be opened in read-only mode. Correct? That would be a valid alternative solution: cannot open with CodeEditor/cannot edit with CodeEditor. I was more focussing on “cannot open”.

Regarding the openHandler in EditorManager which was discussed earlier in this thread: It’s implementation is (as pointed out by @vince-fugnitto):

canHandle(uri: URI, options?: WidgetOpenerOptions): number {
    return 100;
}

Instead of rebinding/extending EditorManager we could modify this method a bit, by adding an exclusion list, which contains the file extensions for which the CodeEditor should not be available. Then canHandle method will be like:

canHandle(uri: URI, options?: WidgetOpenerOptions): number {
    if (this.exclusions.contains(uri.path.ext)) {
        return 0;
    }
    return 100;
}

To define that CodeEditor is not applicable for a certain extension, a dedicated method is added:

excludeExtension(extension: string) {
   this.exclusions.add(extension);
}

This method is invoked as follows:

@inject(EditorManager)
protected editorManager: EditorManager;

@postConstruct()
protected init() {
    this.editorManager.excludeExtension(".wf");
}

What about this proposal? Would it make sense to create a PR for this change in EditorManager?

You can subclass EditorManager and override canHandle. There is no need for a PR. If something goes into theia repo it should be used there. If it is not used downstreams should use DI to customize.

Thanks for the replies and discussion. For me it makes clear what solution direction to follow: extend EditorManager with required logic, and rebind it using DI.

1 Like

Hi @akosyakov, sorry for coming back… I followed your approach by implement MyEditorManager extends EditorManager. The rebind logic in the module definition looks as follows:

export default new ContainerModule((bind, unbind, isBound, rebind) => {
    bind(MyEditorManager).toSelf().inSingletonScope();

    // rebind the EditorManager of theia by our custom one
    rebind(EditorManager).to(MyEditorManager).inSingletonScope();
    bind(OpenHandler).toService(MyEditorManager);
});

However this fails because EditorManager was not yet registered (isBound(EditorManager) returns false), so the rebind statement fails. With debugging I saw that standard EditorManager is registered after my custom one.

So question is:

  • Is there any defined order in registering extensions. And can we influence this order?
  • Is there a way to “rebind” the EditorManager at a later moment.

Any suggestion welcome.

Bindings are applied in the topological order according to extension dependency graph. Your extension should explicitly depend on @theia/editor in package.json to be loaded afterwards.

@akosyakov, thanks, that helps.
I followed the approach you suggested: an extension with the custom EditorManager (MyEditorManager) and an extension with the WF View. When I run, I see that 2 instances are created of MyEditorManager (the constructor is called twice). It looks that the object bound in one extension is not visible in the other extension.
Is this as expected? Did I make a mistake (see code below)? And is there a way to overcome?

For completeness, I have added the code where I do the binding/resolving:

MyEditorManager extension
In the extension with my customer EditorManager, called MyEditorManager. In the my-editor-manager-module.ts I have placed this code:

import { ContainerModule } from 'inversify';
import { EditorManager } from '@theia/editor/lib/browser';
import { MyEditorManager } from './my-editor-manager';

export default new ContainerModule((bind, unbind, isBound, rebind) => {
    bind(MyEditorManager).toSelf().inSingletonScope();
    rebind(EditorManager).to(MyEditorManager).inSingletonScope();
});  

WF view extension
In the extension with the WF viewer, I want to tell that files extension “.wf” should not be available for the code editor, using the new MyEditorManager.
So in my WfViewerOpenHandler, I use the exclude method from my custom EditorManager

    @postConstruct()
    protected async init() {
        this.myEditorManager.exclude(".wf");
    }

Which is part of the following code:

import { injectable, inject, postConstruct } from 'inversify';
import { MyEditorManager } from '@my/my-editor-manager/lib/browser/my-editor-manager';

...

@injectable()
export class WfViewerOpenHandler implements OpenHandler {

    readonly id = 'wf.openViewer';
    readonly label = 'Open with WF viewer';

    ...
	
    @inject(MyEditorManager)
    protected readonly myEditorManager: MyEditorManager;

    @postConstruct()
    protected async init() {
        this.myEditorManager.exclude('.wf');
    }

    canHandle(uri: URI): number {
        if (uri.path.ext == '.wf') {
            return 1000;
        }
        return 0;
    }

    async open(uri: URI): Promise<TheiaWidgetExtensionWidget | undefined> {
        ....
    }
}

It should be rebind(EditorManager).toService(MyEditorManager), otherwise you get different instances when injecting MyEditorManager or EditorManager.

Thanks @akosyakov. It works.