/*******************************************************************************
* Copyright (c) 2012-2015 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.jseditor.client.texteditor;
import java.util.List;
import java.util.logging.Logger;
import org.eclipse.che.ide.api.text.TypedRegion;
import org.eclipse.che.ide.collections.StringMap;
import org.eclipse.che.ide.collections.StringMap.IterationCallback;
import org.eclipse.che.ide.jseditor.client.annotation.AnnotationModel;
import org.eclipse.che.ide.jseditor.client.annotation.ClearAnnotationModelEvent;
import org.eclipse.che.ide.jseditor.client.annotation.GutterAnnotationRenderer;
import org.eclipse.che.ide.jseditor.client.annotation.InlineAnnotationRenderer;
import org.eclipse.che.ide.jseditor.client.annotation.MinimapAnnotationRenderer;
import org.eclipse.che.ide.jseditor.client.annotation.QueryAnnotationsEvent;
import org.eclipse.che.ide.jseditor.client.changeintercept.ChangeInterceptorProvider;
import org.eclipse.che.ide.jseditor.client.changeintercept.TextChange;
import org.eclipse.che.ide.jseditor.client.changeintercept.TextChangeInterceptor;
import org.eclipse.che.ide.jseditor.client.codeassist.CodeAssistCallback;
import org.eclipse.che.ide.jseditor.client.codeassist.CodeAssistProcessor;
import org.eclipse.che.ide.jseditor.client.codeassist.CompletionProposal;
import org.eclipse.che.ide.jseditor.client.codeassist.CompletionReadyCallback;
import org.eclipse.che.ide.jseditor.client.codeassist.CompletionsSource;
import org.eclipse.che.ide.jseditor.client.document.DocumentHandle;
import org.eclipse.che.ide.jseditor.client.events.CompletionRequestHandler;
import org.eclipse.che.ide.jseditor.client.events.GutterClickEvent;
import org.eclipse.che.ide.jseditor.client.events.TextChangeHandler;
import org.eclipse.che.ide.jseditor.client.keymap.KeyBindingAction;
import org.eclipse.che.ide.jseditor.client.keymap.Keybinding;
import org.eclipse.che.ide.jseditor.client.partition.DocumentPartitioner;
import org.eclipse.che.ide.jseditor.client.position.PositionConverter;
import org.eclipse.che.ide.jseditor.client.quickfix.QuickAssistProcessor;
import org.eclipse.che.ide.jseditor.client.quickfix.QuickAssistantFactory;
import org.eclipse.che.ide.jseditor.client.reconciler.Reconciler;
import org.eclipse.che.ide.jseditor.client.annotation.AnnotationModelEvent;
import org.eclipse.che.ide.jseditor.client.codeassist.CodeAssistant;
import org.eclipse.che.ide.jseditor.client.codeassist.CodeAssistantFactory;
import org.eclipse.che.ide.jseditor.client.editorconfig.TextEditorConfiguration;
import org.eclipse.che.ide.jseditor.client.events.CompletionRequestEvent;
import org.eclipse.che.ide.jseditor.client.events.DocumentChangeEvent;
import org.eclipse.che.ide.jseditor.client.events.GutterClickHandler;
import org.eclipse.che.ide.jseditor.client.events.TextChangeEvent;
import org.eclipse.che.ide.jseditor.client.events.doc.DocReadyWrapper;
import org.eclipse.che.ide.jseditor.client.events.doc.DocReadyWrapper.DocReadyInit;
import org.eclipse.che.ide.jseditor.client.gutter.Gutters;
import org.eclipse.che.ide.jseditor.client.gutter.HasGutter;
import org.eclipse.che.ide.jseditor.client.minimap.HasMinimap;
import org.eclipse.che.ide.jseditor.client.quickfix.QuickAssistAssistant;
import org.eclipse.che.ide.jseditor.client.text.TextPosition;
import com.google.web.bindery.event.shared.EventBus;
import elemental.events.KeyboardEvent.KeyCode;
import elemental.events.MouseEvent;
/**
* Initialization controller for the text editor.
* Sets-up (when available) the different components that depend on the document being ready.
*/
public class TextEditorInit<T extends EditorWidget> {
/** The logger. */
private static final Logger LOG = Logger.getLogger(TextEditorInit.class.getName());
private final TextEditorConfiguration configuration;
private final EventBus generalEventBus;
private final CodeAssistantFactory codeAssistantFactory;
private final QuickAssistantFactory quickAssistantFactory;
private final EmbeddedTextEditorPresenter<T> textEditor;
/**
* The quick assist assistant.
*/
private QuickAssistAssistant quickAssist;
public TextEditorInit(final TextEditorConfiguration configuration,
final EventBus generalEventBus,
final CodeAssistantFactory codeAssistantFactory,
final QuickAssistantFactory quickAssistantFactory,
final EmbeddedTextEditorPresenter<T> textEditor) {
this.configuration = configuration;
this.generalEventBus = generalEventBus;
this.codeAssistantFactory = codeAssistantFactory;
this.quickAssistantFactory = quickAssistantFactory;
this.textEditor = textEditor;
}
/**
* Initialize the text editor.
* Sets itself as {@link org.eclipse.che.ide.jseditor.client.events.DocumentReadyEvent} handler.
*/
public void init() {
final DocReadyInit<TextEditorInit<T>> init = new DocReadyInit<TextEditorInit<T>>() {
@Override
public void initialize(final DocumentHandle documentHandle, final TextEditorInit<T> wrapped) {
configurePartitioner(documentHandle);
configureReconciler(documentHandle);
configureAnnotationModel(documentHandle);
configureCodeAssist(documentHandle);
configureQuickAssist(documentHandle);
configureChangeInterceptors(documentHandle);
}
};
new DocReadyWrapper<TextEditorInit<T>>(generalEventBus, this.textEditor.getEditorHandle(), init, this);
}
/**
* Configures the editor's DocumentPartitioner.
* @param documentHandle the handle to the document
*/
private void configurePartitioner(final DocumentHandle documentHandle) {
final DocumentPartitioner partitioner = configuration.getPartitioner();
if (partitioner != null) {
partitioner.setDocumentHandle(documentHandle);
documentHandle.getDocEventBus().addHandler(DocumentChangeEvent.TYPE, partitioner);
partitioner.initialize();
}
}
/**
* Configures the editor's Reconciler.
* @param documentHandle the handle to the document
*/
private void configureReconciler(final DocumentHandle documentHandle) {
final Reconciler reconciler = configuration.getReconciler();
if (reconciler != null) {
reconciler.setDocumentHandle(documentHandle);
documentHandle.getDocEventBus().addHandler(DocumentChangeEvent.TYPE, reconciler);
reconciler.install();
}
}
/**
* Configures the editor's annotation model.
* @param documentHandle the handle on the editor
*/
private void configureAnnotationModel(final DocumentHandle documentHandle) {
final AnnotationModel annotationModel = configuration.getAnnotationModel();
if (annotationModel == null) {
return;
}
// add the renderers (event handler) before the model (event source)
// gutter renderer
if (textEditor instanceof HasGutter && ((HasGutter)this.textEditor).getGutter() != null) {
final GutterAnnotationRenderer annotationRenderer = new GutterAnnotationRenderer();
annotationRenderer.setDocument(documentHandle.getDocument());
annotationRenderer.setHasGutter(((HasGutter)this.textEditor).getGutter());
documentHandle.getDocEventBus().addHandler(AnnotationModelEvent.TYPE, annotationRenderer);
documentHandle.getDocEventBus().addHandler(ClearAnnotationModelEvent.TYPE, annotationRenderer);
}
// inline renderer
final InlineAnnotationRenderer inlineAnnotationRenderer = new InlineAnnotationRenderer();
inlineAnnotationRenderer.setDocument(documentHandle.getDocument());
inlineAnnotationRenderer.setHasTextMarkers(this.textEditor.getHasTextMarkers());
documentHandle.getDocEventBus().addHandler(AnnotationModelEvent.TYPE, inlineAnnotationRenderer);
documentHandle.getDocEventBus().addHandler(ClearAnnotationModelEvent.TYPE, inlineAnnotationRenderer);
// minimap renderer
if (this.textEditor instanceof HasMinimap && ((HasMinimap)this.textEditor).getMinimap() != null) {
final MinimapAnnotationRenderer minimapAnnotationRenderer = new MinimapAnnotationRenderer();
minimapAnnotationRenderer.setDocument(documentHandle.getDocument());
minimapAnnotationRenderer.setMinimap(((HasMinimap)this.textEditor).getMinimap());
documentHandle.getDocEventBus().addHandler(AnnotationModelEvent.TYPE, minimapAnnotationRenderer);
documentHandle.getDocEventBus().addHandler(ClearAnnotationModelEvent.TYPE, minimapAnnotationRenderer);
}
annotationModel.setDocumentHandle(documentHandle);
documentHandle.getDocEventBus().addHandler(DocumentChangeEvent.TYPE, annotationModel);
// the model listens to QueryAnnotation events
documentHandle.getDocEventBus().addHandler(QueryAnnotationsEvent.TYPE, annotationModel);
}
/**
* Configure the editor's code assistant.
* @param documentHandle the handle on the document
*/
private void configureCodeAssist(final DocumentHandle documentHandle) {
if (this.codeAssistantFactory == null) {
return;
}
final StringMap<CodeAssistProcessor> processors = configuration.getContentAssistantProcessors();
if (processors != null && !processors.isEmpty()) {
LOG.info("Creating code assistant.");
final CodeAssistant codeAssistant = this.codeAssistantFactory.create(this.textEditor,
this.configuration.getPartitioner());
processors.iterate(new IterationCallback<CodeAssistProcessor>() {
@Override
public void onIteration(final String key, final CodeAssistProcessor value) {
codeAssistant.setCodeAssistantProcessor(key, value);
}
});
final KeyBindingAction action = new KeyBindingAction() {
@Override
public void action() {
showCompletion(codeAssistant);
}
};
final HasKeybindings hasKeybindings = this.textEditor.getHasKeybindings();
hasKeybindings.addKeybinding(new Keybinding(true, false, false, false, KeyCode.SPACE, action));
// handle CompletionRequest events that come from text operations instead of simple key binding
documentHandle.getDocEventBus().addHandler(CompletionRequestEvent.TYPE, new CompletionRequestHandler() {
@Override
public void onCompletionRequest(final CompletionRequestEvent event) {
showCompletion(codeAssistant);
}
});
} else {
final KeyBindingAction action = new KeyBindingAction() {
@Override
public void action() {
showCompletion();
}
};
final HasKeybindings hasKeybindings = this.textEditor.getHasKeybindings();
hasKeybindings.addKeybinding(new Keybinding(true, false, false, false, KeyCode.SPACE, action));
// handle CompletionRequest events that come from text operations instead of simple key binding
documentHandle.getDocEventBus().addHandler(CompletionRequestEvent.TYPE, new CompletionRequestHandler() {
@Override
public void onCompletionRequest(final CompletionRequestEvent event) {
showCompletion();
}
});
}
}
/**
* Show the available completions.
*
* @param codeAssistant the code assistant
*/
private void showCompletion(final CodeAssistant codeAssistant) {
final int cursor = textEditor.getCursorOffset();
if (cursor < 0) {
return;
}
final CodeAssistProcessor processor = codeAssistant.getProcessor(cursor);
if (processor != null) {
this.textEditor.showCompletionProposals(new CompletionsSource() {
@Override
public void computeCompletions(final CompletionReadyCallback callback) {
// cursor must be computed here again so it's original value is not baked in
// the SMI instance closure - important for completion update when typing
final int cursor = textEditor.getCursorOffset();
codeAssistant.computeCompletionProposals(cursor, new CodeAssistCallback() {
@Override
public void proposalComputed(final List<CompletionProposal> proposals) {
callback.onCompletionReady(proposals);
}
});
}
});
} else {
showCompletion();
}
}
/** Show the available completions. */
private void showCompletion() {
this.textEditor.showCompletionProposals();
}
/**
* Sets up the quick assist assistant.
* @param documentHandle the handle to the document
*/
private void configureQuickAssist(final DocumentHandle documentHandle) {
final QuickAssistProcessor processor = configuration.getQuickAssistProcessor();
if (this.quickAssistantFactory != null && processor != null) {
this.quickAssist = quickAssistantFactory.createQuickAssistant(this.textEditor);
this.quickAssist.setQuickAssistProcessor(processor);
documentHandle.getDocEventBus().addHandler(GutterClickEvent.TYPE, new GutterClickHandler() {
@Override
public void onGutterClick(final GutterClickEvent event) {
if (Gutters.ANNOTATION_GUTTER.equals(event.getGutterId())) {
final MouseEvent originalEvent = event.getEvent();
showQuickAssistant(event.getLineNumber(),
originalEvent.getClientX(),
originalEvent.getClientY());
}
}
});
//add a key binding
final KeyBindingAction action = new KeyBindingAction() {
@Override
public void action() {
final PositionConverter positionConverter = textEditor.getPositionConverter();
if (positionConverter != null) {
final TextPosition cursor = textEditor.getCursorPosition();
final int lineNumber = cursor.getLine();
final PositionConverter.PixelCoordinates pixelPos = positionConverter.textToPixel(cursor);
showQuickAssistant(lineNumber, pixelPos.getX(), pixelPos.getY());
}
}
};
final HasKeybindings hasKeybindings = this.textEditor.getHasKeybindings();
hasKeybindings.addKeybinding(new Keybinding(true, false, false, false, KeyCode.ONE, action));
hasKeybindings.addKeybinding(new Keybinding(true, false, false, false, KeyCode.NUM_ONE, action));
}
}
private void showQuickAssistant(final int lineNumber, int clientX, int clientY) {
quickAssist.showPossibleQuickAssists(lineNumber, clientX, clientY);
}
private void configureChangeInterceptors(final DocumentHandle documentHandle) {
final ChangeInterceptorProvider interceptors = configuration.getChangeInterceptorProvider();
if (interceptors != null) {
documentHandle.getDocEventBus().addHandler(TextChangeEvent.TYPE, new TextChangeHandler() {
@Override
public void onTextChange(final TextChangeEvent event) {
final TextChange change = event.getChange();
if (change == null) {
return;
}
final TextPosition from = change.getFrom();
if (from == null) {
return;
}
final int startOffset = documentHandle.getDocument().getIndexFromPosition(from);
final TypedRegion region = configuration.getPartitioner().getPartition(startOffset);
if (region == null) {
return;
}
final List<TextChangeInterceptor> filteredInterceptors = interceptors.getInterceptors(region.getType());
if (filteredInterceptors == null || filteredInterceptors.isEmpty()) {
return;
}
// don't apply the interceptors if the range end doesn't belong to the same partition
final TextPosition to = change.getTo();
if (to != null && ! from.equals(to)) {
final int endOffset = documentHandle.getDocument().getIndexFromPosition(to);
if (endOffset < region.getOffset() || endOffset > region.getOffset() + region.getLength()) {
return;
}
}
// stop as soon as one interceptors has modified the content
for (final TextChangeInterceptor interceptor: filteredInterceptors) {
final TextChange result = interceptor.processChange(change,
documentHandle.getDocument().getReadOnlyDocument());
if (result != null) {
event.update(result);
break;
}
}
}
});
}
}
}