/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.ide.editor;
import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonObject;
import elemental.util.ArrayOf;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.web.bindery.event.shared.EventBus;
import org.eclipse.che.api.promises.client.Operation;
import org.eclipse.che.api.promises.client.OperationException;
import org.eclipse.che.api.promises.client.Promise;
import org.eclipse.che.api.promises.client.PromiseProvider;
import org.eclipse.che.api.promises.client.callback.AsyncPromiseHelper;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.ide.CoreLocalizationConstant;
import org.eclipse.che.ide.actions.LinkWithEditorAction;
import org.eclipse.che.ide.api.component.StateComponent;
import org.eclipse.che.ide.api.constraints.Constraints;
import org.eclipse.che.ide.api.constraints.Direction;
import org.eclipse.che.ide.api.data.HasDataObject;
import org.eclipse.che.ide.api.editor.AsyncEditorProvider;
import org.eclipse.che.ide.api.editor.EditorAgent;
import org.eclipse.che.ide.api.editor.EditorInput;
import org.eclipse.che.ide.api.editor.EditorOpenedEvent;
import org.eclipse.che.ide.api.editor.EditorPartPresenter;
import org.eclipse.che.ide.api.editor.EditorPartPresenter.EditorPartCloseHandler;
import org.eclipse.che.ide.api.editor.EditorProvider;
import org.eclipse.che.ide.api.editor.EditorRegistry;
import org.eclipse.che.ide.api.editor.OpenEditorCallbackImpl;
import org.eclipse.che.ide.api.editor.texteditor.HasReadOnlyProperty;
import org.eclipse.che.ide.api.editor.texteditor.TextEditor;
import org.eclipse.che.ide.api.event.ActivePartChangedEvent;
import org.eclipse.che.ide.api.event.ActivePartChangedHandler;
import org.eclipse.che.ide.api.event.FileEvent;
import org.eclipse.che.ide.api.event.SelectionChangedEvent;
import org.eclipse.che.ide.api.event.SelectionChangedHandler;
import org.eclipse.che.ide.api.event.WindowActionEvent;
import org.eclipse.che.ide.api.event.WindowActionHandler;
import org.eclipse.che.ide.api.filetypes.FileType;
import org.eclipse.che.ide.api.filetypes.FileTypeRegistry;
import org.eclipse.che.ide.api.parts.EditorMultiPartStack;
import org.eclipse.che.ide.api.parts.EditorMultiPartStackState;
import org.eclipse.che.ide.api.parts.EditorPartStack;
import org.eclipse.che.ide.api.parts.EditorTab;
import org.eclipse.che.ide.api.parts.PartPresenter;
import org.eclipse.che.ide.api.parts.PropertyListener;
import org.eclipse.che.ide.api.parts.WorkspaceAgent;
import org.eclipse.che.ide.api.preferences.PreferencesManager;
import org.eclipse.che.ide.api.resources.Resource;
import org.eclipse.che.ide.api.resources.VirtualFile;
import org.eclipse.che.ide.api.selection.Selection;
import org.eclipse.che.ide.api.workspace.event.WorkspaceStoppedEvent;
import org.eclipse.che.ide.editor.synchronization.EditorContentSynchronizer;
import org.eclipse.che.ide.part.editor.multipart.EditorMultiPartStackPresenter;
import org.eclipse.che.ide.part.explorer.project.ProjectExplorerPresenter;
import org.eclipse.che.ide.resource.Path;
import org.eclipse.che.ide.resources.reveal.RevealResourceEvent;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Lists.newArrayList;
import static java.lang.Boolean.parseBoolean;
import static org.eclipse.che.ide.api.parts.PartStackType.EDITING;
/**
* Default implementation of {@link EditorAgent}.
*
* @see EditorAgent
**/
@Singleton
public class EditorAgentImpl implements EditorAgent,
EditorPartCloseHandler,
ActivePartChangedHandler,
SelectionChangedHandler,
WindowActionHandler,
StateComponent,
WorkspaceStoppedEvent.Handler {
private final EventBus eventBus;
private final WorkspaceAgent workspaceAgent;
private final FileTypeRegistry fileTypeRegistry;
private final PreferencesManager preferencesManager;
private final EditorRegistry editorRegistry;
private final CoreLocalizationConstant coreLocalizationConstant;
private final EditorMultiPartStack editorMultiPartStack;
private final List<EditorPartPresenter> openedEditors;
private final Map<EditorPartPresenter, String> openedEditorsToProviders;
private final Provider<EditorContentSynchronizer> editorContentSynchronizerProvider;
private final PromiseProvider promiseProvider;
private final ResourceProvider resourceProvider;
private List<EditorPartPresenter> dirtyEditors;
private EditorPartPresenter activeEditor;
private PartPresenter activePart;
@Inject
public EditorAgentImpl(EventBus eventBus,
FileTypeRegistry fileTypeRegistry,
PreferencesManager preferencesManager,
EditorRegistry editorRegistry,
WorkspaceAgent workspaceAgent,
CoreLocalizationConstant coreLocalizationConstant,
EditorMultiPartStackPresenter editorMultiPartStack,
Provider<EditorContentSynchronizer> editorContentSynchronizerProvider,
PromiseProvider promiseProvider,
ResourceProvider resourceProvider) {
this.eventBus = eventBus;
this.fileTypeRegistry = fileTypeRegistry;
this.preferencesManager = preferencesManager;
this.editorRegistry = editorRegistry;
this.workspaceAgent = workspaceAgent;
this.coreLocalizationConstant = coreLocalizationConstant;
this.editorMultiPartStack = editorMultiPartStack;
this.editorContentSynchronizerProvider = editorContentSynchronizerProvider;
this.promiseProvider = promiseProvider;
this.resourceProvider = resourceProvider;
this.openedEditors = newArrayList();
this.openedEditorsToProviders = new HashMap<>();
eventBus.addHandler(ActivePartChangedEvent.TYPE, this);
eventBus.addHandler(SelectionChangedEvent.TYPE, this);
eventBus.addHandler(WindowActionEvent.TYPE, this);
eventBus.addHandler(WorkspaceStoppedEvent.TYPE, this);
}
@Override
public void onClose(EditorPartPresenter editor) {
closeEditor(editor);
}
@Override
public void onActivePartChanged(ActivePartChangedEvent event) {
activePart = event.getActivePart();
if (!(event.getActivePart() instanceof EditorPartPresenter)) {
return;
}
activeEditor = (EditorPartPresenter)event.getActivePart();
activeEditor.activate();
final String isLinkedWithEditor = preferencesManager.getValue(LinkWithEditorAction.LINK_WITH_EDITOR);
if (parseBoolean(isLinkedWithEditor)) {
final VirtualFile file = activeEditor.getEditorInput().getFile();
eventBus.fireEvent(new RevealResourceEvent(file.getLocation()));
}
}
@Override
public void onWindowClosing(WindowActionEvent event) {
for (EditorPartPresenter editorPartPresenter : getOpenedEditors()) {
if (editorPartPresenter.isDirty()) {
event.setMessage(coreLocalizationConstant.changesMayBeLost()); //TODO need to move this into standalone component
return;
}
}
}
@Override
public void onWindowClosed(WindowActionEvent event) {
//do nothing
}
@Override
public void openEditor(@NotNull final VirtualFile file) {
doOpen(file, new OpenEditorCallbackImpl(), null);
}
@Override
public void openEditor(@NotNull VirtualFile file, Constraints constraints) {
doOpen(file, new OpenEditorCallbackImpl(), constraints);
}
@Override
public void closeEditor(final EditorPartPresenter editor) {
if (editor == null) {
return;
}
final EditorPartStack editorPartStack = editorMultiPartStack.getPartStackByPart(editor);
if (editorPartStack == null) {
return;
}
editor.onClosing(new AsyncCallback<Void>() {
@Override
public void onSuccess(Void result) {
EditorTab editorTab = editorPartStack.getTabByPart(editor);
doCloseEditor(editorTab);
}
@Override
public void onFailure(Throwable caught) {
}
});
}
private void doCloseEditor(EditorTab tab) {
checkArgument(tab != null, "Null editor tab occurred");
EditorPartPresenter editor = tab.getRelativeEditorPart();
if (editor == null) {
return;
}
openedEditors.remove(editor);
openedEditorsToProviders.remove(editor);
editor.close(false);
if (editor instanceof TextEditor) {
editorContentSynchronizerProvider.get().unTrackEditor(editor);
}
if (activeEditor != null && activeEditor == editor) {
activeEditor = null;
}
eventBus.fireEvent(FileEvent.createFileClosedEvent(tab));
}
@Override
public void openEditor(@NotNull VirtualFile file, @NotNull OpenEditorCallback callback) {
doOpen(file, callback, null);
}
private void doOpen(final VirtualFile file, final OpenEditorCallback callback, final Constraints constraints) {
EditorPartStack activePartStack = editorMultiPartStack.getActivePartStack();
if (constraints == null && activePartStack != null) {
PartPresenter partPresenter = activePartStack.getPartByPath(file.getLocation());
if (partPresenter != null) {
workspaceAgent.setActivePart(partPresenter, EDITING);
callback.onEditorActivated((EditorPartPresenter)partPresenter);
return;
}
}
final FileType fileType = fileTypeRegistry.getFileTypeByFile(file);
final EditorProvider editorProvider = editorRegistry.getEditor(fileType);
if (editorProvider instanceof AsyncEditorProvider) {
AsyncEditorProvider provider = (AsyncEditorProvider)editorProvider;
Promise<EditorPartPresenter> promise = provider.createEditor(file);
if (promise != null) {
promise.then(new Operation<EditorPartPresenter>() {
@Override
public void apply(EditorPartPresenter arg) throws OperationException {
initEditor(file, callback, fileType, arg, constraints, editorProvider);
}
});
return;
}
}
final EditorPartPresenter editor = editorProvider.getEditor();
initEditor(file, callback, fileType, editor, constraints, editorProvider);
}
private void initEditor(final VirtualFile file, final OpenEditorCallback callback, FileType fileType,
final EditorPartPresenter editor, final Constraints constraints, EditorProvider editorProvider) {
editor.init(new EditorInputImpl(fileType, file), callback);
editor.addCloseHandler(this);
workspaceAgent.openPart(editor, EDITING, constraints);
finalizeInit(file, callback, editor, editorProvider);
}
private void finalizeInit(final VirtualFile file, final OpenEditorCallback callback, final EditorPartPresenter editor,
EditorProvider editorProvider) {
openedEditors.add(editor);
openedEditorsToProviders.put(editor, editorProvider.getId());
workspaceAgent.setActivePart(editor);
editor.addPropertyListener(new PropertyListener() {
@Override
public void propertyChanged(PartPresenter source, int propId) {
if (propId == EditorPartPresenter.PROP_INPUT) {
if (editor instanceof HasReadOnlyProperty) {
((HasReadOnlyProperty)editor).setReadOnly(file.isReadOnly());
}
if (editor instanceof TextEditor) {
editorContentSynchronizerProvider.get().trackEditor(editor);
}
callback.onEditorOpened(editor);
eventBus.fireEvent(FileEvent.createFileOpenedEvent(file));
eventBus.fireEvent(new EditorOpenedEvent(file, editor));
}
}
});
}
@Override
public void activateEditor(@NotNull EditorPartPresenter editor) {
workspaceAgent.setActivePart(editor);
}
@Override
public List<EditorPartPresenter> getDirtyEditors() {
List<EditorPartPresenter> dirtyEditors = new ArrayList<>();
for (EditorPartPresenter partPresenter : openedEditors) {
if (partPresenter.isDirty()) {
dirtyEditors.add(partPresenter);
}
}
return dirtyEditors;
}
@NotNull
@Override
public List<EditorPartPresenter> getOpenedEditors() {
return newArrayList(openedEditors);
}
@Override
public List<EditorPartPresenter> getOpenedEditorsFor(EditorPartStack editorPartStack) {
List<EditorPartPresenter> result = newArrayList();
for (EditorPartPresenter editor : openedEditors) {
if (editorPartStack.containsPart(editor)) {
result.add(editor);
}
}
return result;
}
@Nullable
@Override
public EditorPartPresenter getOpenedEditor(Path path) {
EditorPartStack editorPartStack = editorMultiPartStack.getPartStackByPart(activeEditor);
return editorPartStack == null ? null : (EditorPartPresenter)editorPartStack.getPartByPath(path);
}
/** {@inheritDoc} */
@Override
public void saveAll(final AsyncCallback callback) {
dirtyEditors = getDirtyEditors();
if (dirtyEditors.isEmpty()) {
callback.onSuccess("Success");
} else {
doSave(callback);
}
}
private void doSave(final AsyncCallback callback) {
final EditorPartPresenter partPresenter = dirtyEditors.get(0);
partPresenter.doSave(new AsyncCallback<EditorInput>() {
@Override
public void onFailure(Throwable caught) {
callback.onFailure(caught);
}
@Override
public void onSuccess(EditorInput result) {
dirtyEditors.remove(partPresenter);
if (dirtyEditors.isEmpty()) {
callback.onSuccess("Success");
} else {
doSave(callback);
}
}
});
}
@Override
public EditorPartPresenter getActiveEditor() {
return activeEditor;
}
@Override
public EditorPartPresenter getNextFor(EditorPartPresenter editorPart) {
return editorMultiPartStack.getNextFor(editorPart);
}
@Override
public EditorPartPresenter getPreviousFor(EditorPartPresenter editorPart) {
return editorMultiPartStack.getPreviousFor(editorPart);
}
@Override
public JsonObject getState() {
JsonObject state = Json.createObject();
EditorMultiPartStackState stacks = null;
try {
stacks = editorMultiPartStack.getState();
} catch (IllegalStateException ignore) {
}
if (stacks != null) {
state.put("FILES", storeEditors(stacks));
}
EditorPartPresenter activeEditor = getActiveEditor();
if (activeEditor != null) {
state.put("ACTIVE_EDITOR", activeEditor.getEditorInput().getFile().getLocation().toString());
}
return state;
}
private JsonObject storeEditors(EditorMultiPartStackState splitStacks) {
JsonObject result = Json.createObject();
if (splitStacks.getEditorPartStack() != null) {
result.put("FILES", storeEditors(splitStacks.getEditorPartStack()));
} else {
result.put("DIRECTION", splitStacks.getDirection().toString());
result.put("SPLIT_FIRST", storeEditors(splitStacks.getSplitFirst()));
result.put("SPLIT_SECOND", storeEditors(splitStacks.getSplitSecond()));
result.put("SIZE", splitStacks.getSize());
}
return result;
}
private JsonArray storeEditors(EditorPartStack partStack) {
JsonArray result = Json.createArray();
int i = 0;
List<EditorPartPresenter> parts = partStack.getParts();
for (EditorPartPresenter part : parts) {
JsonObject file = Json.createObject();
file.put("PATH", part.getEditorInput().getFile().getLocation().toString());
file.put("EDITOR_PROVIDER", openedEditorsToProviders.get(part));
if (part instanceof TextEditor) {
file.put("CURSOR_OFFSET", ((TextEditor)part).getCursorOffset());
file.put("TOP_VISIBLE_LINE", ((TextEditor)part).getTopVisibleLine());
}
if (partStack.getActivePart().equals(part)) {
file.put("ACTIVE", true);
}
result.set(i++, file);
}
return result;
}
@Override
@SuppressWarnings("unchecked")
public void loadState(@NotNull final JsonObject state) {
if (state.hasKey("FILES")) {
JsonObject files = state.getObject("FILES");
EditorPartStack partStack = editorMultiPartStack.createRootPartStack();
final Map<EditorPartPresenter, EditorPartStack> activeEditors = new HashMap<>();
List<Promise<Void>> restore = restore(files, partStack, activeEditors);
Promise<ArrayOf<?>> promise = promiseProvider.all2(restore.toArray(new Promise[restore.size()]));
promise.then(new Operation() {
@Override
public void apply(Object arg) throws OperationException {
String activeFile = "";
if (state.hasKey("ACTIVE_EDITOR")) {
activeFile = state.getString("ACTIVE_EDITOR");
}
EditorPartPresenter activeEditorPart = null;
for (Map.Entry<EditorPartPresenter, EditorPartStack> entry : activeEditors.entrySet()) {
entry.getValue().setActivePart(entry.getKey());
if (activeFile.equals(entry.getKey().getEditorInput().getFile().getLocation().toString())) {
activeEditorPart = entry.getKey();
}
}
workspaceAgent.setActivePart(activeEditorPart);
}
});
}
}
private List<Promise<Void>> restore(JsonObject files, EditorPartStack editorPartStack,
Map<EditorPartPresenter, EditorPartStack> activeEditors) {
if (files.hasKey("FILES")) {
//plain
JsonArray filesArray = files.getArray("FILES");
List<Promise<Void>> promises = new ArrayList<>();
for (int i = 0; i < filesArray.length(); i++) {
JsonObject file = filesArray.getObject(i);
Promise<Void> openFile = openFile(file, editorPartStack, activeEditors);
promises.add(openFile);
}
return promises;
} else {
//split
return restoreSplit(files, editorPartStack, activeEditors);
}
}
private List<Promise<Void>> restoreSplit(JsonObject files, EditorPartStack editorPartStack,
Map<EditorPartPresenter, EditorPartStack> activeEditors) {
JsonObject splitFirst = files.getObject("SPLIT_FIRST");
String direction = files.getString("DIRECTION");
double size = files.getNumber("SIZE");
EditorPartStack split = editorMultiPartStack.split(editorPartStack, new Constraints(Direction.valueOf(direction), null), size);
List<Promise<Void>> restoreFirst = restore(splitFirst, editorPartStack, activeEditors);
JsonObject splitSecond = files.getObject("SPLIT_SECOND");
List<Promise<Void>> restoreSecond = restore(splitSecond, split, activeEditors);
List<Promise<Void>> result = new ArrayList<>();
result.addAll(restoreFirst);
result.addAll(restoreSecond);
return result;
}
private Promise<Void> openFile(final JsonObject file, final EditorPartStack editorPartStack,
final Map<EditorPartPresenter, EditorPartStack> activeEditors) {
return AsyncPromiseHelper.createFromAsyncRequest(new AsyncPromiseHelper.RequestCall<Void>() {
@Override
public void makeCall(final AsyncCallback<Void> callback) {
String path = file.getString("PATH");
resourceProvider.getResource(path).then(new Operation<java.util.Optional<VirtualFile>>() {
@Override
public void apply(java.util.Optional<VirtualFile> optionalFile) throws OperationException {
if (optionalFile.isPresent()) {
restoreCreateEditor(optionalFile.get(), file, editorPartStack, callback, activeEditors);
} else {
callback.onSuccess(null);
}
}
});
}
});
}
private void restoreCreateEditor(final VirtualFile resourceFile, JsonObject file, final EditorPartStack editorPartStack,
final AsyncCallback<Void> openCallback,
final Map<EditorPartPresenter, EditorPartStack> activeEditors) {
String providerId = file.getString("EDITOR_PROVIDER");
final OpenEditorCallback callback;
if (file.hasKey("CURSOR_OFFSET") && file.hasKey("TOP_VISIBLE_LINE")) {
final int cursorOffset = (int)file.getNumber("CURSOR_OFFSET");
final int topLine = (int)file.getNumber("TOP_VISIBLE_LINE");
callback = new RestoreStateEditorCallBack(cursorOffset, topLine);
} else {
callback = new OpenEditorCallbackImpl();
}
final boolean active = file.hasKey("ACTIVE") && file.getBoolean("ACTIVE");
final EditorProvider provider = editorRegistry.findEditorProviderById(providerId);
if (provider instanceof AsyncEditorProvider) {
((AsyncEditorProvider)provider).createEditor(resourceFile).then(new Operation<EditorPartPresenter>() {
@Override
public void apply(EditorPartPresenter arg) throws OperationException {
restoreInitEditor(resourceFile, callback, fileTypeRegistry.getFileTypeByFile(resourceFile), arg, provider,
editorPartStack);
if (active) {
activeEditors.put(arg, editorPartStack);
}
}
});
} else {
EditorPartPresenter editor = provider.getEditor();
restoreInitEditor(resourceFile, callback, fileTypeRegistry.getFileTypeByFile(resourceFile), editor, provider, editorPartStack);
if (active) {
activeEditors.put(editor, editorPartStack);
}
}
openCallback.onSuccess(null);
}
private void restoreInitEditor(final VirtualFile file, final OpenEditorCallback callback, FileType fileType,
final EditorPartPresenter editor, EditorProvider editorProvider, EditorPartStack editorPartStack) {
editor.init(new EditorInputImpl(fileType, file), callback);
editor.addCloseHandler(this);
editorPartStack.addPart(editor);
finalizeInit(file, callback, editor, editorProvider);
}
@Override
public void onSelectionChanged(SelectionChangedEvent event) {
final String isLinkedWithEditor = preferencesManager.getValue(LinkWithEditorAction.LINK_WITH_EDITOR);
if (!parseBoolean(isLinkedWithEditor)) {
return;
}
final Selection<?> selection = event.getSelection();
if (selection instanceof Selection.NoSelectionProvided) {
return;
}
Resource currentResource = null;
if (selection == null || selection.getHeadElement() == null || selection.getAllElements().size() > 1) {
return;
}
final Object headObject = selection.getHeadElement();
if (headObject instanceof HasDataObject) {
Object data = ((HasDataObject)headObject).getData();
if (data instanceof Resource) {
currentResource = (Resource)data;
}
} else if (headObject instanceof Resource) {
currentResource = (Resource)headObject;
}
EditorPartStack activePartStack = editorMultiPartStack.getActivePartStack();
if (currentResource == null || activePartStack == null || activeEditor == null) {
return;
}
final Path locationOfActiveOpenedFile = activeEditor.getEditorInput().getFile().getLocation();
final Path selectedResourceLocation = currentResource.getLocation();
if (!(activePart instanceof ProjectExplorerPresenter) &&
selectedResourceLocation.equals(locationOfActiveOpenedFile)) {
return;
}
PartPresenter partPresenter = activePartStack.getPartByPath(selectedResourceLocation);
if (partPresenter != null) {
workspaceAgent.setActivePart(partPresenter, EDITING);
}
}
@Override
public void onWorkspaceStopped(WorkspaceStoppedEvent event) {
for (EditorPartPresenter editor : getOpenedEditors()) {
closeEditor(editor);
}
}
private static class RestoreStateEditorCallBack extends OpenEditorCallbackImpl {
private final int cursorOffset;
private final int topLine;
public RestoreStateEditorCallBack(int cursorOffset, int topLine) {
this.cursorOffset = cursorOffset;
this.topLine = topLine;
}
@Override
public void onEditorOpened(EditorPartPresenter editor) {
if (editor instanceof TextEditor) {
TextEditor textEditor = (TextEditor)editor;
textEditor.getCursorModel().setCursorPosition(cursorOffset);
}
}
@Override
public void onEditorActivated(EditorPartPresenter editor) {
if (editor instanceof TextEditor) {
((TextEditor)editor).setTopLine(topLine);
}
}
}
}