/* * 02/21/2005 * * CodeTemplateManager.java - manages code templates. * Copyright (C) 2005 Robert Futrell * robert_futrell at users.sourceforge.net * http://fifesoft.com/rsyntaxtextarea * * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. */ package org.fife.ui.rsyntaxtextarea; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.beans.XMLDecoder; import java.beans.XMLEncoder; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.Serializable; import java.util.*; import javax.swing.KeyStroke; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.Segment; import org.fife.ui.rsyntaxtextarea.templates.CodeTemplate; /** * Manages "code templates." * <p> * * All methods in this class are synchronized for thread safety, but as a best practice, you should probably only modify * the templates known to a <code>CodeTemplateManager</code> on the EDT. Modifying a <code>CodeTemplate</code> retrieved * from a <code>CodeTemplateManager</code> while <em>not</em> on the EDT could cause problems. * * @author Robert Futrell * @version 0.1 */ public class CodeTemplateManager { private int maxTemplateIDLength; private List templates; private KeyStroke insertTrigger; private String insertTriggerString; private Segment s; private TemplateComparator comparator; private File directory; private static final int mask = InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK; static final KeyStroke TEMPLATE_KEYSTROKE = KeyStroke. getKeyStroke(KeyEvent.VK_SPACE, mask); /** * Constructor. */ public CodeTemplateManager() { // Default insert trigger is a space. // FIXME: See notes in RSyntaxTextAreaDefaultInputMap. setInsertTrigger(TEMPLATE_KEYSTROKE); s = new Segment(); comparator = new TemplateComparator(); templates = new ArrayList(); } /** * Registers the specified template with this template manager. * * @param template * The template to register. * @throws IllegalArgumentException * If <code>template</code> is <code>null</code>. * @see #removeTemplate(CodeTemplate) * @see #removeTemplate(String) */ public synchronized void addTemplate(CodeTemplate template) { if (template == null) { throw new IllegalArgumentException("template cannot be null"); } templates.add(template); sortTemplates(); } /** * Returns the keystroke that is the "insert trigger" for templates; that is, the character that, when inserted into * an instance of <code>RSyntaxTextArea</code>, triggers the search for a template matching the token ending at the * caret position. * * @return The insert trigger. * @see #getInsertTriggerString() * @see #setInsertTrigger(KeyStroke) */ /* * FIXME: This text IS what's inserted if the trigger character is pressed in a text area but no template matches, * but it is NOT the trigger character used in the text areas. This is because space (" ") is hard-coded into * RSyntaxTextAreaDefaultInputMap.java. We need to make this dynamic somehow. See * RSyntaxTextAreaDefaultInputMap.java. */ public KeyStroke getInsertTrigger() { return insertTrigger; } /** * Returns the "insert trigger" for templates; that is, the character that, when inserted into an instance of * <code>RSyntaxTextArea</code>, triggers the search for a template matching the token ending at the caret position. * * @return The insert trigger character. * @see #getInsertTrigger() * @see #setInsertTrigger(KeyStroke) */ /* * FIXME: This text IS what's inserted if the trigger character is pressed in a text area but no template matches, * but it is NOT the trigger character used in the text areas. This is because space (" ") is hard-coded into * RSyntaxTextAreaDefaultInputMap.java. We need to make this dynamic somehow. See * RSyntaxTextAreaDefaultInputMap.java. */ public String getInsertTriggerString() { return insertTriggerString; } /** * Returns the template that should be inserted at the current caret position, assuming the trigger character was * pressed. * * @param textArea * The text area that's getting text inserted into it. * @return A template that should be inserted, if appropriate, or <code>null</code> if no template should be * inserted. */ public synchronized CodeTemplate getTemplate(RSyntaxTextArea textArea) { int caretPos = textArea.getCaretPosition(); int charsToGet = Math.min(caretPos, maxTemplateIDLength); try { Document doc = textArea.getDocument(); doc.getText(caretPos - charsToGet, charsToGet, s); int index = Collections.binarySearch(templates, s, comparator); return index >= 0 ? (CodeTemplate) templates.get(index) : null; } catch (BadLocationException ble) { ble.printStackTrace(); throw new InternalError("Error in CodeTemplateManager"); } } /** * Returns the number of templates this manager knows about. * * @return The template count. */ public synchronized int getTemplateCount() { return templates.size(); } /** * Returns the templates currently available. * * @return The templates available. */ public synchronized CodeTemplate[] getTemplates() { CodeTemplate[] temp = new CodeTemplate[templates.size()]; return (CodeTemplate[]) templates.toArray(temp); } /** * Returns whether the specified character is a valid character for a <code>CodeTemplate</code> id. * * @param ch * The character to check. * @return Whether the character is a valid template character. */ public static final boolean isValidChar(char ch) { return RSyntaxUtilities.isLetterOrDigit(ch) || ch == '_'; } /** * Returns the specified code template. * * @param template * The template to remove. * @return <code>true</code> if the template was removed, <code>false</code> if the template was not in this * template manager. * @throws IllegalArgumentException * If <code>template</code> is <code>null</code>. * @see #removeTemplate(String) * @see #addTemplate(CodeTemplate) */ public synchronized boolean removeTemplate(CodeTemplate template) { if (template == null) { throw new IllegalArgumentException("template cannot be null"); } // TODO: Do a binary search return templates.remove(template); } /** * Returns the code template with the specified id. * * @param id * The id to check for. * @return The code template that was removed, or <code>null</code> if there was no template with the specified ID. * @throws IllegalArgumentException * If <code>id</code> is <code>null</code>. * @see #removeTemplate(CodeTemplate) * @see #addTemplate(CodeTemplate) */ public synchronized CodeTemplate removeTemplate(String id) { if (id == null) { throw new IllegalArgumentException("id cannot be null"); } // TODO: Do a binary search for (Iterator i = templates.iterator(); i.hasNext();) { CodeTemplate template = (CodeTemplate) i.next(); if (id.equals(template.getID())) { i.remove(); return template; } } return null; } /** * Replaces the current set of available templates with the ones specified. * * @param newTemplates * The new set of templates. Note that we will be taking a shallow copy of these and sorting them. */ public synchronized void replaceTemplates(CodeTemplate[] newTemplates) { templates.clear(); if (newTemplates != null) { for (int i = 0; i < newTemplates.length; i++) { templates.add(newTemplates[i]); } } sortTemplates(); // Also recomputes maxTemplateIDLength. } /** * Saves all templates as XML files in the current template directory. * * @return Whether or not the save was successful. */ public synchronized boolean saveTemplates() { if (templates == null) return true; if (directory == null || !directory.isDirectory()) return false; // Blow away all old XML files to start anew, as some might be from // templates we're removed from the template manager. File[] oldXMLFiles = directory.listFiles(new XMLFileFilter()); if (oldXMLFiles == null) return false; // Either an IOException or it isn't a directory. int count = oldXMLFiles.length; for (int i = 0; i < count; i++) { /* boolean deleted = */oldXMLFiles[i].delete(); } // Save all current templates as XML. boolean wasSuccessful = true; for (Iterator i = templates.iterator(); i.hasNext();) { CodeTemplate template = (CodeTemplate) i.next(); File xmlFile = new File(directory, template.getID() + ".xml"); try { XMLEncoder e = new XMLEncoder(new BufferedOutputStream( new FileOutputStream(xmlFile))); e.writeObject(template); e.close(); } catch (IOException ioe) { ioe.printStackTrace(); wasSuccessful = false; } } return wasSuccessful; } /** * Sets the "trigger" character for templates. * * @param trigger * The trigger character to set for templates. This means that when this character is pressed in an * <code>RSyntaxTextArea</code>, the last-typed token is found, and is checked against all template ID's * to see if a template should be inserted. If a template ID matches, that template is inserted; if not, * the trigger character is inserted. If this parameter is <code>null</code>, no change is made to the * trigger character. * @see #getInsertTrigger() * @see #getInsertTriggerString() */ /* * FIXME: The trigger set here IS inserted when no matching template is found, but a space character (" ") is always * used as the "trigger" to look for templates. This is because it is hard-coded in RSyntaxTextArea's input map this * way. We need to change this. See RSyntaxTextAreaDefaultInputMap.java. */ public void setInsertTrigger(KeyStroke trigger) { if (trigger != null) { insertTrigger = trigger; insertTriggerString = Character.toString(trigger.getKeyChar()); } } /** * Sets the directory in which to look for templates. Calling this method adds any new templates found in the * specified directory to the templates already registered. * * @param dir * The new directory in which to look for templates. * @return The new number of templates in this template manager, or <code>-1</code> if the specified directory does * not exist. */ public synchronized int setTemplateDirectory(File dir) { if (dir != null && dir.isDirectory()) { this.directory = dir; File[] files = dir.listFiles(new XMLFileFilter()); int newCount = files == null ? 0 : files.length; int oldCount = templates.size(); List temp = new ArrayList(oldCount + newCount); temp.addAll(templates); for (int i = 0; i < newCount; i++) { try { XMLDecoder d = new XMLDecoder(new BufferedInputStream( new FileInputStream(files[i]))); Object obj = d.readObject(); if (!(obj instanceof CodeTemplate)) { throw new IOException("Not a CodeTemplate: " + files[i].getAbsolutePath()); } temp.add(obj); d.close(); } catch (/* IO, NoSuchElement */Exception e) { // NoSuchElementException can be thrown when reading // an XML file not in the format expected by XMLDecoder. // (e.g. CodeTemplates in an old format). e.printStackTrace(); } } templates = temp; sortTemplates(); return getTemplateCount(); } return -1; } /** * Removes any null entries in the current set of templates (if any), sorts the remaining templates, and computes * the new maximum template ID length. */ private synchronized void sortTemplates() { // Get the maximum length of a template ID. maxTemplateIDLength = 0; // Remove any null entries (should only happen because of // IOExceptions, etc. when loading from files), and sort // the remaining list. for (Iterator i = templates.iterator(); i.hasNext();) { CodeTemplate temp = (CodeTemplate) i.next(); if (temp == null || temp.getID() == null) { i.remove(); } else { maxTemplateIDLength = Math.max(maxTemplateIDLength, temp.getID().length()); } } Collections.sort(templates); } /** * A comparator that takes a <code>CodeTemplate</code> as its first parameter and a <code>Segment</code> as its * second, and knows to compare the template's ID to the segment's text. */ private static class TemplateComparator implements Comparator, Serializable { public int compare(Object template, Object segment) { // Get template start index (0) and length. CodeTemplate t = (CodeTemplate) template; final char[] templateArray = t.getID().toCharArray(); int i = 0; int len1 = templateArray.length; // Find "token" part of segment and get its offset and length. Segment s = (Segment) segment; char[] segArray = s.array; int len2 = s.count; int j = s.offset + len2 - 1; while (j >= s.offset && isValidChar(segArray[j])) { j--; } j++; int segShift = j - s.offset; len2 -= segShift; int n = Math.min(len1, len2); while (n-- != 0) { char c1 = templateArray[i++]; char c2 = segArray[j++]; if (c1 != c2) return c1 - c2; } return len1 - len2; } } /** * A file filter for File.listFiles() (NOT for JFileChoosers!) that accepts only XML files. */ private static class XMLFileFilter implements FileFilter { public boolean accept(File f) { return f.getName().toLowerCase().endsWith(".xml"); } } }