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