/******************************************************************************* * 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; } }