org.languagetool.gui.LanguageToolSupport.java Source code

Java tutorial

Introduction

Here is the source code for org.languagetool.gui.LanguageToolSupport.java

Source

/* LanguageTool, a natural language style checker
 * Copyright (C) 2005 Daniel Naber (http://www.danielnaber.de)
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
 * USA
 */
package org.languagetool.gui;

import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.Nullable;
import org.languagetool.JLanguageTool;
import org.languagetool.Language;
import org.languagetool.Languages;
import org.languagetool.MultiThreadedJLanguageTool;
import org.languagetool.UserConfig;
import org.languagetool.language.LanguageIdentifier;
import org.languagetool.rules.*;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Support for associating a LanguageTool instance and a JTextComponent
 *
 * @author Panagiotis Minos
 * @since 2.3
 */
class LanguageToolSupport {

    static final String CONFIG_FILE = ".languagetool.cfg";

    //maximum entries in the activate rule menu.
    //If entries' number is bigger, create per category submenus
    //can set to 0 to always create category submenus
    private static final int MAX_RULES_NO_CATEGORY_MENU = 12;
    //maximum rule menu entries, if more create a More submenu
    private static final int MAX_RULES_PER_MENU = 12;
    //maximum category menu entries, if more create a More submenu
    private static final int MAX_CATEGORIES_PER_MENU = 12;

    private final UndoRedoSupport undo;
    private final LanguageIdentifier langIdentifier;
    private final JFrame frame;
    private final JTextComponent textComponent;
    private final EventListenerList listenerList = new EventListenerList();
    private final ResourceBundle messages;
    private final List<RuleMatch> ruleMatches;
    private final List<Span> documentSpans;

    private MultiThreadedJLanguageTool languageTool;
    private ScheduledExecutorService checkExecutor;
    private MouseListener mouseListener;
    private ActionListener actionListener;
    private int millisecondDelay = 1500;
    private AtomicInteger check;
    private boolean popupMenuEnabled = true;
    private boolean backgroundCheckEnabled = true;
    private Configuration config;
    private boolean mustDetectLanguage = false;

    /**
     * LanguageTool support for a JTextComponent
     */
    LanguageToolSupport(JFrame frame, JTextComponent textComponent) {
        this(frame, textComponent, null);
    }

    /**
     * LanguageTool support for a JTextComponent
     * @since 2.7
     */
    LanguageToolSupport(JFrame frame, JTextComponent textComponent, UndoRedoSupport support) {
        this.frame = frame;
        this.textComponent = textComponent;
        this.messages = JLanguageTool.getMessageBundle();
        ruleMatches = new ArrayList<>();
        documentSpans = new ArrayList<>();
        this.undo = support;
        this.langIdentifier = new LanguageIdentifier();
        init();
    }

    void addLanguageToolListener(LanguageToolListener ltListener) {
        listenerList.add(LanguageToolListener.class, ltListener);
    }

    void removeLanguageToolListener(LanguageToolListener ltListener) {
        listenerList.remove(LanguageToolListener.class, ltListener);
    }

    private void fireEvent(LanguageToolEvent.Type type, Object caller, long elapsedTime) {
        LanguageToolEvent event = new LanguageToolEvent(this, type, caller, elapsedTime);
        fireEvent(event);
    }

    private void fireEvent(LanguageToolEvent.Type type, Object caller) {
        LanguageToolEvent event = new LanguageToolEvent(this, type, caller);
        fireEvent(event);
    }

