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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions packages/core/src/browser/widget-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

}

getWidgets(factoryId: string): Widget[] {
Expand All @@ -69,6 +70,10 @@ export class WidgetManager {
return result;
}

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

/*
* creates or returns the widget for the given description.
*/
Expand Down Expand Up @@ -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

return JSON.stringify(options);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/cpp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"@theia/editor": "^0.1.1",
"@theia/monaco": "^0.1.1",
"@theia/filesystem": "^0.1.1",
"@theia/preferences": "^0.1.1"
"@theia/preferences-api": "^0.1.1"
},
"publishConfig": {
"access": "public"
Expand Down
2 changes: 1 addition & 1 deletion packages/cpp/src/common/cpp-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
PreferenceService,
PreferenceContribution,
PreferenceSchema
} from '@theia/preferences/lib/common';
} from '@theia/preferences-api/lib/common';

export const CppConfigSchema: PreferenceSchema = {
"type": "object",
Expand Down
3 changes: 2 additions & 1 deletion packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"description": "Theia - Editor Extension",
"dependencies": {
"@theia/core": "^0.1.1",
"@theia/preferences": "^0.1.1",
"@theia/preferences-api": "^0.1.1",
"@theia/filesystem": "^0.1.1",
"@theia/workspace": "^0.1.1"
},
"publishConfig": {
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/src/browser/editor-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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


export default new ContainerModule(bind => {
bindEditorPreferences(bind);
Expand All @@ -30,4 +31,5 @@ export default new ContainerModule(bind => {
bind(EditorKeybindingContext).toSelf().inSingletonScope();
bind(KeybindingContext).toDynamicValue(context => context.container.get(EditorKeybindingContext));
bind(KeybindingContribution).to(EditorKeybindingContribution);
// 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.

remove

});
32 changes: 30 additions & 2 deletions packages/editor/src/browser/editor-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import { TextEditorProvider, Range, Position } from "./editor";
import { WidgetFactory, WidgetManager } from '@theia/core/lib/browser/widget-manager';
import { Widget } from '@phosphor/widgets';
import { FileIconProvider } from '@theia/filesystem/lib/browser/icons/file-icons';
import { WorkspaceCommandContribution } from "@theia/workspace/lib/browser/workspace-commands";

export const EditorManager = Symbol("EditorManager");

export interface EditorManager extends OpenHandler {

/**
* All opened editors.
*/
Expand All @@ -27,6 +29,7 @@ export interface EditorManager extends OpenHandler {
* Reject if the given input is not an editor input or an editor cannot be opened.
*/
open(uri: URI, input?: EditorInput): Promise<EditorWidget>;

Copy link

Choose a reason for hiding this comment

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

remove

/**
* The most recently focused editor.
*/
Expand Down Expand Up @@ -64,10 +67,14 @@ export class EditorManagerImpl implements EditorManager, WidgetFactory {
@inject(SelectionService) protected readonly selectionService: SelectionService,
@inject(FrontendApplication) protected readonly app: FrontendApplication,
@inject(WidgetManager) protected readonly widgetManager: WidgetManager,
@inject(FileIconProvider) protected readonly iconProvider: FileIconProvider
@inject(FileIconProvider) protected readonly iconProvider: FileIconProvider,
@inject(WorkspaceCommandContribution) protected readonly uiEvent: WorkspaceCommandContribution
) {
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.

});
}

get editors() {
Expand Down Expand Up @@ -105,6 +112,28 @@ export class EditorManagerImpl implements EditorManager, WidgetFactory {
});
}

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

Choose a reason for hiding this comment

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

This is just externalizing your original implementation into the widget manager.
You should use EditorManager methosd to get to your widget.

}

/*
* update a widget key following a rename
*/
private updateWidgetKey(key: any, widget: Widget, factoryId: string, options: any, newOptions: any): void {
this.widgetManager.getWidgetsMap().delete(key);
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


// don't call directly, but use WidgetManager
createWidget(uriAsString: string): Promise<Widget> {
const uri = new URI(uriAsString);
Expand Down Expand Up @@ -155,7 +184,6 @@ export class EditorManagerImpl implements EditorManager, WidgetFactory {
}
return undefined;
}

}

export namespace EditorManagerImpl {
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/browser/editor-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
PreferenceContribution,
PreferenceSchema,
PreferenceChangeEvent
} from '@theia/preferences/lib/common';
} from '@theia/preferences-api/lib/common';

export const editorPreferenceSchema: PreferenceSchema = {
"type": "object",
Expand Down
5 changes: 4 additions & 1 deletion packages/filesystem/src/common/file-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class FileResource implements Resource {

protected readonly toDispose = new DisposableCollection();
protected readonly onDidChangeContentsEmitter = new Emitter<void>();
protected readonly onDidChangeUriEmitter = new Emitter<void>();

constructor(
readonly uri: URI,
Expand Down Expand Up @@ -51,6 +52,9 @@ export class FileResource implements Resource {
return this.onDidChangeContentsEmitter.event;
}

get onDidChangeUri(): Event<void> {
return this.onDidChangeUriEmitter.event;
}
}

@injectable()
Expand All @@ -72,5 +76,4 @@ export class FileResourceResolver implements ResourceResolver {
throw new Error('The given uri is a directory: ' + uri);
});
}

}
21 changes: 21 additions & 0 deletions packages/filesystem/src/node/node-filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ import { injectable, inject, optional } from "inversify";
import URI from "@theia/core/lib/common/uri";
import { FileUri } from "@theia/core/lib/node";
import { FileStat, FileSystem } from "../common/filesystem";
import { Emitter, Event } from '@theia/core/lib/common/event';

