blob: 8281dc1d6a80c21a4fa309a2b1f18828b761b303 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.lib.editor.codetemplates;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.PreferenceChangeEvent;
import java.util.prefs.PreferenceChangeListener;
import java.util.prefs.Preferences;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.swing.text.Position;
import javax.swing.undo.UndoableEdit;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.mimelookup.MimePath;
import org.netbeans.editor.Acceptor;
import org.netbeans.editor.AcceptorFactory;
import org.netbeans.lib.editor.codetemplates.api.CodeTemplate;
import org.netbeans.lib.editor.codetemplates.spi.CodeTemplateFilter;
import org.netbeans.lib.editor.util.swing.DocumentUtilities;
import org.netbeans.spi.editor.hints.ErrorDescription;
import org.netbeans.spi.editor.hints.ErrorDescriptionFactory;
import org.netbeans.spi.editor.hints.Fix;
import org.netbeans.spi.editor.hints.HintsController;
import org.netbeans.spi.editor.hints.Severity;
import org.openide.ErrorManager;
import org.openide.text.CloneableEditorSupport;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakListeners;
/**
* Abbreviation detection detects typing of an abbreviation
* in the document.
*
* @author Miloslav Metelka
* @version 1.00
*/
public final class AbbrevDetection implements DocumentListener, PropertyChangeListener, KeyListener, CaretListener, PreferenceChangeListener {
private static final Logger LOG = Logger.getLogger(AbbrevDetection.class.getName());
private static final RequestProcessor RP = new RequestProcessor(AbbrevDetection.class.getName());
/**
* Document property which determines whether an ongoing document modification
* should be completely ignored by the abbreviation framework.
* <br/>
* This is useful e.g. for code templates parameter replication.
*/
private static final String ABBREV_IGNORE_MODIFICATION_DOC_PROPERTY
= "abbrev-ignore-modification"; // NOI18N
private static final String EDITING_TEMPLATE_DOC_PROPERTY = "code-template-insert-handler"; //NOI18N
private static final String SURROUND_WITH = NbBundle.getMessage(SurroundWithFix.class, "TXT_SurroundWithHint_Label"); //NOI18N
private static final int SURROUND_WITH_DELAY = 250;
public static AbbrevDetection get(JTextComponent component) {
AbbrevDetection ad = (AbbrevDetection)component.getClientProperty(AbbrevDetection.class);
if (ad == null) {
ad = new AbbrevDetection(component);
component.putClientProperty(AbbrevDetection.class, ad);
}
return ad;
}
public static synchronized void remove(JTextComponent component) {
AbbrevDetection ad = (AbbrevDetection)component.getClientProperty(AbbrevDetection.class);
if (ad != null) {
assert ad.component == component : "Wrong component: AbbrevDetection.component=" + ad.component + ", component=" + component;
ad.uninstall();
component.putClientProperty(AbbrevDetection.class, null);
}
}
private JTextComponent component;
/** Document for which this abbreviation detection was constructed. */
private Document doc;
private DocumentListener weakDocL;
/**
* Offset after the last typed character of the collected abbreviation.
*/
private Position abbrevEndPosition;
/**
* Abbreviation characters captured from typing.
*/
private final StringBuffer abbrevChars = new StringBuffer();
// /** Chars on which to expand acceptor */
// private Acceptor expandAcceptor;
/** Which chars reset abbreviation accounting */
private Acceptor resetAcceptor;
private MimePath mimePath = null;
private Preferences prefs = null;
private PreferenceChangeListener weakPrefsListener = null;
private ErrorDescription errorDescription = null;
private List<Fix> surrounsWithFixes = null;
private Timer surroundsWithTimer;
private AbbrevDetection(JTextComponent component) {
this.component = component;
component.addCaretListener(this);
doc = component.getDocument();
if (doc != null) {
listenOnDoc();
}
String mimeType = DocumentUtilities.getMimeType(component);
if (mimeType != null) {
mimePath = MimePath.parse(mimeType);
prefs = MimeLookup.getLookup(mimePath).lookup(Preferences.class);
prefs.addPreferenceChangeListener(WeakListeners.create(PreferenceChangeListener.class, this, prefs));
}
// Load the settings
preferenceChange(null);
component.addKeyListener(this);
component.addPropertyChangeListener(this);
surroundsWithTimer = new Timer(0, new ActionListener() {
public void actionPerformed(ActionEvent e) {
// #124515, give up when the document is locked otherwise we are likely
// to cause a deadlock.
if (!DocumentUtilities.isReadLocked(doc)) {
showSurroundWithHint();
}
}
});
surroundsWithTimer.setRepeats(false);
}
private void listenOnDoc() {
weakDocL = WeakListeners.document(this, doc);
doc.addDocumentListener(weakDocL);
}
private void uninstall() {
assert component != null : "Can't call uninstall before the construction finished";
component.removeCaretListener(this);
if (doc != null) {
listenOnDoc();
}
component.removeKeyListener(this);
component.removePropertyChangeListener(this);
surroundsWithTimer.stop();
}
public void preferenceChange(PreferenceChangeEvent evt) {
String settingName = evt == null ? null : evt.getKey();
if (settingName == null || "abbrev-reset-acceptor".equals(settingName)) { //NOI18N
resetAcceptor = getResetAcceptor(prefs, mimePath);
}
}
public void insertUpdate(DocumentEvent evt) {
if (!isIgnoreModification()) {
if (DocumentUtilities.isTypingModification(evt.getDocument()) && !isAbbrevDisabled()) {
int offset = evt.getOffset();
int length = evt.getLength();
appendTypedText(offset, length);
} else { // not typing modification -> reset abbreviation collecting
resetAbbrevChars();
}
}
}
public void removeUpdate(DocumentEvent evt) {
if (!isIgnoreModification()) {
if (DocumentUtilities.isTypingModification(evt.getDocument()) && !isAbbrevDisabled()) {
int offset = evt.getOffset();
int length = evt.getLength();
removeAbbrevText(offset, length);
} else { // not typing modification -> reset abbreviation collecting
resetAbbrevChars();
}
}
}
public void changedUpdate(DocumentEvent evt) {
}
public void propertyChange(PropertyChangeEvent evt) {
if ("document".equals(evt.getPropertyName())) { //NOI18N
if (doc != null && weakDocL != null) {
doc.removeDocumentListener(weakDocL);
weakDocL = null;
}
doc = component.getDocument();
if (doc != null) {
listenOnDoc();
}
// unregister and destroy the old preferences (if we have any)
if (prefs != null) {
prefs.removePreferenceChangeListener(weakPrefsListener);
prefs = null;
weakPrefsListener = null;
mimePath = null;
}
// load and hook up to preferences for the new mime type
String mimeType = DocumentUtilities.getMimeType(component);
if (mimeType != null) {
mimePath = MimePath.parse(mimeType);
prefs = MimeLookup.getLookup(mimePath).lookup(Preferences.class);
weakPrefsListener = WeakListeners.create(PreferenceChangeListener.class, this, prefs);
prefs.addPreferenceChangeListener(weakPrefsListener);
}
// reload the settings
preferenceChange(null);
}
}
public void keyPressed(KeyEvent evt) {
checkExpansionKeystroke(evt);
}
public void keyReleased(KeyEvent evt) {
checkExpansionKeystroke(evt);
}
public void keyTyped(KeyEvent evt) {
checkExpansionKeystroke(evt);
}
public void caretUpdate(CaretEvent evt) {
if (evt.getDot() != evt.getMark()) {
surroundsWithTimer.setInitialDelay(SURROUND_WITH_DELAY);
surroundsWithTimer.restart();
} else {
surroundsWithTimer.stop();
hideSurroundWithHint();
}
}
private boolean isIgnoreModification() {
return Boolean.TRUE.equals(doc.getProperty(ABBREV_IGNORE_MODIFICATION_DOC_PROPERTY));
}
private boolean isAbbrevDisabled() {
return org.netbeans.editor.Abbrev.isAbbrevDisabled(component);
}
private void checkExpansionKeystroke(KeyEvent evt) {
Position pos = null;
Document d = null;
synchronized (abbrevChars) {
if (abbrevEndPosition != null && component != null && doc != null
&& component.getCaretPosition() == abbrevEndPosition.getOffset()
&& !isAbbrevDisabled()
&& doc.getProperty(EDITING_TEMPLATE_DOC_PROPERTY) == null
) {
pos = abbrevEndPosition;
d = component.getDocument();
}
}
if (pos != null && d != null) {
CodeTemplateManagerOperation operation = CodeTemplateManagerOperation.get(d, pos.getOffset());
if (operation != null) {
KeyStroke expandKeyStroke = operation.getExpansionKey();
if (expandKeyStroke.equals(KeyStroke.getKeyStrokeForEvent(evt))) {
if (expand(operation)) {
evt.consume();
}
}
}
}
}
/**
* Get current abbreviation string.
*/
private CharSequence getAbbrevText() {
return abbrevChars;
}
/**
* Reset abbreviation string collecting.
*/
private void resetAbbrevChars() {
synchronized(abbrevChars) {
abbrevChars.setLength(0);
abbrevEndPosition = null;
}
}
private void appendTypedText(int offset, int insertLength) {
if (abbrevEndPosition == null
|| offset + insertLength != abbrevEndPosition.getOffset()
) {
// Does not follow previous insert
resetAbbrevChars();
}
if (abbrevEndPosition == null) { // starting the new string
try {
// Start new accounting if previous char would reset abbrev
// i.e. check that not start typing 'u' after existing 'p' which would
// errorneously expand to 'public'
if (offset == 0
|| resetAcceptor.accept(DocumentUtilities.getText(doc, offset - 1, 1).charAt(0))
) {
abbrevEndPosition = doc.createPosition(offset + insertLength);
}
} catch (BadLocationException e) {
ErrorManager.getDefault().notify(e);
}
}
if (abbrevEndPosition != null) {
try {
String typedText = doc.getText(offset, insertLength); // typically just one char
boolean textAccepted = true;
for (int i = typedText.length() - 1; i >= 0; i--) {
if (resetAcceptor.accept(typedText.charAt(i))) {
// In theory there could be more than one character in the typed text
// and the resetting could occur on the very first char
// the next chars would not be accumulated as the insert
// is treated as a batch.
textAccepted = false;
break;
}
}
if (textAccepted) {
abbrevChars.append(typedText);
// abbrevEndPosition should move appropriately
} else {
resetAbbrevChars();
}
} catch (BadLocationException e) {
ErrorManager.getDefault().notify(e);
resetAbbrevChars();
}
}
}
private void removeAbbrevText(int offset, int removeLength) {
synchronized(abbrevChars) {
if (abbrevEndPosition != null) {
// Abbrev position should already move appropriately
if (offset == abbrevEndPosition.getOffset()
&& abbrevChars.length() >= removeLength
) { // removed at end
abbrevChars.setLength(abbrevChars.length() - removeLength);
} else {
resetAbbrevChars();
}
}
}
}
public boolean expand(CodeTemplateManagerOperation op) {
CharSequence abbrevText = getAbbrevText();
int abbrevEndOffset = abbrevEndPosition.getOffset();
if (expand(op, component, abbrevEndOffset - abbrevText.length(), abbrevText)) {
resetAbbrevChars();
return true;
} else {
return false;
}
}
private void showSurroundWithHint() {
try {
final Caret caret = component.getCaret();
if (caret != null) {
final Position pos = doc.createPosition(caret.getDot());
RP.post(new Runnable() {
public void run() {
final List<Fix> fixes = SurroundWithFix.getFixes(component);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
if (!fixes.isEmpty()) {
errorDescription = ErrorDescriptionFactory.createErrorDescription(
Severity.HINT, SURROUND_WITH, surrounsWithFixes = fixes, doc, pos, pos);
HintsController.setErrors(doc, SURROUND_WITH, Collections.singleton(errorDescription));
} else {
hideSurroundWithHint();
}
}
});
}
});
}
} catch (BadLocationException ble) {
Logger.getLogger("global").log(Level.WARNING, ble.getMessage(), ble);
}
}
private void hideSurroundWithHint() {
if (surrounsWithFixes != null)
surrounsWithFixes = null;
if (errorDescription != null) {
errorDescription = null;
HintsController.setErrors(doc, SURROUND_WITH, Collections.<ErrorDescription>emptySet());
}
}
public static Acceptor getResetAcceptor(Preferences prefs, MimePath mimePath) {
return prefs != null ? (Acceptor) callFactory(prefs, mimePath, "abbrev-reset-acceptor", AcceptorFactory.WHITESPACE) : AcceptorFactory.WHITESPACE; //NOI18N
}
// copied from org.netbeans.modules.editor.lib.SettingsConversions
private static Object callFactory(Preferences prefs, MimePath mimePath, String settingName, Object defaultValue) {
String factoryRef = prefs.get(settingName, null);
if (factoryRef != null) {
int lastDot = factoryRef.lastIndexOf('.'); //NOI18N
assert lastDot != -1 : "Need fully qualified name of class with the static setting factory method."; //NOI18N
String classFqn = factoryRef.substring(0, lastDot);
String methodName = factoryRef.substring(lastDot + 1);
ClassLoader loader = Lookup.getDefault().lookup(ClassLoader.class);
try {
Class factoryClass = loader.loadClass(classFqn);
Method factoryMethod;
try {
// normally the method should accept mime path and the a setting name
factoryMethod = factoryClass.getDeclaredMethod(methodName, MimePath.class, String.class);
} catch (NoSuchMethodException nsme) {
// but there might be methods that don't need those params
try {
factoryMethod = factoryClass.getDeclaredMethod(methodName);
} catch (NoSuchMethodException nsme2) {
// throw the first exception complaining about the full signature
throw nsme;
}
}
Object value;
if (factoryMethod.getParameterTypes().length == 2) {
value = factoryMethod.invoke(null, mimePath, settingName);
} else {
value = factoryMethod.invoke(null);
}
if (value != null) {
return value;
}
} catch (Exception e) {
LOG.log(Level.WARNING, null, e);
}
}
return defaultValue;
}
private static boolean expand(CodeTemplateManagerOperation op, JTextComponent component, int abbrevStartOffset, CharSequence abbrev) {
op.waitLoaded();
CodeTemplate ct = op.findByAbbreviation(abbrev.toString());
if (ct != null) {
if (accept(ct, CodeTemplateManagerOperation.getTemplateFilters(component, abbrevStartOffset))) {
Document doc = component.getDocument();
sendUndoableEdit(doc, CloneableEditorSupport.BEGIN_COMMIT_GROUP);
try {
// Remove the abbrev text
doc.remove(abbrevStartOffset, abbrev.length());
ct.insert(component);
} catch (BadLocationException ble) {
} finally {
sendUndoableEdit(doc, CloneableEditorSupport.END_COMMIT_GROUP);
}
return true;
}
}
return false;
}
private static boolean accept(CodeTemplate template, Collection<? extends CodeTemplateFilter> filters) {
for(CodeTemplateFilter filter : filters) {
if (!filter.accept(template)) {
return false;
}
}
return true;
}
private static void sendUndoableEdit(Document d, UndoableEdit ue) {
if(d instanceof AbstractDocument) {
UndoableEditListener[] uels = ((AbstractDocument)d).getUndoableEditListeners();
UndoableEditEvent ev = new UndoableEditEvent(d, ue);
for(UndoableEditListener uel : uels) {
uel.undoableEditHappened(ev);
}
}
}
}