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

[WIP] [editor] Renaming a file should rename a corresponding editor #582

Closed
wants to merge 1 commit into from

Conversation

lmcgupe
Copy link
Contributor

@lmcgupe lmcgupe commented Sep 27, 2017

Signed-off-by: Guy Perron [email protected]

@hexa00
Copy link

hexa00 commented Sep 28, 2017

I tought you were trying to close and open just for experimentation.

But if you ask for review, you should know this is not the proper way forward.
You should listen on filesystem changes and update the title for the widget.

@svenefftinge
Copy link
Contributor

We don't need anything in the widget-manager for closing. Just call widget.close().

@lmcgupe
Copy link
Contributor Author

lmcgupe commented Sep 28, 2017

The scope of this fix is when the rename is done within the IDE.

@lmcgupe lmcgupe requested a review from hexa00 September 29, 2017 00:01
let key = this.toKey({ factoryId, options });
let existingWidget = this.widgets.get(key);
if (existingWidget) {
existingWidget.title.label = newLabel;
Copy link

Choose a reason for hiding this comment

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

Changing the label of a widget and changing it's key are 2 different things.

You should have a method to change the key here called updateWidgetKey for example.

And the title should be changed after filesysem.move is done and sucessful

Copy link
Member

Choose a reason for hiding this comment

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

It should happen in the editor manager, nothing to do with the widget manager

dialog.open().then(name => {
renameWidget(this.openerService, uri.parent.resolve(uri.path.base), uri.parent.resolve(name), name).then(() => {
this.fileSystem.move(uri.toString(), uri.parent.resolve(name).toString());
});
Copy link

Choose a reason for hiding this comment

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

we should:

  • move the file and make sure it worked
  • update the widget key
  • change the title

@@ -43,6 +43,8 @@ export interface OpenHandler {
* Never reject if `canHandle` return a positive number; otherwise should reject.
*/
open(uri: URI, options?: OpenerOptions): MaybePromise<object | undefined>;

renameWidget(uri: URI, newUri: URI, newLabel: string, options?: OpenerOptions): void;
Copy link
Member

Choose a reason for hiding this comment

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

You should not change OpenerService, only EditorManager should be changed.

@akosyakov
Copy link
Member

akosyakov commented Sep 29, 2017

To make it right, i think it shold be based on Resource. Editors are not working with files, but with resources, e.g. java resource to open source code, git resource to read history and so on.

@svenefftinge What do you think about adding optional onDidChangeUri event to Resource interface. FileResource will listen to file changes and if a resource is renamed when notify an editor that it could update label and icon.

@akosyakov
Copy link
Member

akosyakov commented Sep 29, 2017

To implement it we would need also expose an underlying editor resource via TextEditor interface to allow EditorManager to access it here:
https://github.com/theia-ide/theia/blob/05f2d772f7c6effdcc9afb2642944b5786d4ba6a/packages/editor/src/browser/editor-manager.ts#L116-L121

So it would be here something like:

if (textEditor.resource.onDidChangeUri) {
  textEditor.resource.onDidChangeUri(newUri => {
    newEditor.id = this.id + ":" + newUri.toString();
    newEditor.title.label = newUri.path.base; 
    newEditor.title.iconClass = this.iconProvider.getFileIconForURI(textEditor.newUri);
  });
}

@akosyakov
Copy link
Member

We also don't have an event so far to indicate a file rename, you should look in the chokidar watcher code in vscode to learn how to identify it. I saw something like that there.

@hexa00
Copy link

hexa00 commented Sep 29, 2017

Nice thanks for the pointers @akosyakov!

Note I think we could do this in 2 steps, first use the UI rename as a trigger to the onDidChangeUri and after use the FileWatcher.

Also note that vscode doesn't rename if a file is moved from the filesystem, it marks the editor as delete from disk if you had unsaved changes or it closes the editor if you had no change.

@lmcgupe lmcgupe force-pushed the editor branch 2 times, most recently from 43d065c to 8fa4026 Compare September 29, 2017 20:50
@svenefftinge
Copy link
Contributor

Modifying a widget after creation will break the contract of the widget manager. After reload it will try to create a widget based on the old uri.

@akosyakov
Copy link
Member

akosyakov commented Oct 1, 2017

Modifying a widget after creation will break the contract of the widget manager. After reload it will try to create a widget based on the old uri.

Won't it flicker, lose an editor context (selection, viewport and so on) and break an existing layout if we just close one and open another?

* check if a widget exists already and rename it
*/
renameWidget(factoryId: string, options: any, newOptions: any, newLabel: any): void {
let key = this.toKey({ factoryId, options });
Copy link
Member

@akosyakov akosyakov Oct 1, 2017

Choose a reason for hiding this comment

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

please check that tslint is enabled in VS code, you should have errors here

let key = this.toKey({ factoryId, options });
let existingWidget = this.widgets.get(key);
if (existingWidget) {
existingWidget.title.label = newLabel;
Copy link
Member

Choose a reason for hiding this comment

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

It should happen in the editor manager, nothing to do with the widget manager

@@ -61,7 +62,9 @@ export class WorkspaceCommandContribution implements CommandContribution {
@inject(FileSystem) protected readonly fileSystem: FileSystem,
@inject(WorkspaceServer) protected readonly workspaceServer: WorkspaceServer,
@inject(SelectionService) protected readonly selectionService: SelectionService,
@inject(OpenerService) protected readonly openerService: OpenerService
@inject(OpenerService) protected readonly openerService: OpenerService,
@inject(EditorManagerImpl) private editorService: EditorManagerImpl
Copy link
Member

Choose a reason for hiding this comment

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

program against the interface not implementation, i.e. EditorManager

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I moved the code over to the EditorManager, this introduced a dependency on the editor package. But now since the editor package also has a dependency on workspace, it won't compile, complaining that editor package doesn't exist (circular dependency). Any idea ?

Copy link
Member

Choose a reason for hiding this comment

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

Could you try to remove a dependency from @theia/editor to @theia/workspace? The latter does not seem to be used in the former.

this.fileSystem.move(uri.toString(), uri.parent.resolve(name).toString())
);
dialog.open().then(name => {
this.fileSystem.move(uri.toString(), uri.parent.resolve(name).toString()).then(() => {
Copy link
Member

@akosyakov akosyakov Oct 1, 2017

Choose a reason for hiding this comment

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

I think @svenefftinge meant that you only should close an existing editor and open new:

const newUri = uri.parent.resolve(name);
this.fileSystem.move(uri.toString(), newUri.toString()).then(() => {
  const widget = this.editorService.getEditor(uri);
  if (editor) {
    const selection = widget.editor.selection;
    widget.close();
    open(this.openerService, newUri, {selection});
  }
});

Ideally, it should be done not here but on move/rename events from the filesystem in the editor extension. Making it here:

  • breaks the unidirectional flow, only the filesystem should be the source of truth;
  • covers only a specific case, e.g. renaming files in Finder won't make the same effect;
  • makes the workspace extension depend on the editor extension.

Copy link
Contributor

@svenefftinge svenefftinge Oct 1, 2017

Choose a reason for hiding this comment

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

No, I did not mean that. A real rename seems better to me in general. But we have to take care of all the loose ends.

Copy link

Choose a reason for hiding this comment

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

Maybe we should have an generic key to reference the widget directly to avoid lose ends ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Could you elaborate on what you mean?

Copy link

Choose a reason for hiding this comment

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

I mean that if something has to keep a reference to an editor widget it should reference the widget object directly or we should provide a key that is not bound to any resource to refer to it. Like some abstract id.

So the URI would be used as key to open a new widget or find a widget for that URI but once you get a reference to it, it should be the id or the object directly, a bit like files names and files descriptors.. you open(filename) get a file descriptor so you could getWidgetForURI() get a widget for a URI but use getWidgetId() if you already had a widget and now know only it's id. I don't see where however that would be useful.. just keeping the object seems ok since serializing that doesn't make sense as the widget-manager is in the frontend.

Modifying a widget after creation will break the contract of the widget manager. After reload it will try to create a widget based on the old uri.

Why would it try to reload with an old uri ? if you updated it before you serialized the widget ?

Granted if it's changed between save and restore there's nothing we can do but that is true whatever we do...

@svenefftinge
Copy link
Contributor

svenefftinge commented Oct 1, 2017

Won't it flicker, lose an editor context (selection, viewport and so on) and break an existing layout if we just close one and open another?

Yes, that's probably not a good solution, but simply changing the widget state doesn't work either.

@lmcgupe
Copy link
Contributor Author

lmcgupe commented Oct 2, 2017 via email

@lmcgupe
Copy link
Contributor Author

lmcgupe commented Oct 2, 2017 via email

@akosyakov
Copy link
Member

akosyakov commented Oct 3, 2017

hum.. with the workspace dependency removed from editor, now it makes another error pop up. In the editor package it complains that preferences is not there.

@theia/editor should not depend on @theia/preferences only on @theia/preferences-api, the import should be replaced with from '@theia/preferences-api'.

@lmcgupe
Copy link
Contributor Author

lmcgupe commented Oct 3, 2017

Changing to preferences-api got one step further. Now it fails to find
[compile] src/browser/editor-manager.ts(16,34): error TS2307: Cannot find module '@theia/filesystem/lib/browser/icons/file-icons'.

@lmcgupe
Copy link
Contributor Author

lmcgupe commented Oct 3, 2017

got over it after adding filesystem dependency inside editor's package.json

@lmcgupe
Copy link
Contributor Author

lmcgupe commented Oct 5, 2017

To implement it we would need also expose an underlying editor resource via TextEditor interface to allow EditorManager to access it here:
I modified common/resource.ts by add an onDidChangeUri. Added a resource field inside browser/editor.ts.
In a similar way what is done with OnDidChangeUriEmitter. Concerning this latter method, I tried hitting a break point inside file-resource.ts, in the onDidChangeContents method, after modifying a file, but it didn't trigger. Is that supposed to work ?

@akosyakov
Copy link
Member

You should put a breakpoint here: https://github.com/theia-ide/theia/blob/31d18c38aa08e6b81d127fdc8de3a03d6796dbbf/packages/filesystem/src/common/file-resource.ts#L28

onDidChangeContents is only hit when someone wants to install a listener, we don't have such clients in the code base, but I used it in the markdown extension:
https://github.com/theia-ide/theia-extension-example/blob/0731d92b3ede2d46e60ddaf61c1e6ff526ad4ad4/theia-markdown/src/browser/markdown-resource.ts#L33-L35

@epatpol
Copy link
Contributor

epatpol commented Oct 6, 2017

@akosyakov I didn't find anything for a file "rename" in chokidar i.e paulmillr/chokidar#303. I guess we would have to implement it chokidar-client side ourselves?

@epatpol
Copy link
Contributor

epatpol commented Oct 6, 2017

Note I think we could do this in 2 steps, first use the UI rename as a trigger to the onDidChangeUri and after use the FileWatcher.

I'm not sure about doing it in 2 steps though, how would the ui trigger the onDidChangeUri method of file-resource? Would we add a temporary method to that resource that can trigger the event from outside (instead of from the fs-watcher). Sorry maybe I misunderstood but I'm having an easier time understanding the event from the FS in the backend then UI -> Resource.OnDidChangeUri.fire() (usually the emitter is private right?).

@akosyakov
Copy link
Member

I meant to look into how vscode using chokidar. I've just looked myself, I was wrong: they are not able to detect rename as well and it only works if it triggered via UI.

@hexa00
Copy link

hexa00 commented Oct 10, 2017

@epatpol even the fs-watcher would need such a method, it would fire it's own moved event I guess and all file resources that match would fire the onDidChangeURI event.

We could do the same with the UI, a move would fire an event and the file resource would listen on that and fire its own event.

If we were to change the UI to the fs-watcher we could just change how fires that first event.

@lmcgupe lmcgupe force-pushed the editor branch 3 times, most recently from 25cbc36 to 476d42c Compare October 13, 2017 15:01
this.fileSystem.move(uri.toString(), uri.parent.resolve(name).toString()).then(() => {
this.editorService.renameWidget(uri.parent.resolve(uri.path.base), uri.parent.resolve(name), name);
let event: IUriChangedEvent = { factoryId: uri.parent.resolve(uri.path.base), options: uri.parent.resolve(name), newOptions: name, newLabel: ' ' };
this.uiWatcher.onIUriChangedEmitter.fire(event);
Copy link

Choose a reason for hiding this comment

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

couple of things

  • you don't need a watcher
  • nothing actually uses that event at the moment ?
  • you still call editorservice rename-widget

It should be that workspace-commands fires the event
and editor service receive it at the very least

Might be other considerations too.. need to think about this more

@lmcgupe lmcgupe force-pushed the editor branch 4 times, most recently from 4c16fa7 to 0280980 Compare October 19, 2017 17:33
@@ -36,8 +36,11 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
(props: FileDialogProps) =>
createFileDialog(ctx.container, props)
);
bind(WorkspaceCommandContribution).toSelf().inSingletonScope();
Copy link

Choose a reason for hiding this comment

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

This should not be there

@@ -56,7 +56,8 @@ export class WidgetManager {

constructor(
@inject(ContributionProvider) @named(WidgetFactory) protected readonly factoryProvider: ContributionProvider<WidgetFactory>,
@inject(ILogger) protected logger: ILogger) {
@inject(ILogger) protected logger: ILogger
) {
Copy link

Choose a reason for hiding this comment

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

no need to change this

getWidgetsMap(): Map<string, Widget> {
return this.widgets;
}

Copy link

Choose a reason for hiding this comment

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

this is wrong you should not expose the internal map

@@ -105,7 +110,7 @@ export class WidgetManager {
return undefined;
}

protected toKey(options: WidgetConstructionOptions) {
public toKey(options: WidgetConstructionOptions) {
Copy link

Choose a reason for hiding this comment

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

You should not need this public either

@@ -16,6 +16,7 @@ import { EditorCommandHandlers } from "./editor-command";
import { EditorKeybindingContribution, EditorKeybindingContext } from "./editor-keybinding";
import { bindEditorPreferences } from './editor-preferences';
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
// import { WorkspaceCommandContribution } from '@theia/workspace/lib/browser/workspace-commands';
Copy link

Choose a reason for hiding this comment

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

remove

options = newOptions;
key = this.widgetManager.toKey({ factoryId, options });
this.widgetManager.getWidgetsMap().set(key, widget);
}
Copy link

Choose a reason for hiding this comment

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

Ditto... this one makes sense in the widget-manager

newOptions: uri.parent.resolve(name).toString(),
newLabel: name
};
this.onIUriChangedEmitter.fire(event);
Copy link

Choose a reason for hiding this comment

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

Actually thinking more about this it would make more sense to have FileSytem.move() fire this event and have the event emitter there.
From the workspace command it looks quite odd..

bind(CommandContribution).to(WorkspaceCommandContribution).inSingletonScope();
bind(MenuContribution).to(FileMenuContribution).inSingletonScope();



Copy link

Choose a reason for hiding this comment

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

remove


protected readonly onIUriChangedEmitter = new Emitter<IUriChangedEvent>();

get onIUriChanged(): Event<IUriChangedEvent> {
Copy link

Choose a reason for hiding this comment

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

We should move this to FileSytem and call it FileMovedEvent and it should be about what moved.

Copy link

Choose a reason for hiding this comment

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

Once the EditorManager receives it it can check if it has a editor for that URI and update it.

) {
this.currentObserver = new EditorManagerImpl.Observer('current', app);
this.activeObserver = new EditorManagerImpl.Observer('active', app);
uiEvent.onIUriChanged(event => {
this.renameWidget(event.factoryId.toString(), event.options, event.newOptions, event.newLabel);
Copy link

Choose a reason for hiding this comment

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

We should get the FileMovedEvent from FS here.
And then figureout the proper way to rename based on the URI change. we have all the info needed.

@marcdumais-work
Copy link
Contributor

This PR is not being actively worked-on - closing for now.

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.

6 participants