/*
* $Id$
*
* Copyright (c) 2004-2010 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.spelling;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.text.MessageFormat;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import net.sourceforge.texlipse.TexlipsePlugin;
import net.sourceforge.texlipse.editor.TeXSpellingReconcileStrategy.TeXSpellingProblemCollector;
import net.sourceforge.texlipse.properties.TexlipseProperties;
import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext;
import org.eclipse.swt.graphics.Image;
import org.eclipse.ui.texteditor.spelling.ISpellingEngine;
import org.eclipse.ui.texteditor.spelling.ISpellingProblemCollector;
import org.eclipse.ui.texteditor.spelling.SpellingContext;
import org.eclipse.ui.texteditor.spelling.SpellingProblem;
import com.swabunga.spell.engine.Word;
import com.swabunga.spell.event.SpellCheckEvent;
import com.swabunga.spell.event.SpellCheckListener;
import com.swabunga.spell.event.SpellChecker;
import com.swabunga.spell.event.StringWordTokenizer;
/**
* The default spelling engine for LaTeX files. Uses Jazzy for spell
* checking.
* @author Boris von Loesch
*
*/
public class TexSpellingEngine implements ISpellingEngine, SpellCheckListener {
public static class TexSpellingProblem extends SpellingProblem {
private SpellCheckEvent fError;
private String fLang;
private int fOffset;
private final static Image fCorrectionImage = TexlipsePlugin.getImage("correction_change");
private final static String CHANGE_TO = TexlipsePlugin.getResourceString("spellCheckerChangeWord");
private final static String MESSAGE = TexlipsePlugin.getResourceString("spellMarkerMessage");
public TexSpellingProblem(SpellCheckEvent error, int roffset, String lang) {
this.fError = error;
this.fOffset = roffset + fError.getWordContextPosition();
fLang = lang;
}
@Override
public ICompletionProposal[] getProposals() {
return getProposals(null);
}
@SuppressWarnings("unchecked")
@Override
public ICompletionProposal[] getProposals(IQuickAssistInvocationContext context) {
List<Word> sugg = fError.getSuggestions();
int length = fError.getInvalidWord().length();
ICompletionProposal[] props = new ICompletionProposal[sugg.size() + 2];
for (int i=0; i < sugg.size(); i++) {
String suggestion = sugg.get(i).toString();
String s = MessageFormat.format(CHANGE_TO,
new Object[] { suggestion });
props[i] = new CompletionProposal(suggestion,
fOffset, length, suggestion.length(), fCorrectionImage, s, null, null);
}
props[props.length - 2] = new IgnoreProposal(ignore, fError.getInvalidWord(), context.getSourceViewer());
props[props.length - 1] = new AddToDictProposal(fError, fLang, context.getSourceViewer());
return props;
}
/**
* Updates the offset of the spelling error
* @param offset
*/
public void setOffset(int offset) {
fOffset = offset;
}
@Override
public int getOffset() {
return fOffset;
}
@Override
public String getMessage() {
return MessageFormat.format(MESSAGE, new Object[] { fError.getInvalidWord() });
}
@Override
public int getLength() {
return fError.getInvalidWord().length();
}
}
private final static String DEFAULT_DICT_PATH = "/dict/";
private final static String DEFAULT_LANG = "en";
private static SpellChecker spellCheck;
private static TexSpellDictionary dict;
private static String currentLang;
private static Set<String> ignore;
private List<SpellCheckEvent> errors;
/**
* Returns a SpellChecker that checks the language of the current project
* @param project
* @return null, if no dictionary for the current language was found
*/
private static SpellChecker getSpellChecker(String lang) {
//Return null, when no language is set
if (lang == null) return null;
if (lang.equals(currentLang)) return spellCheck;
//Get dictionary path from preferences and check if it exists
String dictPathSt = TexlipsePlugin.getPreference(TexlipseProperties.SPELLCHECKER_DICT_DIR);
if (dictPathSt == null || "".equals(dictPathSt.trim())) return null;
File dictPath = new File(dictPathSt);
if (!dictPath.exists() || !dictPath.isDirectory()) return null;
File f = new File(dictPath.getAbsolutePath() + File.separator + lang + ".dict");
if (!f.exists() || !f.canRead()) return null;
//Set spellCheck to null to allow the GC to trash the current dictionary
spellCheck = null;
dict = null;
currentLang = lang;
try {
FileInputStream input = new FileInputStream(f);
Reader r = new BufferedReader(new InputStreamReader(input, "UTF-8"));
dict = new TexSpellDictionary(r);
r.close();
String customDictPath = TexlipsePlugin.getPreference(TexlipseProperties.SPELLCHECKER_CUSTOM_DICT_DIR);
if (customDictPath != null && !"".equals(customDictPath.trim())) {
dict.setUserDict(new File (customDictPath + File.separator + lang + "_user.dict"));
}
spellCheck = new SpellChecker(dict);
return spellCheck;
} catch (IOException e) {
TexlipsePlugin.log("Error while loading dictionary", e);
}
return null;
}
/**
* <p>Returns the dictionary for that language, or the default dictionary if no
* exists.</p>
* <p><b>Beware:</b> Only use local references for the dictionary, otherwise
* it can not be trashed by the GC and we get memory problems.
* @param lang Language of the file
* @return The dictionary
*/
public static TexSpellDictionary getDict(String lang) {
getSpellChecker(lang);
return dict;
}
/**
* Returns the project that belongs to the given document.
* Thanks to Nitin Dahyabhai for the tip.
*
* @param document
* @return the project or <code>null</code> if no project was found
*/
private static IProject getProject(IDocument document) {
ITextFileBufferManager fileBufferMgr = FileBuffers.getTextFileBufferManager();
ITextFileBuffer fileBuffer = fileBufferMgr.getTextFileBuffer(document);
if (fileBuffer != null) {
IWorkspace workspace = ResourcesPlugin.getWorkspace();
IResource res = workspace.getRoot().findMember(fileBuffer.getLocation());
if (res != null) return res.getProject();
}
return null;
}
public void check(IDocument document, IRegion[] regions, SpellingContext context,
ISpellingProblemCollector collector, IProgressMonitor monitor) {
if (ignore == null) {
ignore = new HashSet<String>();
}
IProject project = getProject(document);
String lang = DEFAULT_LANG;
if (project != null) {
lang = TexlipseProperties.getProjectProperty(project, TexlipseProperties.LANGUAGE_PROPERTY);
}
//Get spellchecker for the correct language
SpellChecker spellCheck = getSpellChecker(lang);
if (spellCheck == null) return;
if (collector instanceof TeXSpellingProblemCollector) {
((TeXSpellingProblemCollector) collector).setRegions(regions);
}
try {
spellCheck.addSpellCheckListener(this);
for (final IRegion r : regions) {
errors = new LinkedList<SpellCheckEvent>();
int roffset = r.getOffset();
//Create a new wordfinder and initialize it
TexlipseWordFinder wf = new TexlipseWordFinder();
wf.setIgnoreComments(TexlipsePlugin.getDefault().getPreferenceStore().getBoolean(TexlipseProperties.SPELLCHECKER_IGNORE_COMMENTS));
wf.setIgnoreMath(TexlipsePlugin.getDefault().getPreferenceStore().getBoolean(TexlipseProperties.SPELLCHECKER_IGNORE_MATH));
spellCheck.checkSpelling(new StringWordTokenizer(
document.get(roffset, r.getLength()), wf));
for (SpellCheckEvent error : errors) {
SpellingProblem p = new TexSpellingProblem(error, roffset, lang);
collector.accept(p);
}
}
spellCheck.removeSpellCheckListener(this);
} catch (BadLocationException e) {
e.printStackTrace();
}
}
/**
* Checks if the input string contains upper case letters after the first letter
* @param word
* @return
*/
public static boolean isMixedCase(String word) {
for (int i = 1; i < word.length(); i++) {
if (Character.isUpperCase(word.charAt(i))) return true;
}
return false;
}
public void spellingError(SpellCheckEvent event) {
String invWord = event.getInvalidWord();
if (invWord.length() < 3) return;
if (invWord.indexOf('_') > -1) return;
if (invWord.indexOf('^') > -1) return;
if (ignore.contains(invWord)) return;
if (TexlipsePlugin.getDefault().getPreferenceStore().getBoolean(TexlipseProperties.SPELLCHECKER_IGNORE_MIXED_CASE)) {
if (isMixedCase(invWord)) return;
}
errors.add(event);
}
}