    private void fireEvent(LanguageToolEvent event) {
        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();
        // Process the listeners last to first, notifying
        // those that are interested in this event
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == LanguageToolListener.class) {
                // Lazily create the event:
                ((LanguageToolListener) listeners[i + 1]).languageToolEventOccurred(event);
            }
        }
    }

    JTextComponent getTextComponent() {
        return textComponent;
    }

    List<RuleMatch> getMatches() {
        return this.ruleMatches;
    }

    void reloadConfig() {
        //FIXME
        //if mother tongue changes then create new JLanguageTool instance

        boolean update = false;

        Language language = languageTool.getLanguage();
        languageTool = new MultiThreadedJLanguageTool(language, config.getMotherTongue(),
                new UserConfig(config.getConfigurableValues()));
        config.initStyleCategories(languageTool.getAllRules());

        Set<String> disabledRules = config.getDisabledRuleIds();
        if (disabledRules == null) {
            disabledRules = Collections.emptySet();
        }

        Set<String> common = new HashSet<>(disabledRules);
        common.retainAll(languageTool.getDisabledRules());
        Set<String> toDisable = new HashSet<>(disabledRules);
        toDisable.removeAll(common);
        Set<String> toEnable = new HashSet<>(languageTool.getDisabledRules());
        toEnable.removeAll(common);

        for (String ruleId : toDisable) {
            languageTool.disableRule(ruleId);
            update = true;
        }
        for (String ruleId : toEnable) {
            languageTool.enableRule(ruleId);
            update = true;
        }

        Set<String> disabledCategoryNames = config.getDisabledCategoryNames();
        if (disabledCategoryNames == null) {
            disabledCategoryNames = Collections.emptySet();
        }
        Set<CategoryId> disabledCategories = new HashSet<>();
        Map<CategoryId, Category> langCategories = languageTool.getCategories();

        for (CategoryId id : langCategories.keySet()) {
            String categoryName = langCategories.get(id).getName();
            if (disabledCategoryNames.contains(categoryName)) {
                disabledCategories.add(id);
            }
        }

        Set<CategoryId> ltDisabledCategories = new HashSet<>();
        for (CategoryId id : langCategories.keySet()) {
            if (languageTool.isCategoryDisabled(id)) {
                ltDisabledCategories.add(id);
            }
        }

        Set<CategoryId> commonCat = new HashSet<>(disabledCategories);
        commonCat.retainAll(ltDisabledCategories);

        Set<CategoryId> toDisableCat = new HashSet<>(disabledCategories);
        toDisableCat.removeAll(commonCat);

        Set<CategoryId> toEnableCat = new HashSet<>(ltDisabledCategories);
        toEnableCat.removeAll(commonCat);

        for (CategoryId id : toDisableCat) {
            languageTool.disableCategory(id);
        }
        for (CategoryId id : toEnableCat) {
            languageTool.enableRuleCategory(id);
        }
        if (!toDisableCat.isEmpty() || !toEnableCat.isEmpty()) {
            // ugly hack to trigger reInitSpellCheckIgnoreWords()
            update = true;
        }

        Set<String> enabledRules = config.getEnabledRuleIds();
        if (enabledRules == null) {
            enabledRules = Collections.emptySet();
        }
        for (String ruleName : enabledRules) {
            languageTool.enableRule(ruleName);
            update = true;
        }

        //    languageTool.setConfigValues(config.getConfigValues());

        if (update) {
            //FIXME
            //we could skip a full check if the user disabled but didn't enable rules
            checkImmediately(null);
            fireEvent(LanguageToolEvent.Type.RULE_ENABLED, null);
        }
    }

    private void reloadLanguageTool(Language language) {
        try {
            //FIXME
            //no need to read again the file
            config = new Configuration(new File(System.getProperty("user.home")), CONFIG_FILE, language);
            //config still contains old language, update it
            this.config.setLanguage(language);
            // Calling shutdown here may cause a RejectedExecutionException:
            //if (languageTool != null) {
            //  languageTool.shutdownWhenDone();
            //}
            languageTool = new MultiThreadedJLanguageTool(language, config.getMotherTongue(),
                    new UserConfig(config.getConfigurableValues()));
            config.initStyleCategories(languageTool.getAllRules());
            languageTool.setCleanOverlappingMatches(false);
            Tools.configureFromRules(languageTool, config);
            activateLanguageModelRules(language);
            activateWord2VecModelRules(language);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void activateLanguageModelRules(Language language) {
        if (config.getNgramDirectory() != null) {
            File ngramLangDir = new File(config.getNgramDirectory(), language.getShortCode());
            if (ngramLangDir.exists()) {
                try {
                    languageTool.activateLanguageModelRules(config.getNgramDirectory());
                } catch (Exception e) {
                    JOptionPane.showMessageDialog(null, "Error while loading ngram database.\n" + e.getMessage());
                }
            } else {
                // user might have set ngram directory to use it for e.g. English, but they
                // might not have the data for other languages that supports ngram, so don't
                // annoy them with an error dialog:
                System.err.println("Not loading ngram data, directory does not exist: " + ngramLangDir);
            }
        }
    }

    private void activateWord2VecModelRules(Language language) {
        if (config.getWord2VecDirectory() != null) {
            File word2vecDir = new File(config.getWord2VecDirectory(), language.getShortCode());
            if (word2vecDir.exists()) {
                try {
                    languageTool.activateWord2VecModelRules(config.getWord2VecDirectory());
                } catch (Exception e) {
                    JOptionPane.showMessageDialog(null, "Error while loading word2vec model.\n" + e.getMessage());
                }
            } else {
                System.err.println("Not loading word2vec data, directory does not exist: " + word2vecDir);
            }
        }
    }

    private void init() {
        try {
            config = new Configuration(new File(System.getProperty("user.home")), CONFIG_FILE, null);
        } catch (IOException ex) {
            throw new RuntimeException("Could not load configuration", ex);
        }

        Language defaultLanguage = config.getLanguage();
        if (defaultLanguage == null) {
            defaultLanguage = Languages.getLanguageForLocale(Locale.getDefault());
        }

        /**
         * Warm-up: we have a lot of lazy init in LT, which causes the first check to
         * be very slow (several seconds) for languages with a lot of data and a lot of
         * rules. We just assume that the default language is the language that the user
         * often uses and init the LT object for that now, not just when it's first used.
         * This makes the first check feel much faster:
         */
        reloadLanguageTool(defaultLanguage);

        checkExecutor = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setDaemon(true);
                t.setPriority(Thread.MIN_PRIORITY);
                t.setName(t.getName() + "-lt-background");
                return t;
            }
        });

        check = new AtomicInteger(0);

        this.textComponent.getDocument().addDocumentListener(new DocumentListener() {
            @Override
            public void insertUpdate(DocumentEvent e) {
                mustDetectLanguage = config.getAutoDetect();
                recalculateSpans(e.getOffset(), e.getLength(), false);
                if (backgroundCheckEnabled) {
                    checkDelayed(null);
                }
            }

            @Override
            public void removeUpdate(DocumentEvent e) {
                mustDetectLanguage = config.getAutoDetect();
                recalculateSpans(e.getOffset(), e.getLength(), true);
                if (backgroundCheckEnabled) {
                    checkDelayed(null);
                }
            }

            @Override
            public void changedUpdate(DocumentEvent e) {
                mustDetectLanguage = config.getAutoDetect();
                if (backgroundCheckEnabled) {
                    checkDelayed(null);
                }
            }
        });

        mouseListener = new MouseListener() {
            @Override
            public void mouseClicked(MouseEvent me) {
            }

            @Override
            public void mousePressed(MouseEvent me) {
                if (me.isPopupTrigger()) {
                    showPopup(me);
                }
            }

            @Override
            public void mouseReleased(MouseEvent me) {
                if (me.isPopupTrigger()) {
                    showPopup(me);
                }
            }

            @Override
            public void mouseEntered(MouseEvent me) {
            }

            @Override
            public void mouseExited(MouseEvent me) {
            }
        };
        this.textComponent.addMouseListener(mouseListener);

        actionListener = e -> _actionPerformed(e);

        mustDetectLanguage = config.getAutoDetect();
        if (!this.textComponent.getText().isEmpty() && backgroundCheckEnabled) {
            checkImmediately(null);
        }
    }

    public int getMillisecondDelay() {
        return millisecondDelay;
    }

    /**
     * The text checking delay in milliseconds.
     */
    public void setMillisecondDelay(int millisecondDelay) {
        this.millisecondDelay = millisecondDelay;
    }

    public boolean isPopupMenuEnabled() {
        return popupMenuEnabled;
    }

    public void setPopupMenuEnabled(boolean popupMenuEnabled) {
        if (this.popupMenuEnabled == popupMenuEnabled) {
            return;
        }
        this.popupMenuEnabled = popupMenuEnabled;
        if (popupMenuEnabled) {
            textComponent.addMouseListener(mouseListener);
        } else {
            textComponent.removeMouseListener(mouseListener);
        }
    }

    public boolean isBackgroundCheckEnabled() {
        return backgroundCheckEnabled;
    }

    public void setBackgroundCheckEnabled(boolean backgroundCheckEnabled) {
        if (this.backgroundCheckEnabled == backgroundCheckEnabled) {
            return;
        }
        this.backgroundCheckEnabled = backgroundCheckEnabled;
        if (backgroundCheckEnabled) {
            checkImmediately(null);
        }
    }

    public void setLanguage(Language language) {
        reloadLanguageTool(language);
        if (backgroundCheckEnabled) {
            checkImmediately(null);
        }
    }

    Language getLanguage() {
        return this.languageTool.getLanguage();
    }

    public Configuration getConfig() {
        return config;
    }

    // called from Main.showOptions() and Main.tagTextAndDisplayResults()
    JLanguageTool getLanguageTool() {
        return languageTool;
    }

    void disableRule(String ruleId) {
        Rule rule = this.getRuleForId(ruleId);
        if (rule == null) {
            //System.err.println("No rule with id: <"+ruleId+">");
            return;
        }
        if (rule.isDefaultOff()) {
            config.getEnabledRuleIds().remove(ruleId);
        } else {
            config.getDisabledRuleIds().add(ruleId);
        }
        languageTool.disableRule(ruleId);
        updateHighlights(ruleId);
        fireEvent(LanguageToolEvent.Type.RULE_DISABLED, null);
    }

    void enableRule(String ruleId) {
        Rule rule = this.getRuleForId(ruleId);
        if (rule == null) {
            //System.err.println("No rule with id: <"+ruleId+">");
            return;
        }
        if (rule.isDefaultOff()) {
            config.getEnabledRuleIds().add(ruleId);
        } else {
            config.getDisabledRuleIds().remove(ruleId);
        }
        languageTool.enableRule(ruleId);
        fireEvent(LanguageToolEvent.Type.RULE_ENABLED, null);
        checkImmediately(null);
    }

    @Nullable
    private Span getSpan(int offset) {
        for (Span cur : documentSpans) {
            if (cur.end > cur.start && cur.start <= offset && offset < cur.end) {
                return cur;
            }
        }
        return null;
    }

    private void showPopup(MouseEvent event) {
        if (documentSpans.isEmpty() && languageTool.getDisabledRules().isEmpty()) {
            //No errors and no disabled Rules
            return;
        }

        int offset = this.textComponent.viewToModel(event.getPoint());
        Span span = getSpan(offset);
        JPopupMenu popup = new JPopupMenu("Grammar Menu");
        if (span != null) {
            JLabel msgItem = new JLabel("<html>"
                    + span.msg.replace("<suggestion>", "<b>").replace("</suggestion>", "</b>") + "</html>");
            msgItem.setToolTipText(span.desc.replace("<suggestion>", "").replace("</suggestion>", ""));
            msgItem.setBorder(new JMenuItem().getBorder());
            popup.add(msgItem);

            popup.add(new JSeparator());

            for (String r : span.replacement) {
                ReplaceMenuItem item = new ReplaceMenuItem(r, span);
                popup.add(item);
                item.addActionListener(actionListener);
            }

            popup.add(new JSeparator());

            JMenuItem moreItem = new JMenuItem(messages.getString("guiMore"));
            moreItem.addActionListener(e -> showDialog(textComponent, span.msg, span.desc, span.rule, span.url));
            popup.add(moreItem);

            JMenuItem ignoreItem = new JMenuItem(messages.getString("guiTurnOffRule"));
            ignoreItem.addActionListener(e -> disableRule(span.rule.getId()));
            popup.add(ignoreItem);
            popup.applyComponentOrientation(ComponentOrientation.getOrientation(Locale.getDefault()));
        }

        List<Rule> disabledRules = getDisabledRules();
        if (!disabledRules.isEmpty()) {
            JMenu activateRuleMenu = new JMenu(messages.getString("guiActivateRule"));
            addDisabledRulesToMenu(disabledRules, activateRuleMenu);
            popup.add(activateRuleMenu);
        }

        if (span != null) {
            textComponent.setCaretPosition(span.start);
            textComponent.moveCaretPosition(span.end);
        }

        popup.addPopupMenuListener(new PopupMenuListener() {
            @Override
            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
            }

            @Override
            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
            }

            @Override
            public void popupMenuCanceled(PopupMenuEvent e) {
                if (span != null) {
                    textComponent.setCaretPosition(span.start);
                }
            }
        });
        popup.show(textComponent, event.getPoint().x, event.getPoint().y);

    }

    private List<Rule> getDisabledRules() {
        List<Rule> disabledRules = new ArrayList<>();
        for (String ruleId : languageTool.getDisabledRules()) {
            Rule rule = getRuleForId(ruleId);
            if (rule == null || rule.isDefaultOff()) {
                continue;
            }
            disabledRules.add(rule);
        }
        Collections.sort(disabledRules, (r1, r2) -> r1.getDescription().compareTo(r2.getDescription()));
        return disabledRules;
    }

    private void addDisabledRulesToMenu(List<Rule> disabledRules, JMenu menu) {
        if (disabledRules.size() <= MAX_RULES_NO_CATEGORY_MENU) {
            createRulesMenu(menu, disabledRules);
            return;
        }

        TreeMap<String, ArrayList<Rule>> categories = new TreeMap<>();
        for (Rule rule : disabledRules) {
            if (!categories.containsKey(rule.getCategory().getName())) {
                categories.put(rule.getCategory().getName(), new ArrayList<>());
            }
            categories.get(rule.getCategory().getName()).add(rule);
        }

        JMenu parent = menu;
        int count = 0;
        for (String category : categories.keySet()) {
            count++;
            JMenu submenu = new JMenu(category);
            parent.add(submenu);
            createRulesMenu(submenu, categories.get(category));

            if (categories.keySet().size() <= MAX_CATEGORIES_PER_MENU) {
                continue;
            }

            //if menu contains MAX_CATEGORIES_PER_MENU-1, add a `more` menu
            //but only if the remain entries are more than one
            if ((count % (MAX_CATEGORIES_PER_MENU - 1) == 0) && (categories.keySet().size() - count > 1)) {
                JMenu more = new JMenu(messages.getString("guiActivateRuleMoreCategories"));
                parent.add(more);
                parent = more;
            }
        }
    }

    private void createRulesMenu(JMenu parent, List<Rule> rules) {
        JMenu menu = parent;
        int count = 0;

        for (Rule rule : rules) {
            count++;
            String id = rule.getId();
            JMenuItem ruleItem = new JMenuItem(rule.getDescription());
            ruleItem.addActionListener(e -> enableRule(id));
            menu.add(ruleItem);

            if (rules.size() <= MAX_RULES_PER_MENU) {
                continue;
            }

            //if menu contains MAX_RULES_PER_MENU-1, add a `more` menu
            //but only if the remain entries are more than one
            if ((count % (MAX_RULES_PER_MENU - 1) == 0) && (rules.size() - count > 1)) {
                JMenu more = new JMenu(messages.getString("guiActivateRuleMoreRules"));
                menu.add(more);
                menu = more;
            }
        }
    }

    @Nullable
    Rule getRuleForId(String ruleId) {
        List<Rule> allRules = languageTool.getAllRules();
        for (Rule rule : allRules) {
            if (rule.getId().equals(ruleId)) {
                return rule;
            }
        }
        return null;
    }

    private void _actionPerformed(ActionEvent e) {
        ReplaceMenuItem src = (ReplaceMenuItem) e.getSource();

        this.documentSpans.remove(src.span);
        applySuggestion(e.getActionCommand(), src.span.start, src.span.end);
    }

    private void applySuggestion(String str, int start, int end) {
        if (end < start) {
            throw new IllegalArgumentException("end before start: " + end + " < " + start);
        }
        Document doc = this.textComponent.getDocument();
        if (doc != null) {
            try {
                if (this.undo != null) {
                    this.undo.startCompoundEdit();
                }
                if (doc instanceof AbstractDocument) {
                    ((AbstractDocument) doc).replace(start, end - start, str, null);
                } else {
                    doc.remove(start, end - start);
                    doc.insertString(start, str, null);
                }
            } catch (BadLocationException e) {
                throw new IllegalArgumentException(e);
            } finally {
                if (this.undo != null) {
                    this.undo.endCompoundEdit();
                }
            }
        }
    }

    public void checkDelayed() {
        checkDelayed(null);
    }

    public void checkDelayed(Object caller) {
        check.getAndIncrement();
        checkExecutor.schedule(new RunnableImpl(caller), millisecondDelay, TimeUnit.MILLISECONDS);
    }

    public void checkImmediately() {
        checkImmediately(null);
    }

    public void checkImmediately(Object caller) {
        check.getAndIncrement();
        checkExecutor.schedule(new RunnableImpl(caller), 0, TimeUnit.MILLISECONDS);
    }

    Language autoDetectLanguage(String text) {
        Language lang = langIdentifier.detectLanguage(text);
        if (lang == null) {
            lang = Languages.getLanguageForLocale(Locale.getDefault());
        }
        if (lang.hasVariant()) {
            // UI only shows variants like "English (American)", not just "English", so use that:
            lang = lang.getDefaultLanguageVariant();
        }
        return lang;
    }

    private synchronized List<RuleMatch> checkText(Object caller) throws IOException {
        if (this.mustDetectLanguage) {
            mustDetectLanguage = false;
            if (!this.textComponent.getText().isEmpty()) {
                Language detectedLanguage = autoDetectLanguage(this.textComponent.getText());
                if (!detectedLanguage.equals(this.languageTool.getLanguage())) {
                    reloadLanguageTool(detectedLanguage);
                    if (SwingUtilities.isEventDispatchThread()) {
                        fireEvent(LanguageToolEvent.Type.LANGUAGE_CHANGED, caller);
                    } else {
                        try {
                            SwingUtilities.invokeAndWait(
                                    () -> fireEvent(LanguageToolEvent.Type.LANGUAGE_CHANGED, caller));
                        } catch (InterruptedException ex) {
                            //ignore
                        } catch (InvocationTargetException ex) {
                            throw new RuntimeException(ex);
                        }
                    }
                }
            }
        }
        if (SwingUtilities.isEventDispatchThread()) {
            fireEvent(LanguageToolEvent.Type.CHECKING_STARTED, caller);
        } else {
            try {
                SwingUtilities.invokeAndWait(() -> fireEvent(LanguageToolEvent.Type.CHECKING_STARTED, caller));
            } catch (InterruptedException ex) {
                //ignore
            } catch (InvocationTargetException ex) {
                throw new RuntimeException(ex);
            }
        }

        long startTime = System.currentTimeMillis();
        List<RuleMatch> matches = this.languageTool.check(this.textComponent.getText());
        long elapsedTime = System.currentTimeMillis() - startTime;

        int v = check.get();
        if (v == 0) {
            if (!SwingUtilities.isEventDispatchThread()) {
                SwingUtilities.invokeLater(() -> {
                    updateHighlights(matches);
                    fireEvent(LanguageToolEvent.Type.CHECKING_FINISHED, caller, elapsedTime);
                });
            } else {
                updateHighlights(matches);
                fireEvent(LanguageToolEvent.Type.CHECKING_FINISHED, caller, elapsedTime);
            }
        }
        return matches;
    }

    private void removeHighlights() {
        for (Highlighter.Highlight hl : textComponent.getHighlighter().getHighlights()) {
            if (hl.getPainter() instanceof HighlightPainter) {
                textComponent.getHighlighter().removeHighlight(hl);
            }
        }
    }

    private void recalculateSpans(int offset, int length, boolean remove) {
        if (length == 0) {
            return;
        }
        for (Span span : this.documentSpans) {
            if (offset >= span.end) {
                continue;
            }
            if (!remove) {
                if (offset <= span.start) {
                    span.start += length;
                }
                span.end += length;
            } else {
                if (offset + length <= span.end) {
                    if (offset > span.start) {
                        //
                    } else if (offset + length <= span.start) {
                        span.start -= length;
                    } else {
                        span.start = offset;
                    }
                    span.end -= length;
                } else {
                    span.end -= Math.min(length, span.end - offset);
                }
            }
        }
        updateHighlights();
    }

    private void updateHighlights(String disabledRule) {
        List<Span> spans = new ArrayList<>();
        List<RuleMatch> matches = new ArrayList<>();
        for (RuleMatch match : ruleMatches) {
            if (match.getRule().getId().equals(disabledRule)) {
                continue;
            }
            matches.add(match);
            spans.add(new Span(match));
        }
        prepareUpdateHighlights(matches, spans);
    }

    private void updateHighlights(List<RuleMatch> matches) {
        List<Span> spans = new ArrayList<>();
        for (RuleMatch match : matches) {
            spans.add(new Span(match));
        }
        prepareUpdateHighlights(matches, spans);
    }

    private void prepareUpdateHighlights(List<RuleMatch> matches, List<Span> spans) {
        ruleMatches.clear();
        documentSpans.clear();
        ruleMatches.addAll(matches);
        documentSpans.addAll(spans);
        updateHighlights();
    }

    private void updateHighlights() {
        removeHighlights();

        Highlighter h = textComponent.getHighlighter();

        for (Span span : documentSpans) {
            if (span.start == span.end) {
                continue;
            }
            try {
                if (span.start < span.end) { //to avoid the BadLocationException
                    ITSIssueType issueType = span.rule.getLocQualityIssueType();
                    Color ulColor = config.getUnderlineColor(span.rule.getCategory().getName());
                    Color colorForIssueType = getConfig().getErrorColors().get(issueType);
                    Color bgColor = colorForIssueType != null ? colorForIssueType : null;
                    Color underlineColor = ITSIssueType.Misspelling == span.rule.getLocQualityIssueType()
                            ? Color.red
                            : ulColor;
                    HighlightPainter painter = new HighlightPainter(bgColor, underlineColor);
                    h.addHighlight(span.start, span.end, painter);
                }
            } catch (BadLocationException ex) {
                ex.printStackTrace();
            }
        }
    }

    private void showDialog(Component parent, String title, String message, Rule rule, URL url) {
        Tools.showRuleInfoDialog(parent, title, message, rule, url, messages,
                languageTool.getLanguage().getShortCodeWithCountryAndVariant());
    }

    private static class ReplaceMenuItem extends JMenuItem {

        private final Span span;

        private ReplaceMenuItem(String name, Span span) {
            super(name);
            this.span = span;
        }
    }

    private static class Span {

        private static final int MAX_SUGGESTIONS = 5;

        private int start;
        private int end;
        private final String msg;
        private final String desc;
        private final List<String> replacement;
        private final Rule rule;
        private final URL url;

        private Span(RuleMatch match) {
            start = match.getFromPos();
            end = match.getToPos();
            String tmp = match.getShortMessage();
            if (StringUtils.isEmpty(tmp)) {
                tmp = match.getMessage();
            }
            msg = Tools.shortenComment(tmp);
            desc = match.getMessage();
            replacement = new ArrayList<>();
            List<String> repl = match.getSuggestedReplacements();
            replacement.addAll(repl.subList(0, Math.min(MAX_SUGGESTIONS, repl.size())));
            rule = match.getRule();
            url = match.getUrl() != null ? match.getUrl() : rule.getUrl();
        }
    }

    private class RunnableImpl implements Runnable {

        private final Object caller;

        private RunnableImpl(Object caller) {
            this.caller = caller;
        }

        @Override
        public void run() {
            int v = check.decrementAndGet();
            if (v != 0) {
                return;
            }
            try {
                checkText(caller);
            } catch (Exception ex) {
                Tools.showError(ex);
            }
        }
    }
}