source: other-projects/rsyntax-textarea/src/java/org/fife/ui/rsyntaxtextarea/CodeTemplateManager.java@ 25584

Last change on this file since 25584 was 25584, checked in by davidb, 12 years ago

Initial cut an a text edit area for GLI that supports color syntax highlighting

File size: 13.8 KB
Line 
1/*
2 * 02/21/2005
3 *
4 * CodeTemplateManager.java - manages code templates.
5 *
6 * This library is distributed under a modified BSD license. See the included
7 * RSyntaxTextArea.License.txt file for details.
8 */
9package org.fife.ui.rsyntaxtextarea;
10
11import java.awt.event.InputEvent;
12import java.awt.event.KeyEvent;
13import java.beans.XMLDecoder;
14import java.beans.XMLEncoder;
15import java.io.BufferedInputStream;
16import java.io.BufferedOutputStream;
17import java.io.File;
18import java.io.FileFilter;
19import java.io.FileInputStream;
20import java.io.FileOutputStream;
21import java.io.IOException;
22import java.io.Serializable;
23import java.util.*;
24import javax.swing.KeyStroke;
25import javax.swing.text.BadLocationException;
26import javax.swing.text.Document;
27import javax.swing.text.Segment;
28
29import org.fife.ui.rsyntaxtextarea.templates.CodeTemplate;
30
31
32/**
33 * Manages "code templates."<p>
34 *
35 * All methods in this class are synchronized for thread safety, but as a
36 * best practice, you should probably only modify the templates known to a
37 * <code>CodeTemplateManager</code> on the EDT. Modifying a
38 * <code>CodeTemplate</code> retrieved from a <code>CodeTemplateManager</code>
39 * while <em>not</em> on the EDT could cause problems.
40 *
41 * @author Robert Futrell
42 * @version 0.1
43 */
44public class CodeTemplateManager {
45
46 private int maxTemplateIDLength;
47 private List templates;
48
49 private KeyStroke insertTrigger;
50 private String insertTriggerString;
51 private Segment s;
52 private TemplateComparator comparator;
53 private File directory;
54
55 private static final int mask = InputEvent.CTRL_MASK|InputEvent.SHIFT_MASK;
56 static final KeyStroke TEMPLATE_KEYSTROKE = KeyStroke.
57 getKeyStroke(KeyEvent.VK_SPACE, mask);
58
59
60 /**
61 * Constructor.
62 */
63 public CodeTemplateManager() {
64
65 // Default insert trigger is a space.
66 // FIXME: See notes in RSyntaxTextAreaDefaultInputMap.
67 setInsertTrigger(TEMPLATE_KEYSTROKE);
68
69 s = new Segment();
70 comparator = new TemplateComparator();
71 templates = new ArrayList();
72
73 }
74
75
76 /**
77 * Registers the specified template with this template manager.
78 *
79 * @param template The template to register.
80 * @throws IllegalArgumentException If <code>template</code> is
81 * <code>null</code>.
82 * @see #removeTemplate(CodeTemplate)
83 * @see #removeTemplate(String)
84 */
85 public synchronized void addTemplate(CodeTemplate template) {
86 if (template==null) {
87 throw new IllegalArgumentException("template cannot be null");
88 }
89 templates.add(template);
90 sortTemplates();
91 }
92
93
94 /**
95 * Returns the keystroke that is the "insert trigger" for templates;
96 * that is, the character that, when inserted into an instance of
97 * <code>RSyntaxTextArea</code>, triggers the search for
98 * a template matching the token ending at the caret position.
99 *
100 * @return The insert trigger.
101 * @see #getInsertTriggerString()
102 * @see #setInsertTrigger(KeyStroke)
103 */
104 /*
105 * FIXME: This text IS what's inserted if the trigger character is pressed
106 * in a text area but no template matches, but it is NOT the trigger
107 * character used in the text areas. This is because space (" ") is
108 * hard-coded into RSyntaxTextAreaDefaultInputMap.java. We need to make
109 * this dynamic somehow. See RSyntaxTextAreaDefaultInputMap.java.
110 */
111 public KeyStroke getInsertTrigger() {
112 return insertTrigger;
113 }
114
115
116 /**
117 * Returns the "insert trigger" for templates; that is, the character
118 * that, when inserted into an instance of <code>RSyntaxTextArea</code>,
119 * triggers the search for a template matching the token ending at the
120 * caret position.
121 *
122 * @return The insert trigger character.
123 * @see #getInsertTrigger()
124 * @see #setInsertTrigger(KeyStroke)
125 */
126 /*
127 * FIXME: This text IS what's inserted if the trigger character is pressed
128 * in a text area but no template matches, but it is NOT the trigger
129 * character used in the text areas. This is because space (" ") is
130 * hard-coded into RSyntaxTextAreaDefaultInputMap.java. We need to make
131 * this dynamic somehow. See RSyntaxTextAreaDefaultInputMap.java.
132 */
133 public String getInsertTriggerString() {
134 return insertTriggerString;
135 }
136
137
138 /**
139 * Returns the template that should be inserted at the current caret
140 * position, assuming the trigger character was pressed.
141 *
142 * @param textArea The text area that's getting text inserted into it.
143 * @return A template that should be inserted, if appropriate, or
144 * <code>null</code> if no template should be inserted.
145 */
146 public synchronized CodeTemplate getTemplate(RSyntaxTextArea textArea) {
147 int caretPos = textArea.getCaretPosition();
148 int charsToGet = Math.min(caretPos, maxTemplateIDLength);
149 try {
150 Document doc = textArea.getDocument();
151 doc.getText(caretPos-charsToGet, charsToGet, s);
152 int index = Collections.binarySearch(templates, s, comparator);
153 return index>=0 ? (CodeTemplate)templates.get(index) : null;
154 } catch (BadLocationException ble) {
155 ble.printStackTrace();
156 throw new InternalError("Error in CodeTemplateManager");
157 }
158 }
159
160
161 /**
162 * Returns the number of templates this manager knows about.
163 *
164 * @return The template count.
165 */
166 public synchronized int getTemplateCount() {
167 return templates.size();
168 }
169
170
171 /**
172 * Returns the templates currently available.
173 *
174 * @return The templates available.
175 */
176 public synchronized CodeTemplate[] getTemplates() {
177 CodeTemplate[] temp = new CodeTemplate[templates.size()];
178 return (CodeTemplate[])templates.toArray(temp);
179 }
180
181
182 /**
183 * Returns whether the specified character is a valid character for a
184 * <code>CodeTemplate</code> id.
185 *
186 * @param ch The character to check.
187 * @return Whether the character is a valid template character.
188 */
189 public static final boolean isValidChar(char ch) {
190 return RSyntaxUtilities.isLetterOrDigit(ch) || ch=='_';
191 }
192
193
194 /**
195 * Returns the specified code template.
196 *
197 * @param template The template to remove.
198 * @return <code>true</code> if the template was removed, <code>false</code>
199 * if the template was not in this template manager.
200 * @throws IllegalArgumentException If <code>template</code> is
201 * <code>null</code>.
202 * @see #removeTemplate(String)
203 * @see #addTemplate(CodeTemplate)
204 */
205 public synchronized boolean removeTemplate(CodeTemplate template) {
206
207 if (template==null) {
208 throw new IllegalArgumentException("template cannot be null");
209 }
210
211 // TODO: Do a binary search
212 return templates.remove(template);
213
214 }
215
216
217 /**
218 * Returns the code template with the specified id.
219 *
220 * @param id The id to check for.
221 * @return The code template that was removed, or <code>null</code> if
222 * there was no template with the specified ID.
223 * @throws IllegalArgumentException If <code>id</code> is <code>null</code>.
224 * @see #removeTemplate(CodeTemplate)
225 * @see #addTemplate(CodeTemplate)
226 */
227 public synchronized CodeTemplate removeTemplate(String id) {
228
229 if (id==null) {
230 throw new IllegalArgumentException("id cannot be null");
231 }
232
233 // TODO: Do a binary search
234 for (Iterator i=templates.iterator(); i.hasNext(); ) {
235 CodeTemplate template = (CodeTemplate)i.next();
236 if (id.equals(template.getID())) {
237 i.remove();
238 return template;
239 }
240 }
241
242 return null;
243
244 }
245
246
247 /**
248 * Replaces the current set of available templates with the ones
249 * specified.
250 *
251 * @param newTemplates The new set of templates. Note that we will
252 * be taking a shallow copy of these and sorting them.
253 */
254 public synchronized void replaceTemplates(CodeTemplate[] newTemplates) {
255 templates.clear();
256 if (newTemplates!=null) {
257 for (int i=0; i<newTemplates.length; i++) {
258 templates.add(newTemplates[i]);
259 }
260 }
261 sortTemplates(); // Also recomputes maxTemplateIDLength.
262 }
263
264
265 /**
266 * Saves all templates as XML files in the current template directory.
267 *
268 * @return Whether or not the save was successful.
269 */
270 public synchronized boolean saveTemplates() {
271
272 if (templates==null)
273 return true;
274 if (directory==null || !directory.isDirectory())
275 return false;
276
277 // Blow away all old XML files to start anew, as some might be from
278 // templates we're removed from the template manager.
279 File[] oldXMLFiles = directory.listFiles(new XMLFileFilter());
280 if (oldXMLFiles==null)
281 return false; // Either an IOException or it isn't a directory.
282 int count = oldXMLFiles.length;
283 for (int i=0; i<count; i++) {
284 /*boolean deleted = */oldXMLFiles[i].delete();
285 }
286
287 // Save all current templates as XML.
288 boolean wasSuccessful = true;
289 for (Iterator i=templates.iterator(); i.hasNext(); ) {
290 CodeTemplate template = (CodeTemplate)i.next();
291 File xmlFile = new File(directory, template.getID() + ".xml");
292 try {
293 XMLEncoder e = new XMLEncoder(new BufferedOutputStream(
294 new FileOutputStream(xmlFile)));
295 e.writeObject(template);
296 e.close();
297 } catch (IOException ioe) {
298 ioe.printStackTrace();
299 wasSuccessful = false;
300 }
301 }
302
303 return wasSuccessful;
304
305 }
306
307
308 /**
309 * Sets the "trigger" character for templates.
310 *
311 * @param trigger The trigger character to set for templates. This means
312 * that when this character is pressed in an
313 * <code>RSyntaxTextArea</code>, the last-typed token is found,
314 * and is checked against all template ID's to see if a template
315 * should be inserted. If a template ID matches, that template is
316 * inserted; if not, the trigger character is inserted. If this
317 * parameter is <code>null</code>, no change is made to the trigger
318 * character.
319 * @see #getInsertTrigger()
320 * @see #getInsertTriggerString()
321 */
322 /*
323 * FIXME: The trigger set here IS inserted when no matching template
324 * is found, but a space character (" ") is always used as the "trigger"
325 * to look for templates. This is because it is hard-coded in
326 * RSyntaxTextArea's input map this way. We need to change this.
327 * See RSyntaxTextAreaDefaultInputMap.java.
328 */
329 public void setInsertTrigger(KeyStroke trigger) {
330 if (trigger!=null) {
331 insertTrigger = trigger;
332 insertTriggerString = Character.toString(trigger.getKeyChar());
333 }
334 }
335
336
337 /**
338 * Sets the directory in which to look for templates. Calling this
339 * method adds any new templates found in the specified directory to
340 * the templates already registered.
341 *
342 * @param dir The new directory in which to look for templates.
343 * @return The new number of templates in this template manager, or
344 * <code>-1</code> if the specified directory does not exist.
345 */
346 public synchronized int setTemplateDirectory(File dir) {
347
348 if (dir!=null && dir.isDirectory()) {
349
350 this.directory = dir;
351
352 File[] files = dir.listFiles(new XMLFileFilter());
353 int newCount = files==null ? 0 : files.length;
354 int oldCount = templates.size();
355
356 List temp = new ArrayList(oldCount+newCount);
357 temp.addAll(templates);
358
359 for (int i=0; i<newCount; i++) {
360 try {
361 XMLDecoder d = new XMLDecoder(new BufferedInputStream(
362 new FileInputStream(files[i])));
363 Object obj = d.readObject();
364 if (!(obj instanceof CodeTemplate)) {
365 throw new IOException("Not a CodeTemplate: " +
366 files[i].getAbsolutePath());
367 }
368 temp.add(obj);
369 d.close();
370 } catch (/*IO, NoSuchElement*/Exception e) {
371 // NoSuchElementException can be thrown when reading
372 // an XML file not in the format expected by XMLDecoder.
373 // (e.g. CodeTemplates in an old format).
374 e.printStackTrace();
375 }
376 }
377 templates = temp;
378 sortTemplates();
379
380 return getTemplateCount();
381
382 }
383
384 return -1;
385
386 }
387
388
389 /**
390 * Removes any null entries in the current set of templates (if
391 * any), sorts the remaining templates, and computes the new
392 * maximum template ID length.
393 */
394 private synchronized void sortTemplates() {
395
396 // Get the maximum length of a template ID.
397 maxTemplateIDLength = 0;
398
399 // Remove any null entries (should only happen because of
400 // IOExceptions, etc. when loading from files), and sort
401 // the remaining list.
402 for (Iterator i=templates.iterator(); i.hasNext(); ) {
403 CodeTemplate temp = (CodeTemplate)i.next();
404 if (temp==null || temp.getID()==null) {
405 i.remove();
406 }
407 else {
408 maxTemplateIDLength = Math.max(maxTemplateIDLength,
409 temp.getID().length());
410 }
411 }
412
413 Collections.sort(templates);
414
415 }
416
417
418 /**
419 * A comparator that takes a <code>CodeTemplate</code> as its first
420 * parameter and a <code>Segment</code> as its second, and knows
421 * to compare the template's ID to the segment's text.
422 */
423 private static class TemplateComparator implements Comparator, Serializable{
424
425 public int compare(Object template, Object segment) {
426
427 // Get template start index (0) and length.
428 CodeTemplate t = (CodeTemplate)template;
429 final char[] templateArray = t.getID().toCharArray();
430 int i = 0;
431 int len1 = templateArray.length;
432
433 // Find "token" part of segment and get its offset and length.
434 Segment s = (Segment)segment;
435 char[] segArray = s.array;
436 int len2 = s.count;
437 int j = s.offset + len2 - 1;
438 while (j>=s.offset && isValidChar(segArray[j])) {
439 j--;
440 }
441 j++;
442 int segShift = j - s.offset;
443 len2 -= segShift;
444
445 int n = Math.min(len1, len2);
446 while (n-- != 0) {
447 char c1 = templateArray[i++];
448 char c2 = segArray[j++];
449 if (c1 != c2)
450 return c1 - c2;
451 }
452 return len1 - len2;
453
454 }
455
456 }
457
458
459 /**
460 * A file filter for File.listFiles() (NOT for JFileChoosers!) that
461 * accepts only XML files.
462 */
463 private static class XMLFileFilter implements FileFilter {
464 public boolean accept(File f) {
465 return f.getName().toLowerCase().endsWith(".xml");
466 }
467 }
468
469
470}
Note: See TracBrowser for help on using the repository browser.