/******************************************************************************* * Copyright (c) 2015 Bruno Medeiros and other Contributors. * 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: * Bruno Medeiros - initial API and implementation *******************************************************************************/ package melnorme.lang.ide.ui.text.completion; import static melnorme.utilbox.core.Assert.AssertNamespace.assertNotNull; import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue; import static melnorme.utilbox.core.CoreUtil.array; import java.text.MessageFormat; import org.eclipse.core.resources.IProject; import org.eclipse.jface.bindings.TriggerSequence; import org.eclipse.jface.bindings.keys.KeySequence; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IInformationControlCreator; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.contentassist.ContentAssistEvent; import org.eclipse.jface.text.contentassist.ICompletionListener; import org.eclipse.jface.text.contentassist.ICompletionListenerExtension; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension3; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension4; import org.eclipse.jface.text.contentassist.IContentAssistantExtension2; import org.eclipse.jface.text.contentassist.IContentAssistantExtension3; import org.eclipse.jface.text.contentassist.IContextInformation; import org.eclipse.jface.text.contentassist.IContextInformationValidator; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.keys.IBindingService; import org.eclipse.ui.texteditor.ITextEditor; import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds; import melnorme.lang.ide.core.text.ISourceBufferExt; import melnorme.lang.ide.ui.LangImages; import melnorme.lang.ide.ui.LangUIMessages; import melnorme.lang.ide.ui.editor.EditorUtils; import melnorme.lang.ide.ui.editor.hover.BrowserControlCreator; import melnorme.lang.ide.ui.templates.LangTemplateCompletionProposalComputer; import melnorme.lang.ide.ui.utils.UIOperationsStatusHandler; import melnorme.lang.tooling.toolchain.ops.OperationSoftFailure; import melnorme.lang.tooling.utils.HTMLHelper; import melnorme.utilbox.collections.ArrayList2; import melnorme.utilbox.collections.Indexable; import melnorme.utilbox.concurrency.OperationCancellation; import melnorme.utilbox.core.CommonException; public class LangContentAssistProcessor extends ContenAssistProcessorExt { protected final ContentAssistantExt contentAssistant; protected final Indexable<CompletionProposalsGrouping> categories; protected final ISourceBufferExt sourceBuffer; protected final ITextEditor editor; // can be null protected final IProject project; // can be null public LangContentAssistProcessor(ContentAssistantExt contentAssistant, Indexable<CompletionProposalsGrouping> groupings, ISourceBufferExt sourceBuffer, ITextEditor editor) { this.contentAssistant = assertNotNull(contentAssistant); this.categories = groupings; assertTrue(categories != null && categories.size() > 0); this.sourceBuffer = assertNotNull(sourceBuffer); this.editor = editor; this.project = editor == null ? null : EditorUtils.getAssociatedProject(editor.getEditorInput()); contentAssistant.addCompletionListener(new CompletionSessionListener()); } public static abstract class ContentAssistCategoriesBuilder { public ArrayList2<CompletionProposalsGrouping> getCategories() { ArrayList2<CompletionProposalsGrouping> categories = new ArrayList2<>(); categories.addIfNotNull(createDefaultCategory()); categories.addIfNotNull(createSnippetsCategory()); return categories; } protected CompletionProposalsGrouping createDefaultCategory() { ArrayList2<ILangCompletionProposalComputer> computers = createDefaultCategoryComputers(); return new CompletionProposalsGrouping("default", LangUIMessages.ContentAssistProcessor_defaultProposalCategory, null, computers); } protected ArrayList2<ILangCompletionProposalComputer> createDefaultCategoryComputers() { ArrayList2<ILangCompletionProposalComputer> computers = new ArrayList2<>(); computers.addIfNotNull(createDefaultSymbolsProposalComputer()); computers.addIfNotNull(createSnippetsProposalComputer()); return computers; } protected abstract ILangCompletionProposalComputer createDefaultSymbolsProposalComputer(); protected CompletionProposalsGrouping createSnippetsCategory() { ArrayList2<ILangCompletionProposalComputer> computers = new ArrayList2<>(); computers.addIfNotNull(createSnippetsProposalComputer()); if(computers.isEmpty()) { return null; } return new CompletionProposalsGrouping("snippets", LangUIMessages.ContentAssistProcessor_snippetsProposalCategory, null, computers); } protected ILangCompletionProposalComputer createSnippetsProposalComputer() { return new LangTemplateCompletionProposalComputer(); } } /* ----------------- ----------------- */ protected int invocationIteration = 0; protected boolean isAutoActivation = false; protected class CompletionSessionListener implements ICompletionListener, ICompletionListenerExtension { public CompletionSessionListener() { } @Override public void assistSessionStarted(ContentAssistEvent event) { if(event.processor != LangContentAssistProcessor.this) return; invocationIteration = 0; isAutoActivation = event.isAutoActivated; if (event.assistant instanceof IContentAssistantExtension2) { IContentAssistantExtension2 extension = (IContentAssistantExtension2) event.assistant; KeySequence binding = getGroupingIterationBinding(); boolean repeatedModeEnabled = categories.size() > 1; setRepeatedModeStatus(extension, repeatedModeEnabled, binding); } listener_assistSessionStarted(); } protected void setRepeatedModeStatus(IContentAssistantExtension2 caExt2, boolean enabled, KeySequence binding) { caExt2.setShowEmptyList(enabled); caExt2.setRepeatedInvocationMode(enabled); caExt2.setStatusLineVisible(enabled); if(enabled) { caExt2.setStatusMessage(createIterationMessage()); } if (caExt2 instanceof IContentAssistantExtension3) { IContentAssistantExtension3 ext3 = (IContentAssistantExtension3) caExt2; ext3.setRepeatedInvocationTrigger(binding); } } @Override public void assistSessionRestarted(ContentAssistEvent event) { invocationIteration = 0; } @Override public void assistSessionEnded(ContentAssistEvent event) { if(event.processor != LangContentAssistProcessor.this) return; invocationIteration = 0; listener_assistSessionEnded(); } @Override public void selectionChanged(ICompletionProposal proposal, boolean smartToggle) { } } protected CompletionProposalsGrouping getCurrentCategory() { return getCategory(invocationIteration); } protected CompletionProposalsGrouping getCategory(int categoryIndex) { assertTrue(categoryIndex >= 0); int cappedIndex = categoryIndex % categories.size(); return categories.get(cappedIndex); } /* ----------------- ----------------- */ protected void listener_assistSessionStarted() { for(CompletionProposalsGrouping cat : categories) { cat.sessionStarted(); } } protected void listener_assistSessionEnded() { for(CompletionProposalsGrouping cat : categories) { cat.sessionEnded(); } } @Override protected void resetComputeState() { super.resetComputeState(); // These messages are iteration specific, so they need to be reset: contentAssistant.setStatusMessage(createIterationMessage()); contentAssistant.setEmptyMessage(createEmptyMessage()); } /* ----------------- ----------------- */ @Override protected ICompletionProposal[] doComputeCompletionProposals(ITextViewer viewer, int offset) { CompletionProposalsGrouping cat = getCurrentCategory(); invocationIteration++; Indexable<ICompletionProposal> proposals; try { proposals = cat.computeCompletionProposals(sourceBuffer, viewer, offset); return proposals.toArray(ICompletionProposal.class); } catch(OperationCancellation e) { return null; } catch(CommonException ce) { return returnErrorResult(ce); } catch(OperationSoftFailure e) { String errorMessage = e.getMessage(); if(isAutoActivation) { // don't popup, just display status line error setAndDisplayStatusLineErrorMessage("Error: " + errorMessage); return null; } else { return returnErrorResult(new CommonException(errorMessage)); } } } @Override protected IContextInformation[] doComputeContextInformation(ITextViewer viewer, int offset) { CompletionProposalsGrouping cat = getCurrentCategory(); invocationIteration++; // TODO: make this method like doComputeCompletionProposals Indexable<IContextInformation> proposals = cat.computeContextInformation(sourceBuffer, viewer, offset); setAndDisplayStatusLineErrorMessage(cat.getErrorMessage()); return proposals.toArray(IContextInformation.class); } @Override public IContextInformationValidator getContextInformationValidator() { return null; // TODO: need to add proper support for this } protected ICompletionProposal[] returnErrorResult(CommonException ce) { Display.getCurrent().beep(); return array(new ErrorCompletionProposal(ce)); } protected void setAndDisplayStatusLineErrorMessage(String errorMessage) { this.errorMessage = errorMessage; EditorUtils.setStatusLineErrorMessage(editor, errorMessage, null); Display.getCurrent().beep(); } public static class ErrorCompletionProposal implements ICompletionProposal, ICompletionProposalExtension3, ICompletionProposalExtension4 { protected final CommonException error; public ErrorCompletionProposal(CommonException error) { this.error = assertNotNull(error); } @Override public Image getImage() { return LangImages.NAV_Error.createImage(); } @Override public String getDisplayString() { return "An error occured during Content Assist."; } @Override public String getAdditionalProposalInfo() { String errorMessage = error.getMultiLineRender(); return BrowserControlCreator.wrapHTMLBody("<b>Error:</b><hr/> " + HTMLHelper.escapeToToHTML(errorMessage)); } @Override public Point getSelection(IDocument document) { return null; } @Override public IContextInformation getContextInformation() { return null; } @Override public boolean isAutoInsertable() { return true; } @Override public void apply(IDocument document) { UIOperationsStatusHandler.handleOperationStatus(LangUIMessages.ContentAssistProcessor_opName, error); } @Override public IInformationControlCreator getInformationControlCreator() { return new BrowserControlCreator(); } @Override public CharSequence getPrefixCompletionText(IDocument document, int completionOffset) { return null; } @Override public int getPrefixCompletionStart(IDocument document, int completionOffset) { return completionOffset; } } /* ----------------- Messages ----------------- */ protected String createEmptyMessage() { if(invocationIteration == 0) { return MessageFormat.format(LangUIMessages.ContentAssistProcessor_emptyDefaultProposals, getCurrentCategory().getName()); } return MessageFormat.format(LangUIMessages.ContentAssistProcessor_empty_message, getCurrentCategory().getName()); } protected String createIterationMessage() { TriggerSequence binding = getGroupingIterationBinding(); String nextCategoryLabel = getCategory(invocationIteration + 1).getName(); if(binding == null) { return MessageFormat.format(LangUIMessages.ContentAssistProcessor_toggle_affordance_click_gesture, getCurrentCategory().getName(), nextCategoryLabel, null); } else { return MessageFormat.format(LangUIMessages.ContentAssistProcessor_toggle_affordance_press_gesture, getCurrentCategory().getName(), nextCategoryLabel, binding.format()); } } protected KeySequence getGroupingIterationBinding() { IBindingService bindingSvc = (IBindingService) PlatformUI.getWorkbench().getAdapter(IBindingService.class); TriggerSequence binding = bindingSvc.getBestActiveBindingFor( ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS); if(binding instanceof KeySequence) return (KeySequence) binding; return null; } }