export interface IUriChangedEvent {
factoryId: string;
options: any;
newOptions: any;
newLabel: any;
}


@injectable()
export class FileSystemNodeOptions {
Expand All @@ -34,6 +43,11 @@ export class FileSystemNodeOptions {
@injectable()
export class FileSystemNode implements FileSystem {

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

get onIUriChanged(): Event<IUriChangedEvent> {
return this.onIUriChangedEmitter.event;
}
constructor(
@inject(FileSystemNodeOptions) @optional() protected readonly options: FileSystemNodeOptions = FileSystemNodeOptions.default
) { }
Expand Down Expand Up @@ -154,6 +168,13 @@ export class FileSystemNode implements FileSystem {
if (error) {
return reject(error);
}
let event: IUriChangedEvent = {
factoryId: "code-editor-opener",
options: _sourceUri.parent.resolve(_sourceUri.path.base).toString(),
newOptions: options,
newLabel: FileUri.fsPath(_targetUri).toString()
};
this.onIUriChangedEmitter.fire(event);
resolve(this.doGetStat(_targetUri, 1));
});
}
Expand Down
2 changes: 1 addition & 1 deletion packages/monaco/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"dependencies": {
"@theia/core": "^0.1.1",
"@theia/filesystem": "^0.1.1",
"@theia/preferences": "^0.1.1",
"@theia/preferences-api": "^0.1.1",
"@theia/workspace": "^0.1.1",
"@theia/languages": "^0.1.1",
"@theia/editor": "^0.1.1",
Expand Down
1 change: 1 addition & 0 deletions packages/workspace/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "Theia - Workspace Extension",
"dependencies": {
"@theia/core": "^0.1.1",
"@theia/editor": "^0.1.1",
"@theia/filesystem": "^0.1.1"
},
"publishConfig": {
Expand Down
40 changes: 37 additions & 3 deletions packages/workspace/src/browser/workspace-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { UriSelection } from '@theia/filesystem/lib/common/filesystem-selection'
import { SingleTextInputDialog, ConfirmDialog } from "@theia/core/lib/browser/dialogs";
import { OpenerService, OpenHandler, open } from "@theia/core/lib/browser";
import { WorkspaceService } from './workspace-service';
import { Emitter, Event } from '@theia/core/lib/common/event';

export namespace WorkspaceCommands {
export const NEW_FILE = 'file:newFile';
Expand All @@ -39,6 +40,22 @@ export namespace FileMenus {
export const OPEN_GROUP = [...FILE, '2_open'];
}

// @injectable()
// export class UiEvent {
// public onIUriChangedEmitter = new Emitter<IUriChangedEvent>();

// get onIUriChanged(): Event<IUriChangedEvent> {
// return this.onIUriChangedEmitter.event;
// }
// }

export interface IUriChangedEvent {
factoryId: string;
options: any;
newOptions: any;
newLabel: any;
}

@injectable()
export class FileMenuContribution implements MenuContribution {

Expand All @@ -64,6 +81,15 @@ export class WorkspaceCommandContribution implements CommandContribution {
@inject(OpenerService) protected readonly openerService: OpenerService
) { }




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.

return this.onIUriChangedEmitter.event;
}

registerCommands(registry: CommandRegistry): void {
registry.registerCommand({
id: WorkspaceCommands.NEW_FILE,
Expand Down Expand Up @@ -104,9 +130,17 @@ export class WorkspaceCommandContribution implements CommandContribution {
initialValue: uri.path.base,
validate: name => this.validateFileName(name, parent)
});
dialog.open().then(name =>
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...

let event: IUriChangedEvent = {
factoryId: "code-editor-opener",
options: uri.parent.resolve(uri.path.base).toString(),
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..

});
});
})
}));

Expand Down
3 changes: 3 additions & 0 deletions packages/workspace/src/browser/workspace-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

rebind(StorageService).to(WorkspaceStorageService).inSingletonScope();
});