/** * Copyright (c) 2013 Madhuranga Lakjeewa. * 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: * Madhuranga Lakjeewa - initial API and implementation. */ package org.eclipse.recommenders.internal.snipmatch.rcp.completion; import static org.eclipse.jface.text.IDocument.DEFAULT_CONTENT_TYPE; import static org.eclipse.recommenders.internal.snipmatch.rcp.Constants.PREF_SEARCH_BOX_BACKGROUND; import org.eclipse.core.commands.ExecutionException; import org.eclipse.jdt.internal.ui.text.template.contentassist.TemplateInformationControlCreator; import org.eclipse.jdt.ui.text.java.ContentAssistInvocationContext; import org.eclipse.jface.resource.ColorRegistry; import org.eclipse.jface.resource.FontRegistry; import org.eclipse.jface.text.contentassist.ContentAssistEvent; import org.eclipse.jface.text.contentassist.ContentAssistant; import org.eclipse.jface.text.contentassist.ICompletionListener; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.recommenders.internal.snipmatch.rcp.l10n.Messages; import org.eclipse.recommenders.snipmatch.ISnippet; import org.eclipse.recommenders.snipmatch.rcp.SnippetAppliedEvent; import org.eclipse.recommenders.utils.Nullable; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.custom.VerifyKeyListener; import org.eclipse.swt.events.FocusAdapter; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Caret; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; import com.google.common.base.Throwables; import com.google.common.eventbus.EventBus; /** * Snipmatch snippet completion engine. * <p> * Makes use of JFace content assist infrastructure - but in a bit unusual way. The event handling is probably calling * for trouble later. */ @SuppressWarnings("restriction") public class SnipmatchCompletionEngine<T extends ContentAssistInvocationContext> { private static enum AssistantControlState { KEEP_OPEN, ENABLE_HIDE } private static final int SEARCH_BOX_WIDTH = 273; private final T context; private final AbstractContentAssistProcessor<T> processor; private final String filename; private final EventBus bus; private final ColorRegistry colorRegistry; private final FontRegistry fontRegistry; private final ContentAssistant assistant; private Shell searchShell; private ICompletionProposal selectedProposal; private StyledText searchText; private AssistantControlState state; public SnipmatchCompletionEngine(T context, AbstractContentAssistProcessor<T> processor, @Nullable String filename, EventBus bus, ColorRegistry colorRegistry, FontRegistry fontRegistry) { this.context = context; this.processor = processor; this.filename = filename; this.bus = bus; this.colorRegistry = colorRegistry; this.fontRegistry = fontRegistry; assistant = newContentAssistant(); } private ContentAssistant newContentAssistant() { ContentAssistant assistant = new ContentAssistant() { @Override public void hide() { if (isFocused(searchText) && state != AssistantControlState.ENABLE_HIDE) { // Ignore } else { super.hide(); selectedProposal = null; } } private boolean isFocused(Control control) { Control focusControl = PlatformUI.getWorkbench().getDisplay().getFocusControl(); return control.equals(focusControl); } }; assistant.addCompletionListener(new ICompletionListener() { @Override public void assistSessionEnded(ContentAssistEvent event) { selectedProposal = null; if (searchShell != null) { searchShell.dispose(); } } @Override public void selectionChanged(ICompletionProposal proposal, boolean smartToggle) { selectedProposal = proposal; } @Override public void assistSessionStarted(ContentAssistEvent event) { } }); assistant.setShowEmptyList(true); assistant.enablePrefixCompletion(true); assistant.enableColoredLabels(true); assistant.setRepeatedInvocationMode(true); assistant.setStatusLineVisible(false); assistant.setContentAssistProcessor(processor, DEFAULT_CONTENT_TYPE); assistant.setInformationControlCreator(new TemplateInformationControlCreator(SWT.LEFT_TO_RIGHT)); assistant.setSorter(new ProposalSorter()); return assistant; } public void show() { processor.setContext(context); processor.setFilename(filename); assistant.install(context.getViewer()); state = AssistantControlState.KEEP_OPEN; createSearchPopup(); } private void execute(String commandId) { try { assistant.getHandler(commandId).execute(null); } catch (ExecutionException e) { Throwables.propagate(e); } } private void createSearchPopup() { Shell parentShell = context.getViewer().getTextWidget().getShell(); searchShell = new Shell(parentShell, SWT.ON_TOP); searchShell.setLayout(new FillLayout()); searchShell.addListener(SWT.Traverse, new Listener() { @Override public void handleEvent(Event e) { if (e.detail == SWT.TRAVERSE_ESCAPE) { state = AssistantControlState.ENABLE_HIDE; assistant.uninstall(); } } }); searchText = new StyledText(searchShell, SWT.SINGLE); searchText.setFont(fontRegistry.get("org.eclipse.recommenders.snipmatch.rcp.searchTextFont")); //$NON-NLS-1$ searchText.setBackground(colorRegistry.get(PREF_SEARCH_BOX_BACKGROUND)); searchText.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { if (!assistant.hasProposalPopupFocus()) { state = AssistantControlState.ENABLE_HIDE; PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { @Override public void run() { assistant.uninstall(); } }); } } }); searchText.addVerifyKeyListener(new VerifyKeyListener() { @Override public void verifyKey(VerifyEvent e) { ICompletionProposal appliedProposal = selectedProposal; switch (e.character) { case SWT.CR: e.doit = false; if (appliedProposal instanceof SnippetProposal) { SnippetProposal snippetProposal = (SnippetProposal) appliedProposal; state = AssistantControlState.ENABLE_HIDE; assistant.uninstall(); if (snippetProposal.isValidFor(context.getDocument(), context.getInvocationOffset())) { snippetApplied(snippetProposal); } snippetProposal.apply(context.getViewer(), (char) 0, SWT.NONE, context.getInvocationOffset()); Point selection = snippetProposal.getSelection(context.getDocument()); if (selection != null) { context.getViewer().setSelectedRange(selection.x, selection.y); context.getViewer().revealRange(selection.x, selection.y); } } else { state = AssistantControlState.ENABLE_HIDE; assistant.uninstall(); } return; case SWT.TAB: e.doit = false; return; } // there is no navigation to support if no proposal is selected: if (appliedProposal == null) { return; } // but if there is, let's navigate... switch (e.keyCode) { case SWT.ARROW_UP: execute(ContentAssistant.SELECT_PREVIOUS_PROPOSAL_COMMAND_ID); if (selectedProposal instanceof RepositoryProposal) { execute(ContentAssistant.SELECT_PREVIOUS_PROPOSAL_COMMAND_ID); } return; case SWT.ARROW_DOWN: execute(ContentAssistant.SELECT_NEXT_PROPOSAL_COMMAND_ID); if (selectedProposal instanceof RepositoryProposal) { execute(ContentAssistant.SELECT_NEXT_PROPOSAL_COMMAND_ID); } return; } } }); searchText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { String query = searchText.getText().trim(); processor.setTerms(query); assistant.setEmptyMessage(Messages.COMPLETION_ENGINE_NO_SNIPPETS_FOUND); assistant.showPossibleCompletions(); assistant.showContextInformation(); if (selectedProposal instanceof RepositoryProposal) { execute(ContentAssistant.SELECT_NEXT_PROPOSAL_COMMAND_ID); } } }); placeShell(); searchShell.open(); searchShell.setFocus(); } private void placeShell() { // Pack the shell so that it is high enough for the text field. searchShell.pack(); int searchBoxHeight = searchShell.getSize().y; StyledText editorText = context.getViewer().getTextWidget(); Caret caret = editorText.getCaret(); int lineHeight = caret.getSize().y; Point location = caret.getLocation(); Point anchor = editorText.toDisplay(location.x, location.y + lineHeight - searchBoxHeight); searchShell.setLocation(anchor.x, anchor.y); searchShell.setSize(SEARCH_BOX_WIDTH, searchBoxHeight); } private void snippetApplied(SnippetProposal proposal) { ISnippet snippet = proposal.getSnippet(); String repoUri = null; // TODO How to get the repo uri? bus.post(new SnippetAppliedEvent(snippet.getUuid(), repoUri)); } }