/******************************************************************************* * 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.orion.client; import elemental.events.KeyboardEvent.KeyCode; import org.eclipse.che.ide.api.editor.annotation.AnnotationModel; import org.eclipse.che.ide.api.editor.annotation.HasAnnotationRendering; import org.eclipse.che.ide.api.editor.annotation.QueryAnnotationsEvent; import org.eclipse.che.ide.api.editor.changeintercept.ChangeInterceptorProvider; import org.eclipse.che.ide.api.editor.changeintercept.TextChange; import org.eclipse.che.ide.api.editor.changeintercept.TextChangeInterceptor; import org.eclipse.che.ide.api.editor.codeassist.CodeAssistCallback; import org.eclipse.che.ide.api.editor.codeassist.CodeAssistProcessor; import org.eclipse.che.ide.api.editor.codeassist.CodeAssistant; import org.eclipse.che.ide.api.editor.codeassist.CodeAssistantFactory; import org.eclipse.che.ide.api.editor.codeassist.CompletionProposal; import org.eclipse.che.ide.api.editor.codeassist.CompletionReadyCallback; import org.eclipse.che.ide.api.editor.codeassist.CompletionsSource; import org.eclipse.che.ide.api.editor.document.Document; import org.eclipse.che.ide.api.editor.document.DocumentHandle; import org.eclipse.che.ide.api.editor.editorconfig.TextEditorConfiguration; import org.eclipse.che.ide.api.editor.events.CompletionRequestEvent; import org.eclipse.che.ide.api.editor.events.CompletionRequestHandler; import org.eclipse.che.ide.api.editor.events.DocumentChangeEvent; import org.eclipse.che.ide.api.editor.events.TextChangeEvent; import org.eclipse.che.ide.api.editor.events.TextChangeHandler; import org.eclipse.che.ide.api.editor.formatter.ContentFormatter; import org.eclipse.che.ide.api.editor.keymap.KeyBinding; import org.eclipse.che.ide.api.editor.keymap.KeyBindingAction; import org.eclipse.che.ide.api.editor.partition.DocumentPartitioner; import org.eclipse.che.ide.api.editor.position.PositionConverter; import org.eclipse.che.ide.api.editor.quickfix.QuickAssistAssistant; import org.eclipse.che.ide.api.editor.reconciler.Reconciler; import org.eclipse.che.ide.api.editor.signature.SignatureHelpProvider; import org.eclipse.che.ide.api.editor.text.TextPosition; import org.eclipse.che.ide.api.editor.text.TypedRegion; import org.eclipse.che.ide.api.editor.texteditor.HasKeyBindings; import org.eclipse.che.ide.api.editor.texteditor.TextEditor; import org.eclipse.che.ide.util.browser.UserAgent; import java.util.List; import java.util.Map; import java.util.logging.Logger; /** * Initialization controller for the text editor. * Sets-up (when available) the different components that depend on the document being ready. */ public class OrionEditorInit { /** The logger. */ private static final Logger LOG = Logger.getLogger(OrionEditorInit.class.getName()); private static final String CONTENT_ASSIST = "Content assist"; private static final String QUICK_FIX = "Quick fix"; private final TextEditorConfiguration configuration; private final CodeAssistantFactory codeAssistantFactory; private final OrionEditorPresenter textEditor; private final QuickAssistAssistant quickAssist; /** * The quick assist assistant. */ public OrionEditorInit(final TextEditorConfiguration configuration, final CodeAssistantFactory codeAssistantFactory, final QuickAssistAssistant quickAssist, final OrionEditorPresenter textEditor) { this.configuration = configuration; this.codeAssistantFactory = codeAssistantFactory; this.quickAssist = quickAssist; this.textEditor = textEditor; } /** * Initialize the text editor. * * @param document to initialise with */ public void init(Document document) { DocumentHandle documentHandle = document.getDocumentHandle(); configurePartitioner(documentHandle); configureReconciler(documentHandle); configureAnnotationModel(documentHandle); configureCodeAssist(documentHandle); configureChangeInterceptors(documentHandle); configureFormatter(textEditor); configureSignatureHelp(textEditor); addQuickAssistKeyBinding(); } public void uninstall() { Reconciler reconciler = configuration.getReconciler(); if (reconciler != null) { reconciler.uninstall(); } SignatureHelpProvider signatureHelpProvider = configuration.getSignatureHelpProvider(); if (signatureHelpProvider != null) { signatureHelpProvider.uninstall(); } } private void configureSignatureHelp(TextEditor textEditor) { SignatureHelpProvider signatureHelpProvider = configuration.getSignatureHelpProvider(); if (signatureHelpProvider != null) { signatureHelpProvider.install(textEditor); } } private void configureFormatter(OrionEditorPresenter textEditor) { ContentFormatter formatter = configuration.getContentFormatter(); if (formatter != null) { formatter.install(textEditor); } } /** * 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(textEditor); } } /** * 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) if (textEditor instanceof HasAnnotationRendering) { ((HasAnnotationRendering)textEditor).configure(annotationModel, documentHandle); } 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 Map<String, 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()); for (String key : processors.keySet()) { codeAssistant.setCodeAssistantProcessor(key, processors.get(key)); } final KeyBindingAction action = new KeyBindingAction() { @Override public boolean action() { showCompletion(codeAssistant, true); return true; } }; final HasKeyBindings hasKeyBindings = this.textEditor.getHasKeybindings(); hasKeyBindings.addKeyBinding(new KeyBinding(true, false, false, false, KeyCode.SPACE, action), CONTENT_ASSIST); // 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, false); } }); } else { final KeyBindingAction action = new KeyBindingAction() { @Override public boolean action() { showCompletion(); return true; } }; final HasKeyBindings hasKeyBindings = this.textEditor.getHasKeybindings(); if (UserAgent.isMac()) { hasKeyBindings.addKeyBinding(new KeyBinding(false, false, false, true, KeyCode.SPACE, action), CONTENT_ASSIST); hasKeyBindings.addKeyBinding(new KeyBinding(false, false, true, true, KeyCode.SPACE, action), CONTENT_ASSIST); } else { hasKeyBindings.addKeyBinding(new KeyBinding(true, false, false, false, KeyCode.SPACE, action), CONTENT_ASSIST); } // 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 * @param triggered if triggered by the content assist key binding */ private void showCompletion(final CodeAssistant codeAssistant, final boolean triggered) { 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, triggered, 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(); } /** * Add key binding to quick assist assistant. */ private void addQuickAssistKeyBinding() { if (this.quickAssist != null) { final KeyBindingAction action = new KeyBindingAction() { @Override public boolean action() { final PositionConverter positionConverter = textEditor.getPositionConverter(); if (positionConverter != null) { textEditor.showQuickAssist(); } return true; } }; final HasKeyBindings hasKeyBindings = this.textEditor.getHasKeybindings(); hasKeyBindings.addKeyBinding(new KeyBinding(false, false, true, false, KeyCode.ENTER, action), QUICK_FIX); } } 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; } } } }); } } }