/******************************************************************************* * 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 org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IInformationControlCreator; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension2; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension3; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension4; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension5; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension6; import org.eclipse.jface.text.contentassist.IContentAssistant; import org.eclipse.jface.text.contentassist.IContextInformation; import org.eclipse.jface.text.link.ILinkedModeListener; import org.eclipse.jface.text.link.LinkedModeModel; import org.eclipse.jface.text.link.LinkedModeUI; import org.eclipse.jface.text.link.LinkedModeUI.ExitFlags; import org.eclipse.jface.text.link.LinkedModeUI.IExitPolicy; import org.eclipse.jface.text.link.LinkedPosition; import org.eclipse.jface.text.link.LinkedPositionGroup; import org.eclipse.jface.viewers.StyledString; import org.eclipse.swt.SWT; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.ui.texteditor.link.EditorLinkedModeUI; import melnorme.lang.ide.core.LangCore; import melnorme.lang.ide.core.text.TextSourceUtils; import melnorme.lang.ide.core.utils.EclipseUtils; import melnorme.lang.ide.ui.editor.EditorSourceBuffer.DocumentSourceBuffer; import melnorme.lang.ide.ui.editor.ISourceViewerExt; import melnorme.lang.ide.ui.editor.hover.BrowserControlCreator; import melnorme.lang.ide.ui.text.DocDisplayInfoSupplier; import melnorme.lang.tooling.ToolCompletionProposal; import melnorme.lang.tooling.ast.SourceRange; import melnorme.lang.tooling.toolchain.ops.SourceOpContext; import melnorme.utilbox.collections.Indexable; import melnorme.utilbox.misc.Location; public class LangCompletionProposal implements ICompletionProposal, ICompletionProposalExtension, ICompletionProposalExtension2, ICompletionProposalExtension3, ICompletionProposalExtension4, ICompletionProposalExtension5, ICompletionProposalExtension6 { protected final SourceOpContext sourceOpContext; protected final ToolCompletionProposal proposal; protected volatile String additionalProposalInfo; protected final Image image; protected final IContextInformation contextInformation; protected int relevance = 0; protected int replaceLength; protected StyledString styledDisplayString; public LangCompletionProposal(SourceOpContext sourceOpContext, ToolCompletionProposal proposal, Image image, IContextInformation contextInformation) { super(); this.sourceOpContext = assertNotNull(sourceOpContext); this.proposal = assertNotNull(proposal); this.additionalProposalInfo = proposal.getDocumentation(); this.image = image; this.contextInformation = contextInformation; this.relevance = getDefaultRelevance(); this.replaceLength = proposal.getReplaceLength(); } protected int getReplaceOffset() { return proposal.getReplaceOffset(); } protected int getReplaceLength() { return replaceLength; } public String getBaseReplaceString() { return proposal.getBaseReplaceString(); } public String getEffectiveReplaceString(boolean nameOnly) { if(nameOnly) { return proposal.getBaseReplaceString(); } return proposal.getFullReplaceString(); } public int getRelevance() { return relevance; } protected int getDefaultRelevance() { String underlyingElementName = getUnderlyingElementName(); if(underlyingElementName != null && underlyingElementName.startsWith("_")) { // In C-style languages, identifiers that start with "_" are typically reserved values, // so make it less relevant return 10; } return 0; } protected String getUnderlyingElementName() { return proposal.getLabel(); } public String getSortString() { return proposal.getLabel(); } @Override public String getDisplayString() { return proposal.getLabel(); } @Override public StyledString getStyledDisplayString() { if(styledDisplayString == null) { StyledString styledString = new StyledString(proposal.getLabel()); getStyledDisplayString_TypeLabel(styledString); getStyledDisplayString_ModuleName(styledString); styledDisplayString = styledString; } return styledDisplayString; } protected void getStyledDisplayString_TypeLabel(StyledString styledString) { if(proposal.getTypeLabel() != null) { styledString.append(new StyledString(" " + proposal.getTypeLabel(), StyledString.DECORATIONS_STYLER)); } } protected void getStyledDisplayString_ModuleName(StyledString styledString) { if(proposal.getModuleName() != null) { styledString.append(new StyledString(" - " + proposal.getModuleName(), StyledString.QUALIFIER_STYLER)); } } @Override public Image getImage() { return image; }; @Override public IContextInformation getContextInformation() { return contextInformation; }; @Override public int getContextInformationPosition() { return -1; } protected ContentAssistantExt caext; @Override public void selected(ITextViewer viewer, boolean smartToggle) { if(viewer instanceof ISourceViewerExt) { ISourceViewerExt sourceViewer = (ISourceViewerExt) viewer; IContentAssistant ca = sourceViewer.getContentAssistant(); if(ca instanceof ContentAssistantExt) { caext = (ContentAssistantExt) ca; if(!isAutoInsertable()) { caext.setAdditionalStatusMessage("Press 'Ctrl+Enter' for name-only insertion;"); } else { caext.setAdditionalStatusMessage(null); caext = null; } } } } @Override public void unselected(ITextViewer viewer) { if(caext != null) { caext.setAdditionalStatusMessage(null); } } protected IInformationControlCreator informationControlCreator; @Override public IInformationControlCreator getInformationControlCreator() { if(informationControlCreator == null) { informationControlCreator = new BrowserControlCreator(); } return informationControlCreator; } @Override public String getAdditionalProposalInfo() { Object info = getAdditionalProposalInfo(new NullProgressMonitor()); return info != null ? info.toString() : null; }; protected final Object additionalProposalInfo_mutex = new Object(); @Override public Object getAdditionalProposalInfo(IProgressMonitor monitor) { synchronized (additionalProposalInfo_mutex) { if(additionalProposalInfo == null) { additionalProposalInfo = doGetAdditionalProposalInfo(monitor); } } return additionalProposalInfo; } protected String doGetAdditionalProposalInfo(IProgressMonitor monitor) { Document tempDocument = new Document(sourceOpContext.getSource()); doApply(tempDocument, false); try { tempDocument.replace(endPositionAfterApply, 0, " "); } catch(BadLocationException e) { } DocumentSourceBuffer tempSourceBuffer = new DocumentSourceBuffer(tempDocument) { @Override public Location getLocation_orNull() { return sourceOpContext.getOptionalFileLocation().orElse(null); } }; String doc = new DocDisplayInfoSupplier(tempSourceBuffer, getReplaceOffset()) .doGetDocumentation(EclipseUtils.om(monitor)); if(doc == null) { return null; } return BrowserControlCreator.wrapHTMLBody(doc); } /* ----------------- Application ----------------- */ @Override public char[] getTriggerCharacters() { return null; } @Override public boolean isValidFor(IDocument document, int offset) { return validate(document, offset, null); } @Override public boolean validate(IDocument document, int offset, DocumentEvent event) { if(offset < getReplaceOffset()) return false; String prefix; try { prefix = document.get(getReplaceOffset(), offset - getReplaceOffset()); } catch (BadLocationException e) { return false; } boolean validPrefix = isValidPrefix(prefix); if(validPrefix && event != null) { // adapt replacement length to document event/ int eventEndOffset = event.fOffset + event.fLength; // The event should be a common prefix completion (this should be true anyways) : int replaceEndPos = getReplaceOffset() + getReplaceLength(); if(event.fOffset >= getReplaceOffset() && eventEndOffset <= replaceEndPos) { int delta = (event.fText == null ? 0 : event.fText.length()) - event.fLength; this.replaceLength = Math.max(getReplaceLength() + delta, 0); } } return validPrefix; } protected boolean isValidPrefix(String prefix) { String rplString = getBaseReplaceString(); return TextSourceUtils.isPrefix(prefix, rplString, true); } @Override public int getPrefixCompletionStart(IDocument document, int completionOffset) { return getReplaceOffset(); } @Override public CharSequence getPrefixCompletionText(IDocument document, int completionOffset) { return getBaseReplaceString(); } @Override public boolean isAutoInsertable() { return proposal.getBaseReplaceString().equals(proposal.getFullReplaceString()); } @Override public void apply(IDocument document) { apply(document, (char) 0, 0); } @Override public void apply(IDocument document, char trigger, int offset) { doApply(document, false); } protected int endPositionAfterApply; protected Point positionAfterApply; public void doApply(IDocument document, boolean nameOnly) { int replaceOffset = getReplaceOffset(); String effectiveReplaceString = getEffectiveReplaceString(nameOnly); int replaceLength = getReplaceLength(); endPositionAfterApply = replaceOffset + effectiveReplaceString.length(); positionAfterApply = new Point(endPositionAfterApply, 0); try { document.replace(replaceOffset, replaceLength, effectiveReplaceString); } catch (BadLocationException x) { // ignore } } @Override public Point getSelection(IDocument document) { return positionAfterApply; } @Override public void apply(ITextViewer viewer, char trigger, int stateMask, int offset) { boolean nameOnly = false; if((stateMask & SWT.CTRL) != 0) { nameOnly = true; } doApply(viewer.getDocument(), nameOnly); if(nameOnly) { return; } try { applyLinkedMode(viewer); } catch (BadLocationException e) { LangCore.logInternalError(e); } } protected void applyLinkedMode(ITextViewer viewer) throws BadLocationException { LinkedModeModel model = getLinkedModeModel(viewer); if(model == null) { return; } model.forceInstall(); LinkedModeUI ui = new EditorLinkedModeUI(model, viewer); ui.setExitPolicy(new CompletionProposalExitPolicy()); ui.setExitPosition(viewer, endPositionAfterApply, 0, Integer.MAX_VALUE); if(firstLinkedModeGroupPosition != -1) { positionAfterApply = null; } ui.setCyclingMode(LinkedModeUI.CYCLE_WHEN_NO_PARENT); ui.setDoContextInfo(true); ui.enableColoredLabels(true); ui.enter(); } protected int firstLinkedModeGroupPosition; protected LinkedModeModel getLinkedModeModel(ITextViewer viewer) throws BadLocationException { Indexable<SourceRange> sourceSubElements = proposal.getSourceSubElements(); if(sourceSubElements == null || sourceSubElements.isEmpty()) { return null; } LinkedModeModel model = new LinkedModeModel(); IDocument document = viewer.getDocument(); int replaceOffset = getReplaceOffset(); firstLinkedModeGroupPosition = -1; for (SourceRange sr : sourceSubElements) { LinkedPositionGroup group = new LinkedPositionGroup(); int posOffset = replaceOffset + sr.getOffset(); group.addPosition(new LinkedPosition(document, posOffset, sr.getLength())); if(firstLinkedModeGroupPosition == -1) { firstLinkedModeGroupPosition = posOffset; } model.addGroup(group); } return model; } protected class CompletionProposalExitPolicy implements IExitPolicy { @Override public ExitFlags doExit(LinkedModeModel model, VerifyEvent event, int offset, int length) { switch (event.character) { case SWT.CR: int endOfReplacement = getReplaceOffset() + getEffectiveReplaceString(false).length(); if(offset == endOfReplacement) { return new ExitFlags(ILinkedModeListener.EXIT_ALL, true); } return new ExitFlags(ILinkedModeListener.UPDATE_CARET, false); } return null; } } /* ----------------- ----------------- */ @Override public String toString() { return proposal.toString(); } }