/* * $Id$ * * Copyright (c) 2004-2005 by the TeXlapse Team. * 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 */ package net.sourceforge.texlipse.editor; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sourceforge.texlipse.TexlipsePlugin; import net.sourceforge.texlipse.extension.BibProvider; import net.sourceforge.texlipse.model.ReferenceEntry; import net.sourceforge.texlipse.model.ReferenceManager; import net.sourceforge.texlipse.model.TexCommandEntry; import net.sourceforge.texlipse.model.TexDocumentModel; import net.sourceforge.texlipse.model.TexStyleCompletionManager; import net.sourceforge.texlipse.spelling.SpellChecker; import net.sourceforge.texlipse.templates.TexContextType; import net.sourceforge.texlipse.templates.TexTemplateCompletion; import net.sourceforge.texlipse.texparser.LatexParserUtils; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.Platform; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.contentassist.CompletionProposal; import org.eclipse.jface.text.contentassist.ContextInformation; import org.eclipse.jface.text.contentassist.ContextInformationValidator; 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.source.ISourceViewer; import org.eclipse.swt.graphics.Point; import org.eclipse.ui.texteditor.HippieProposalProcessor; /** * Implements a LaTeX-content assistant for displaying a list of completions for * \ref, \pageref and \cite. For BibTeX-completions (\cite), the details of the * selected entry are displayed. The actualy completions are fetched from the * ReferenceManager, owned by the TexDocumentModel of the current document. * * @author Oskar Ojala */ public class TexCompletionProcessor implements IContentAssistProcessor { private TexTemplateCompletion templatesCompletion = new TexTemplateCompletion( TexContextType.TEX_CONTEXT_TYPE); private TexDocumentModel model; private ReferenceManager refManager; private ISourceViewer fviewer; private TexStyleCompletionManager styleManager; public static final int assistLineLength = 60; private final HippieProposalProcessor hippie = new HippieProposalProcessor(); /** * A regexp pattern for resolving the command used for referencing (in the * 1st group) */ private static final Pattern comCapt = Pattern .compile("([a-zA-Z]+)\\s*(?:\\[.*?\\]\\s*)?"); /** * Receives the document model from the editor (one model/editor view) and * creates a new completion processor. * * @param tdm * The document model for this editor */ public TexCompletionProcessor(TexDocumentModel tdm, ISourceViewer viewer) { this.model = tdm; this.fviewer = viewer; } /* * (non-Javadoc) * * @seeorg.eclipse.jface.text.contentassist.IContentAssistProcessor# * computeCompletionProposals(org.eclipse.jface.text.ITextViewer, int) */ public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) { if (refManager == null) this.refManager = this.model.getRefMana(); model.removeStatusLineErrorMessage(); IDocument doc = viewer.getDocument(); // try to see if a text area is selected Point selectedRange = viewer.getSelectedRange(); if (selectedRange.y > 0) { try { String text = doc.get(selectedRange.x, selectedRange.y); return computeStyleProposals(text, selectedRange); } catch (BadLocationException e) { } } // if not, proceed to templates and "regular" completions try { int lineStartOffset = doc .getLineOffset(doc.getLineOfOffset(offset)); String lineStart = doc.get(lineStartOffset, offset - lineStartOffset); ICompletionProposal[] proposals = null; ICompletionProposal[] templateProposals = computeTemplateCompletions( offset, lineStart, viewer); if (!(lineStart.length() >= 2 && lineStart.endsWith("\\\\")) && (lineStart.length() > 0)) { int seqStartIdx = resolveCompletionStart(lineStart, lineStart .length() - 1); String seqStart = lineStart.substring(seqStartIdx); if (seqStart.startsWith("\\")) { String replacement = seqStart.substring(1); proposals = computeCommandCompletions(offset, replacement .length(), replacement); } else if (seqStart.startsWith("{")) { proposals = resolveReferenceCompletions(lineStart, offset, seqStart); if (proposals == null) { // Maybe there is a wrong spelled word here (e.g. // \section{Wroang ...}) proposals = SpellChecker.getSpellingProposal(offset, fviewer); if (proposals != null && proposals.length > 0) { return proposals; } else { // Hippie!! proposals = hippie.computeCompletionProposals( fviewer, offset); } } } else if (seqStart.length() > 0) { // ---------------------spell-checking-code-starts---------------------- // spell checking can't help with words not starting with a // letter... proposals = SpellChecker.getSpellingProposal(offset, fviewer); if (proposals != null && proposals.length > 0) { return proposals; } // if there is no spelling corrections, use the // HippieProposal proposals = hippie.computeCompletionProposals(fviewer, offset); } } // Concatenate the lists if necessary if (proposals != null) { ICompletionProposal[] value = new ICompletionProposal[proposals.length + templateProposals.length]; System.arraycopy(templateProposals, 0, value, 0, templateProposals.length); System.arraycopy(proposals, 0, value, templateProposals.length, proposals.length); return value; } else { if (templateProposals.length == 0) { model .setStatusLineErrorMessage(" No completions available."); } return templateProposals; } } catch (BadLocationException e) { TexlipsePlugin.log("TexCompletionProcessor: ", e); return new ICompletionProposal[0]; } } /* * (non-Javadoc) * * @seeorg.eclipse.jface.text.contentassist.IContentAssistProcessor# * computeContextInformation(org.eclipse.jface.text.ITextViewer, int) */ public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) { // FIXME -- for testing // Retrieve selected range Point selectedRange = viewer.getSelectedRange(); if (selectedRange.y > 0) { if (styleManager == null) { styleManager = TexStyleCompletionManager.getInstance(); } return styleManager.getStyleContext(); } return new ContextInformation[0]; } /* * (non-Javadoc) * * @seeorg.eclipse.jface.text.contentassist.IContentAssistProcessor# * getCompletionProposalAutoActivationCharacters() */ public char[] getCompletionProposalAutoActivationCharacters() { return new char[] { '{', '\\' }; } /* * (non-Javadoc) * * @seeorg.eclipse.jface.text.contentassist.IContentAssistProcessor# * getContextInformationAutoActivationCharacters() */ public char[] getContextInformationAutoActivationCharacters() { // return new char[] {'#'}; return null; } /* * (non-Javadoc) * * @see * org.eclipse.jface.text.contentassist.IContentAssistProcessor#getErrorMessage * () */ public String getErrorMessage() { return null; } /* * (non-Javadoc) * * @seeorg.eclipse.jface.text.contentassist.IContentAssistProcessor# * getContextInformationValidator() */ public IContextInformationValidator getContextInformationValidator() { return new ContextInformationValidator(this); } /** * Resolves the completion replacement string so that the activation * character is the first character following the returned offset. * * @param doc * The document * @param offset * The offset from where to search backwards * @return The offset where the activation char is found or -1 if it was not * found */ private int resolveCompletionStart(String doc, int offset) { while (offset > 0) { if (Character.isWhitespace(doc.charAt(offset)) || doc.charAt(offset) == '}' || doc.charAt(offset) == '{' || doc.charAt(offset) == '\\') break; offset--; } return offset; } /** * Resolves the command used for referencing (i.e. from '\foo{bar' it * resolves bar), figures out what kind of reference we want, fetches the * list of matching completions and returns it. * * @param line * The line containing the referencing command * @param offset * The offset of the cursor position in the document * @param lineEnd * The last part of the line, containing the partial match * @return The completion proposals */ private ICompletionProposal[] resolveReferenceCompletions(String line, int offset, String lineEnd) { int lastIndex = line.lastIndexOf('\\'); if (lastIndex == -1) { return null; } String fullCommand = line.substring(lastIndex + 1, line.length() - lineEnd.length()); Matcher m = comCapt.matcher(fullCommand); if (!m.matches()) { return null; } String command = m.group(1); String replacement = lineEnd.lastIndexOf(',') != -1 ? lineEnd .substring(lineEnd.lastIndexOf(',') + 1) : lineEnd.substring(1); ICompletionProposal[] proposals = null; if (command.indexOf("cite") > -1) { proposals = computeBibCompletions(offset, replacement.length(), replacement); } else if (command.indexOf("ref") > -1) { proposals = computeRefCompletions(offset, replacement.length(), replacement); } return proposals; } /** * Computes and returns BibTeX-proposals. * * @param offset * Current cursor offset * @param replacementLength * The length of the string to be replaced * @param prefix * The already typed prefix of the entry to assist with * @return An array of completion proposals to use directly or null */ private ICompletionProposal[] computeBibCompletions(int offset, int replacementLength, String prefix) { List<ICompletionProposal> resultAsList = new ArrayList<ICompletionProposal>(); List<ReferenceEntry> bibEntries = refManager.getCompletionsBib(prefix); //add the entries of the .bib file(s) to the results if (bibEntries != null) { for (int i = 0; i < bibEntries.size(); i++) { ReferenceEntry bib = bibEntries.get(i); String infoText = bib.info.length() > assistLineLength ? wrapString( bib.info, assistLineLength) : bib.info; resultAsList.add(new CompletionProposal(bib.key, offset - replacementLength, replacementLength, bib.key.length(), null, bib.key, null, infoText)); } } //the extension points IConfigurationElement[] configuration = Platform.getExtensionRegistry() .getConfigurationElementsFor( "net.sourceforge.texlipse.CiteAutocompleteExtension"); if (configuration.length > 0) { for (IConfigurationElement elem : configuration) { try { //fetches the BibProvider and updates the result BibProvider prov = (BibProvider) elem .createExecutableExtension("class"); resultAsList = prov.getCompletions(offset, replacementLength, prefix, refManager.getBibContainer()); } catch (CoreException e) { // e.printStackTrace(); //log } } } //if there are no entries, return null if (resultAsList == null || resultAsList.size() == 0) return null; ICompletionProposal[] result = new ICompletionProposal[resultAsList .size()]; return resultAsList.toArray(result); } /** * Computes and returns reference-proposals (labels). * * @param offset * Current cursor offset * @param replacementLength * The length of the string to be replaced * @param prefix * The already typed prefix of the entry to assist with * @return An array of completion proposals to use directly or null */ private ICompletionProposal[] computeRefCompletions(int offset, int replacementLength, String prefix) { List<ReferenceEntry> refEntries = refManager.getCompletionsRef(prefix); if (refEntries == null) return null; ICompletionProposal[] result = new ICompletionProposal[refEntries .size()]; for (int i = 0; i < refEntries.size(); i++) { String infoText = null; ReferenceEntry ref = refEntries.get(i); if (ref.info != null) { infoText = (ref.info.length() > assistLineLength) ? wrapString( ref.info, assistLineLength) : ref.info; } result[i] = new CompletionProposal(ref.key, offset - replacementLength, replacementLength, ref.key.length(), null, ref.key, null, infoText); } return result; } /** * Returns a replacement String for an ICompleteProposal if there is an open * environment * * @param doc * IDocument.get() * @param offset * current offset * @return null if no open environment was found, else end{+name+} */ static String environmentEnd(String doc, int offset) { int o = offset; while ((o = doc.lastIndexOf("\\begin", o)) >= 0) { IRegion r = LatexParserUtils.getCommand(doc, o + 1); if (r != null) { String command = doc.substring(r.getOffset(), r.getOffset() + r.getLength()); if ("\\begin".equals(command)) { IRegion r2 = LatexParserUtils.getCommandArgument(doc, o); if (r2 != null) { String envName = doc.substring(r2.getOffset(), r2 .getOffset() + r2.getLength()); if (TexAutoIndentStrategy.needsEnd(envName, doc, r .getOffset())) { return "end{" + envName + "}"; } } } } o--; } return null; } /** * Computes and returns command-proposals * * @param offset * Current cursor offset * @param replacementLength * The length of the string to be replaced * @param prefix * The already typed prefix of the entry to assist with * @return An array of completion proposals to use directly or null */ private ICompletionProposal[] computeCommandCompletions(int offset, int replacementLength, String prefix) { List<TexCommandEntry> comEntries = refManager.getCompletionsCom(prefix, TexCommandEntry.NORMAL_CONTEXT); if (comEntries == null) return null; CompletionProposal cp = null; if ("\\".equals(prefix) || "end".startsWith(prefix)) { String endString = environmentEnd(fviewer.getDocument().get(), offset); if (endString != null) { cp = new CompletionProposal(endString, offset - replacementLength, replacementLength, endString .length()); } } int start; ICompletionProposal[] result; if (cp == null) { result = new ICompletionProposal[comEntries.size()]; start = 0; } else { result = new ICompletionProposal[comEntries.size() + 1]; result[0] = cp; start = 1; } for (int i = 0; i < comEntries.size(); i++) { result[i + start] = new TexCompletionProposal(comEntries.get(i), offset - replacementLength, replacementLength, fviewer); } return result; } /** * Calculates and returns the template completions proposals. * * @param offset * Current cursor offset * @param lineStart * The already typed prefix of the entry to assist with * @param viewer * The text viewer of this document * @return An array of completion proposals to use directly */ private ICompletionProposal[] computeTemplateCompletions(int offset, String lineStart, ITextViewer viewer) { int t = lineStart.lastIndexOf(' '); if (t < lineStart.lastIndexOf('\t')) t = lineStart.lastIndexOf('\t'); String replacement = lineStart.substring(t + 1); List<ICompletionProposal> returnProposals = templatesCompletion .addTemplateProposals(viewer, offset, replacement); ICompletionProposal[] proposals = new ICompletionProposal[returnProposals .size()]; returnProposals.toArray(proposals); return proposals; } /** * Wraps the given string to the given column width. * * TODO this method will probably be mvoed to another class * * @param input * The string to wrap * @param width * The wrapping width * @return The wrapped string */ public static String wrapString(String input, int width) { StringBuffer sbout = new StringBuffer(); // \n should suffice since we prettify in parsing... String[] paragraphs = input.split("\r\n|\n|\r"); for (int i = 0; i < paragraphs.length; i++) { // skip if short if (paragraphs[i].length() < width) { sbout.append(paragraphs[i]); sbout.append("\n"); continue; } // imagine how much better this would be with functional // programming... String[] words = paragraphs[i].split("\\s"); int currLength = 0; for (int j = 0; j < words.length; j++) { if (words[j].length() + currLength <= width || currLength == 0) { if (currLength > 0) sbout.append(" "); sbout.append(words[j]); currLength += 1 + words[j].length(); } else { sbout.append("\n"); sbout.append(words[j]); currLength = words[j].length(); } } sbout.append("\n"); } return sbout.toString(); } // Some very quick style completions follow... // TODO improve this // private final static String[] STYLETAGS = new String[] { // "\\bf", "\\it", "\\rm", "\\sf", "\\sc", "\\em", "\\huge", "\\Huge" // }; // private final static String[] STYLELABELS = new String[] { // "bold", "italic", "roman", "sans serif", "small caps", "emphasize", // "huge", "Huge" // }; private ICompletionProposal[] computeStyleProposals(String selectedText, Point selectedRange) { if (styleManager == null) { styleManager = TexStyleCompletionManager.getInstance(); } return styleManager.getStyleCompletions(selectedText, selectedRange); /* * ICompletionProposal[] result = new * ICompletionProposal[STYLETAGS.length]; * * // Loop through all styles for (int i = 0; i < STYLETAGS.length; i++) * { String tag = STYLETAGS[i]; * * // Compute replacement text String replacement = "{" + tag + " " + * selectedText + "}"; * * // Derive cursor position int cursor = tag.length() + 2; * * // Compute a suitable context information IContextInformation * contextInfo = new ContextInformation(null, STYLELABELS[i]+" Style"); * * // Construct proposal result[i] = new CompletionProposal(replacement, * selectedRange.x, selectedRange.y, cursor, null, STYLELABELS[i], * contextInfo, replacement); } return result; */ } }