/*******************************************************************************
* Copyright (c) 2007, 2017 Alphonse Van Assche and others.
* 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:
* Alphonse Van Assche - initial API and implementation
* Neil Guzman - autocomplete with "." and "+" in name (B#375195)
*******************************************************************************/
package org.eclipse.linuxtools.internal.rpm.ui.editor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.contentassist.IContextInformationValidator;
import org.eclipse.jface.text.templates.DocumentTemplateContext;
import org.eclipse.jface.text.templates.Template;
import org.eclipse.jface.text.templates.TemplateContext;
import org.eclipse.jface.text.templates.TemplateContextType;
import org.eclipse.jface.text.templates.TemplateException;
import org.eclipse.jface.text.templates.TemplateProposal;
import org.eclipse.linuxtools.internal.rpm.ui.editor.parser.SpecfileSource;
import org.eclipse.linuxtools.internal.rpm.ui.editor.scanners.SpecfilePartitionScanner;
import org.eclipse.linuxtools.rpm.ui.editor.parser.Specfile;
import org.eclipse.linuxtools.rpm.ui.editor.parser.SpecfileDefine;
import org.eclipse.linuxtools.rpm.ui.editor.parser.SpecfileParser;
import org.eclipse.linuxtools.rpm.ui.editor.parser.SpecfileSection;
/**
* Content assist processor.
*/
public class SpecfileCompletionProcessor implements IContentAssistProcessor {
private static final String SOURCE = "SOURCE"; //$NON-NLS-1$
private static final String EMPTY_STRING = ""; //$NON-NLS-1$
/**
* Comparator implementation for a generic proposal
*
* @author Sami Wagiaalla
*/
private static final String TEMPLATE_ICON = "icons/template_obj.gif"; //$NON-NLS-1$
private static final String MACRO_ICON = "icons/macro_obj.gif"; //$NON-NLS-1$
private static final String PATCH_ICON = "icons/macro_obj.gif"; //$NON-NLS-1$
private static final String PACKAGE_ICON = "icons/rpm.gif"; //$NON-NLS-1$
private static final String PREAMBLE_SECTION_TEMPLATE = "org.eclipse.linuxtools.rpm.ui.editor.preambleSection"; //$NON-NLS-1$
private static final String PRE_SECTION_TEMPLATE = "org.eclipse.linuxtools.rpm.ui.editor.preSection"; //$NON-NLS-1$
private static final String BUILD_SECTION_TEMPLATE = "org.eclipse.linuxtools.rpm.ui.editor.buildSection"; //$NON-NLS-1$
private static final String INSTALL_SECTION_TEMPLATE = "org.eclipse.linuxtools.rpm.ui.editor.installSection"; //$NON-NLS-1$
private static final String CHANGELOG_SECTION_TEMPLATE = "org.eclipse.linuxtools.rpm.ui.editor.changelogSection"; //$NON-NLS-1$
@Override
public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
List<ICompletionProposal> result = new ArrayList<>();
Specfile specfile = new SpecfileParser().parse(viewer.getDocument());
String prefix = completionWord(viewer.getDocument(), offset);
Region region = new Region(offset - prefix.length(), prefix.length());
// RPM macro's are useful in the whole specfile.
List<ICompletionProposal> rpmMacroProposals = computeRpmMacroProposals(region, specfile, prefix);
// Sources completion
List<ICompletionProposal> sourcesProposals = computeSourcesProposals(region, specfile, prefix);
result.addAll(sourcesProposals);
// Get the current content type
String currentContentType = viewer.getDocument().getDocumentPartitioner().getContentType(region.getOffset());
if (currentContentType.equals(SpecfilePartitionScanner.SPEC_PREP)) {
List<ICompletionProposal> patchesProposals = computePatchesProposals(region, specfile, prefix);
result.addAll(patchesProposals);
}
if (currentContentType.equals(SpecfilePartitionScanner.SPEC_PACKAGES)) {
// don't show template in the RPM packages content type.
// (when the line begin with Requires, BuildRequires etc...)
List<ICompletionProposal> rpmPackageProposals = computeRpmPackageProposals(region, prefix);
result.addAll(rpmPackageProposals);
result.addAll(rpmMacroProposals);
} else {
// don't show RPM packages proposals in all others content type.
List<? extends ICompletionProposal> templateProposals = computeTemplateProposals(viewer, region, specfile,
prefix);
result.addAll(templateProposals);
result.addAll(rpmMacroProposals);
}
if (currentContentType.equals(SpecfilePartitionScanner.SPEC_GROUP)) {
IDocument document = viewer.getDocument();
try {
int lineNumber = document.getLineOfOffset(region.getOffset());
int lineOffset = document.getLineOffset(lineNumber);
if (region.getOffset() - lineOffset > 5) {
result.clear();
String groupPrefix = getGroupPrefix(viewer, offset);
result.addAll(computeRpmGroupProposals(region, groupPrefix));
}
} catch (BadLocationException e) {
SpecfileLog.logError(e);
}
}
return result.toArray(new ICompletionProposal[result.size()]);
}
/**
* Compute the templates proposals, these proposals are contextual on
* sections. Return an array of template proposals for the given viewer,
* region, specfile, prefix.
*
* @param viewer
* the viewer for which the context is created
* @param region
* the region into <code>document</code> for which the context is
* created
* @param specfile
* the specfile element
* @param prefix
* the prefix string
* @return a ICompletionProposal[]
*/
private List<? extends ICompletionProposal> computeTemplateProposals(ITextViewer viewer, IRegion region,
Specfile specfile, String prefix) {
TemplateContext context = createContext(viewer, region, specfile);
List<TemplateProposal> matches = new ArrayList<>();
if (context == null) {
return matches;
}
ITextSelection selection = (ITextSelection) viewer.getSelectionProvider().getSelection();
context.setVariable("selection", selection.getText()); //$NON-NLS-1$
String id = context.getContextType().getId();
Template[] templates = Activator.getDefault().getTemplateStore().getTemplates(id);
for (Template template : templates) {
try {
context.getContextType().validate(template.getPattern());
} catch (TemplateException e) {
continue;
}
int relevance = getRelevance(template, prefix);
if (relevance > 0) {
matches.add(new TemplateProposal(template, context, region,
Activator.getDefault().getImage(TEMPLATE_ICON), relevance));
}
}
Collections.sort(matches, (t1, t2) -> (t2.getRelevance() - t1.getRelevance()));
return matches;
}
/**
* Compute RPM macro proposals, these proposals are usable in the whole
* document. Return an array of RPM macro proposals for the given viewer,
* region, prefix.
*
* @param viewer
* the viewer for which the context is created
* @param region
* the region into <code>document</code> for which the context is
* created
* @param prefix
* the prefix string to find
* @return a ICompletionProposal[]
*/
private List<ICompletionProposal> computeRpmMacroProposals(IRegion region, Specfile specfile, String prefix) {
Map<String, String> rpmMacroProposalsMap = Activator.getDefault().getRpmMacroList().getProposals(prefix);
// grab defines and put them into the proposals map
rpmMacroProposalsMap.putAll(getDefines(specfile, prefix));
ArrayList<ICompletionProposal> proposals = new ArrayList<>();
if (rpmMacroProposalsMap != null) {
for (Map.Entry<String, String> entry : rpmMacroProposalsMap.entrySet()) {
proposals.add(new SpecCompletionProposal(
ISpecfileSpecialSymbols.MACRO_START_LONG + entry.getKey().substring(1)
+ ISpecfileSpecialSymbols.MACRO_END_LONG,
region.getOffset(), region.getLength(), entry.getKey().length() + 2,
Activator.getDefault().getImage(MACRO_ICON), entry.getKey(), null, entry.getValue()));
}
}
return proposals;
}
/**
* Compute patches proposals, these proposals are usable in the whole
* document. Return an array of patches proposals for the given viewer,
* region, prefix.
*
* @param viewer
* the viewer for which the context is created
* @param region
* the region into <code>document</code> for which the context is
* created
* @param prefix
* the prefix string to find
* @return a ICompletionProposal[]
*/
private List<ICompletionProposal> computePatchesProposals(IRegion region, Specfile specfile, String prefix) {
// grab patches and put them into the proposals map
Map<String, String> patchesProposalsMap = getPatches(specfile, prefix);
ArrayList<ICompletionProposal> proposals = new ArrayList<>();
if (patchesProposalsMap != null) {
for (Map.Entry<String, String> entry : patchesProposalsMap.entrySet()) {
proposals.add(new SpecCompletionProposal(entry.getKey(), region.getOffset(), region.getLength(),
entry.getKey().length(), Activator.getDefault().getImage(PATCH_ICON), entry.getKey(), null,
entry.getValue()));
}
}
return proposals;
}
/**
* Compute sources proposals, these proposals are usable in the whole
* document. Return an array of sources proposals for the given viewer,
* region, prefix.
*
* @param viewer
* the viewer for which the context is created
* @param region
* the region into <code>document</code> for which the context is
* created
* @param prefix
* the prefix string to find
* @return a ICompletionProposal[]
*/
private List<ICompletionProposal> computeSourcesProposals(IRegion region, Specfile specfile, String prefix) {
// grab patches and put them into the proposals map
Map<String, String> sourcesProposalsMap = getSources(specfile, prefix);
ArrayList<ICompletionProposal> proposals = new ArrayList<>();
if (sourcesProposalsMap != null) {
for (Map.Entry<String, String> entry : sourcesProposalsMap.entrySet()) {
proposals.add(new SpecCompletionProposal(entry.getKey(), region.getOffset(), region.getLength(),
entry.getKey().length(), Activator.getDefault().getImage(PATCH_ICON), entry.getKey(), null,
entry.getValue()));
}
}
return proposals;
}
/**
* Compute RPM package proposals, these proposals are usable only in the
* preambule section. Return an array of RPM macro proposals for the given
* viewer, region, specfile, prefix.
*
* @param viewer
* the viewer for which the context is created
* @param region
* the region into <code>document</code> for which the context is
* created
* @param prefix
* the prefix string
* @return a ICompletionProposal[]
*/
private List<ICompletionProposal> computeRpmPackageProposals(IRegion region, String prefix) {
List<String[]> rpmPkgProposalsList = Activator.getDefault().getRpmPackageList().getProposals(prefix);
ArrayList<ICompletionProposal> proposals = new ArrayList<>();
if (rpmPkgProposalsList != null) {
for (String[] item : rpmPkgProposalsList) {
proposals.add(new SpecCompletionProposal(item[0], region.getOffset(), region.getLength(),
item[0].length(), Activator.getDefault().getImage(PACKAGE_ICON), item[0], null, item[1]));
}
}
Collections.sort(proposals, (a, b) -> a.getDisplayString().compareToIgnoreCase(b.getDisplayString()));
return proposals;
}
private List<ICompletionProposal> computeRpmGroupProposals(IRegion region, String prefix) {
List<String> rpmGroupProposalsList = Activator.getDefault().getRpmGroups();
ArrayList<ICompletionProposal> proposals = new ArrayList<>();
for (String item : rpmGroupProposalsList) {
if (item.startsWith(prefix)) {
proposals.add(new SpecCompletionProposal(item, region.getOffset(), region.getLength(), item.length(),
Activator.getDefault().getImage(PACKAGE_ICON), item, null, item));
}
}
return proposals;
}
/**
* Create a template context for the givens Specfile, offset.
*
* @param specfile
* the sepcfile element
* @param offset
* the offset of the <code>documment</code>
* @return a TemplateContextType
*/
private TemplateContextType getContextType(Specfile specfile, int offset) {
List<SpecfileSection> elements = specfile.getSections();
if (elements.size() == 0 || offset < elements.get(0).getLineEndPosition()) {
return Activator.getDefault().getContextTypeRegistry().getContextType(PREAMBLE_SECTION_TEMPLATE);
} else if (elements.size() == 1 || offset < elements.get(1).getLineEndPosition()) {
return Activator.getDefault().getContextTypeRegistry().getContextType(PRE_SECTION_TEMPLATE);
} else if (elements.size() == 2 || offset < elements.get(2).getLineEndPosition()) {
return Activator.getDefault().getContextTypeRegistry().getContextType(BUILD_SECTION_TEMPLATE);
} else if (elements.size() == 3 || offset < elements.get(3).getLineEndPosition()) {
return Activator.getDefault().getContextTypeRegistry().getContextType(INSTALL_SECTION_TEMPLATE);
} else {
return Activator.getDefault().getContextTypeRegistry().getContextType(CHANGELOG_SECTION_TEMPLATE);
}
}
/**
* Create a template context for the given Specfile and offset.
*
* @param viewer
* the viewer for which the context is created
* @param region
* the region into <code>document</code> for which the context is
* created
* @param specfile
* the specfile element
* @return a TemplateContextType
*/
private TemplateContext createContext(ITextViewer viewer, IRegion region, Specfile specfile) {
TemplateContextType contextType = getContextType(specfile, region.getOffset());
if (contextType != null) {
IDocument document = viewer.getDocument();
return new DocumentTemplateContext(contextType, document, region.getOffset(), region.getLength());
}
return null;
}
/**
* Get relevance on templates for the given template and prefix.
*
* @param template
* the <code>Template</code> to get relevance
* @param prefix
* the prefix <code>String</code> to check.
* @return a relevant code (90 if <code>true</code> and 0 if not)
*/
private int getRelevance(Template template, String prefix) {
if (template.getName().startsWith(prefix)) {
return 90;
}
return 0;
}
/**
* Get the prefix for a given viewer, offset.
*
* @param viewer
* the viewer for which the context is created
* @param offset
* the offset into <code>document</code> for which the prefix is
* research
* @return the prefix
*/
private String getGroupPrefix(ITextViewer viewer, int offset) {
int i = offset;
IDocument document = viewer.getDocument();
if (i > document.getLength()) {
return EMPTY_STRING;
}
try {
while (i > 0) {
char ch = document.getChar(i - 1);
if (!Character.isLetterOrDigit(ch) && (ch != '/')) {
break;
}
i--;
}
return document.get(i, offset - i);
} catch (BadLocationException e) {
return EMPTY_STRING;
}
}
/**
* Get defines as a String key->value pair for a given specfile and prefix.
*
* @param specfile
* to get defines from.
* @param prefix
* used to find defines.
* @return a <code>HashMap</code> of defines.
*
*/
private Map<String, String> getDefines(Specfile specfile, String prefix) {
Collection<SpecfileDefine> defines = specfile.getDefines();
Map<String, String> ret = new HashMap<>();
String defineName;
for (SpecfileDefine define : defines) {
defineName = "%" + define.getName(); //$NON-NLS-1$
if (defineName.startsWith(prefix.replaceFirst("\\{", EMPTY_STRING))) {//$NON-NLS-1$
ret.put(defineName, define.getStringValue());
}
}
return ret;
}
/**
* Get patches as a String key->value pair for a given specfile and prefix.
*
* @param specfile
* to get defines from.
* @param prefix
* used to find defines.
* @return a <code>HashMap</code> of defines.
*
*/
private Map<String, String> getPatches(Specfile specfile, String prefix) {
Collection<SpecfileSource> patches = specfile.getPatches();
Map<String, String> ret = new HashMap<>();
String patchName;
for (SpecfileSource patch : patches) {
patchName = "%patch" + patch.getNumber(); //$NON-NLS-1$
if (patchName.startsWith(prefix)) {
ret.put(patchName.toLowerCase(), RPMUtils.getSourceOrPatchValue(specfile, "patch" //$NON-NLS-1$
+ patch.getNumber()));
}
}
return ret;
}
/**
* Get sources as a String key->value pair for a given specfile and prefix.
*
* @param specfile
* to get defines from.
* @param prefix
* used to find defines.
* @return a <code>HashMap</code> of defines.
*
*/
private Map<String, String> getSources(Specfile specfile, String prefix) {
Collection<SpecfileSource> sources = specfile.getSources();
Map<String, String> ret = new HashMap<>();
String sourceName;
for (SpecfileSource source : sources) {
sourceName = ISpecfileSpecialSymbols.MACRO_START_LONG + SOURCE + source.getNumber()
+ ISpecfileSpecialSymbols.MACRO_END_LONG;
if (sourceName.startsWith(prefix)) {
ret.put(sourceName, RPMUtils.getSourceOrPatchValue(specfile, SOURCE + source.getNumber()));
}
}
return ret;
}
private String completionWord(IDocument doc, int offset) {
String word = null;
if (offset > 0) {
try {
for (int n = offset - 1; n >= 0 && word == null; n--) {
char c = doc.getChar(n);
if (Character.isWhitespace(c)) {
word = doc.get(n + 1, offset - n - 1);
} else if (n == 0) {
// beginning of file
word = doc.get(0, offset - n);
}
}
} catch (BadLocationException e) {
// ignore
}
}
return word == null ? "" : word; //$NON-NLS-1$
}
@Override
public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {
return null;
}
@Override
public char[] getCompletionProposalAutoActivationCharacters() {
return null;
}
@Override
public char[] getContextInformationAutoActivationCharacters() {
return null;
}
@Override
public String getErrorMessage() {
return null;
}
@Override
public IContextInformationValidator getContextInformationValidator() {
return null;
}
}