View Javadoc

1   /*
2    * $Id: OWLFormattingStrategy.java,v 1.9 2005/06/01 17:38:33 jlerner Exp $
3    *
4    * Copyright (c) 1999-2004, BBN Technologies, LLC.
5    * All rights reserved.
6    * http://www.daml.org/legal/opensource/bbn_license.html
7    */
8    
9   package com.bbn.swede.editor.formatting;
10  
11  import java.util.LinkedList;
12  
13  import org.eclipse.jface.text.BadLocationException;
14  import org.eclipse.jface.text.IDocument;
15  import org.eclipse.jface.text.IRegion;
16  import org.eclipse.jface.text.ITypedRegion;
17  import org.eclipse.jface.text.formatter.FormattingContextProperties;
18  import org.eclipse.jface.text.formatter.IFormattingContext;
19  import org.eclipse.jface.text.formatter.IFormattingStrategy;
20  import org.eclipse.jface.text.formatter.IFormattingStrategyExtension;
21  import org.eclipse.text.edits.InsertEdit;
22  import org.eclipse.text.edits.MalformedTreeException;
23  import org.eclipse.text.edits.MultiTextEdit;
24  import org.eclipse.text.edits.ReplaceEdit;
25  import org.eclipse.text.edits.TextEdit;
26  
27  import com.bbn.swede.core.OWLCore;
28  import com.bbn.swede.editor.EditorPlugin;
29  import com.bbn.swede.editor.OWLPartitionScanner;
30  
31  /***
32   * <p>A master strategy for formatting OWL documents.  This strategy is 
33   * responsible for ensuring that begin and end tags are wrapped to new lines 
34   * where  appropriate and that literals are wrapped across multiple lines as 
35   * specified by editor preferences.  This strategy also takes care of proper 
36   * indentation for wrapped tags and literals.</p>
37   * 
38   * <p>Tags must be formatted in their entirety, so a tag partially selected at the
39   * beginning or end of the test range to be formatted may be modified.  In 
40   * order to ensure proper indentation of nested tags and end tags, whitespace
41   * before the first selected tag and after the last selected tag may also be
42   * modified.</p>
43   * @author jlerner
44   */
45  public class OWLFormattingStrategy implements IFormattingStrategy,
46     IFormattingStrategyExtension
47  {
48     private LinkedList _lRegions = new LinkedList();
49     private LinkedList _lDocuments = new LinkedList();
50     
51     /*
52      *  (non-Javadoc)
53      * @see org.eclipse.jface.text.formatter.IFormattingStrategy#formatterStarts(java.lang.String)
54      */
55     public void formatterStarts(String initialIndentation)
56     {
57        //do nothing
58     }
59  
60     /*
61      *  (non-Javadoc)
62      * @see org.eclipse.jface.text.formatter.IFormattingStrategy#format(
63      *    java.lang.String, boolean, java.lang.String, int[])
64      */
65     public String format(String content, boolean isLineStart,
66        String indentation, int[] positions)
67     {
68        //do nothing
69        return null;
70     }
71  
72     /*
73      *  (non-Javadoc)
74      * @see org.eclipse.jface.text.formatter.IFormattingStrategyExtension
75      *    #formatterStops()
76      */
77     public void formatterStops()
78     {
79        _lRegions.clear();
80        _lDocuments.clear();
81     }
82  
83     /* (non-Javadoc)
84      * @see org.eclipse.jface.text.formatter.IFormattingStrategyExtension#format()
85      */
86     public void format()
87     {
88        IDocument document = (IDocument)_lDocuments.removeFirst();
89        IRegion region = (IRegion)_lRegions.removeFirst();
90        if (document == null || region == null)
91        {
92           return;
93        }
94        
95        try
96        {
97           TextEdit edit = wrapTags(document, region);
98           if (edit != null)
99           {
100             edit.apply(document);
101          }
102       }
103       catch (MalformedTreeException e)
104       {
105          OWLCore.logWarning(EditorPlugin.getID(), "Unable to format tags", e);
106       }
107       catch (BadLocationException e)
108       {
109          OWLCore.logWarning(EditorPlugin.getID(), "Unable to format tags", e);
110       }
111       
112    }
113    
114    /***
115     * Calculates the start offset of the range that will actually be formatted.
116     * This may include text before the start of the selected region if it
117     * contains a partial tag or there is whitespace immediately preceding the
118     * region that must be formatted.
119     * @param document The document being formatted
120     * @param region The selected region within the document
121     * @return The offset into <code>document</code> where formatting should
122     *         actually begin
123     * @throws BadLocationException
124     */
125    private int getStartOffset(IDocument document, IRegion region)
126       throws BadLocationException
127    {
128       ITypedRegion partition = FormattingUtils.getNextTagPartition(document, region.getOffset());
129       if (partition == null 
130           || partition.getOffset() >= region.getOffset() + region.getLength())
131       {
132          //No tags to wrap
133          return -1;
134       }
135       
136       ITypedRegion partitionPrev = FormattingUtils.getPreviousTagPartition(document, partition);
137       int iOffset;
138       if (partitionPrev == null)
139       {
140          //partition is the first tag in the document.  Edit offset is the first
141          //trailing whitespace character on the preceding line.
142          int iLine = document.getLineOfOffset(partition.getOffset());
143          iLine--;
144          if (iLine >= 0)
145          {
146             IRegion rLine = document.getLineInformation(iLine);
147             String sLine = document.get(rLine.getOffset(), rLine.getLength()); 
148             String sTrimmed = sLine.trim();
149             iOffset = rLine.getOffset();
150             if (sTrimmed.length() > 0)
151             {
152                iOffset += sLine.indexOf(sTrimmed) + sTrimmed.length();
153             }
154          }
155          else
156          {
157             iLine++;
158             iOffset = document.getLineOffset(iLine);
159          }
160       }
161       else
162       {
163          iOffset = partitionPrev.getOffset() + partitionPrev.getLength();
164       }
165       
166       return iOffset;
167    }
168    
169    /***
170     * Calculates the end offset of the range that will actually be formatted.
171     * This may include text after the start of the selected region if it
172     * contains a partial tag or there is whitespace immediately following the
173     * region that must be formatted.
174     * @param document The document being formatted
175     * @param region The selected region within the document
176     * @return The offset into <code>document</code> where formatting should
177     *         actually begin
178     * @throws BadLocationException
179     */
180    private int getEndOffset(IDocument document, IRegion region)
181       throws BadLocationException
182    {
183       if (region.getLength() <= 0)
184       {
185          return -1;
186       }
187       ITypedRegion partition = FormattingUtils.getPreviousTagPartition(
188          document, region.getOffset() + region.getLength() - 1);
189       if (partition == null 
190           || partition.getOffset() < region.getOffset())
191       {
192          //No tags to wrap
193          return -1;
194       }
195      
196       ITypedRegion partitionNext = FormattingUtils.getNextTagPartition(document, partition);
197       int iOffset;
198       if (partitionNext == null)
199       {
200          int i = partition.getOffset() + partition.getLength();
201          String s = document.get(i, document.getLength() - i);
202          String sTrimmed = s.trim();
203          iOffset = partition.getOffset() + partition.getLength();
204          if (sTrimmed.length() > 0)
205          {
206             //There's something else in the document after this tag, so
207             //we want to ensure that the intervening whitespace is formatted
208             //and the subsequent text is on its own line.
209             iOffset +=  s.indexOf(sTrimmed);
210          }
211       }
212       else
213       {
214          iOffset = partitionNext.getOffset();
215       }
216       return iOffset;
217    }
218    
219    /***
220     * Handles basic wrapping and indentation of tags.
221     * @param document The document being formatted
222     * @param region The region of the document to format
223     * @return a TextEdit containing all of the changes made to the document.
224     * @throws BadLocationException
225     */
226    private TextEdit wrapTags(IDocument document, IRegion region)
227       throws BadLocationException
228    {
229       int iStartOffset = getStartOffset(document, region);
230       if (iStartOffset < 0)
231       {
232          return null;
233       }
234       int iEndOffset = getEndOffset(document, region);
235       if (iEndOffset < iStartOffset)
236       {
237          return null;
238       }
239       MultiTextEdit edit = new MultiTextEdit(iStartOffset, iEndOffset - iStartOffset);
240       String sFormat = document.get(iStartOffset, iEndOffset - iStartOffset);
241       OWLCore.trace("Formatting", sFormat, false);
242       int iOffset = iStartOffset;
243       ITypedRegion partition = FormattingUtils.getNextTagPartition(document, iOffset);
244       int iIndent = getBaseIndent(document, FormattingUtils.getPreviousTagPartition(document, partition));
245       while (partition != null && partition.getOffset() < iEndOffset)
246       {
247          if (partition.getType().equals(OWLPartitionScanner.BEGIN_TAG))
248          {
249             String sBetween = document.get(iOffset, partition.getOffset() - iOffset);
250             String sTrimmed = sBetween.trim();
251             if (sTrimmed.length() > 0)
252             {
253                iOffset += sBetween.indexOf(sTrimmed) + sTrimmed.length();
254             }
255             
256             String sIndent = FormattingUtils.getIndent(iIndent);
257             //            edit.addChild(new InsertEdit(partition.getOffset(), "\n" + sIndent));
258             edit.addChild(new ReplaceEdit(
259                iOffset, partition.getOffset() - iOffset, "\n" + sIndent));
260             iIndent = adjustIndent(iIndent, document, partition);
261          }
262          else if (partition.getType().equals(OWLPartitionScanner.END_TAG))
263          {
264             iIndent = adjustIndent(iIndent, document, partition);
265             TextEdit newEdit = wrapEndTag(iIndent, document, partition);
266             if (newEdit != null)
267             {
268                edit.addChild(newEdit);
269             }
270          }
271          iOffset = partition.getOffset() + partition.getLength();
272          partition = FormattingUtils.getNextTagPartition(document, partition);
273       }
274       
275       if (iOffset < iEndOffset)
276       {
277          String s = document.get(iOffset, iEndOffset - iOffset);
278          String sTrimmed = s.trim();
279          if (sTrimmed.length() > 0)
280          {
281             iEndOffset = iOffset + s.indexOf(sTrimmed);
282          }
283          String sIndent = FormattingUtils.getIndent(iIndent);
284          edit.addChild(new ReplaceEdit(iOffset, iEndOffset - iOffset, "\n" + sIndent));
285       }
286       else
287       {
288          ITypedRegion partitionNext = document.getPartition(iOffset);
289          if (partitionNext.getType().equals(OWLPartitionScanner.BEGIN_TAG)
290              || partitionNext.getType().equals(OWLPartitionScanner.END_TAG))
291          {
292             if (!FormattingUtils.isBeginTag(document, partitionNext))
293             {
294                iIndent = Math.max(0, iIndent - FormattingUtils.getTagIndentSize());
295             }
296             String sIndent = FormattingUtils.getIndent(iIndent);
297             edit.addChild(new InsertEdit(iOffset, "\n" + sIndent));
298          }
299       }
300 
301       return edit;
302 
303    }
304    
305    /***
306     * Determines a base indent level for a formatting operation based on
307     * the previous tag in the document.
308     * @param document The document being formatted
309     * @param partitionPrev The partition containing the previous tag
310     * @return The indentation level to use for the next begin tag inserted
311     * @throws BadLocationException
312     */
313    private int getBaseIndent(IDocument document, ITypedRegion partitionPrev)
314       throws BadLocationException
315    {
316       if (partitionPrev == null)
317       {
318          return 0;
319       }
320       
321       int iBase = FormattingUtils.getIndentLength(document, partitionPrev);
322       
323       if (FormattingUtils.isBeginTag(document, partitionPrev))
324       {
325          return iBase + FormattingUtils.getTagIndentSize();
326       }
327       else 
328       {
329          return iBase;
330       }
331    }
332    
333    /***
334     * Adjusts the indentation level to compensate for a partition that has been
335     * formatted.  The indent will be reduced after an end tag and increased
336     * after a begin tag.
337     * @param iIndent The current indentation level
338     * @param document The document being formatted
339     * @param partition The partition containing the last tag processed
340     * @return The new indentation level
341     * @throws BadLocationException
342     */
343    private int adjustIndent(int iIndent, IDocument document, ITypedRegion partition)
344       throws BadLocationException
345    {
346       if (partition == null)
347       {
348          return iIndent;
349       }
350       
351       if (FormattingUtils.isBeginTag(document, partition))
352       {
353          return iIndent + FormattingUtils.getTagIndentSize();
354       }
355       else if (FormattingUtils.isEndTag(document, partition))
356       {
357          return Math.max(iIndent - FormattingUtils.getTagIndentSize(), 0);
358       }
359       return iIndent;
360    }
361    
362    /***
363     * Determines whether or not an end tag needs to be wrapped to a new line.
364     * If wrapping is necessary, a text edit will be created to perform the
365     * operation.
366     * @param document The document containing the tag
367     * @param tag The partition of the tag within the document
368     * @return A text edit for inserting a newline and any necessary indentation,
369     *         or <code>null</code> if the tag need not be wrapped.
370     */
371    private TextEdit wrapEndTag(int iIndent, IDocument document, ITypedRegion tag)
372       throws BadLocationException
373    {
374       ITypedRegion tagPrevious = FormattingUtils.getPreviousTagPartition(document, tag);
375       if (tagPrevious == null)
376       {
377          return null;
378       }
379       if (FormattingUtils.isBeginTag(document, tagPrevious))
380       {
381          //begin tag is on its own line already and indented by iIndent spaces
382          String s = document.get(tagPrevious.getOffset(), 
383             tag.getOffset() + tag.getLength() - tagPrevious.getOffset());
384          //TODO replace between-tag whitespace; unwrap and re-wrap literals.
385          if (EditorPlugin.splitLiteral(iIndent + s.length()))
386          {
387             int offset = tagPrevious.getOffset() + tagPrevious.getLength();
388             int length = tag.getOffset() - offset;
389             return wrapLiteral(document, offset, length, iIndent);
390          }
391          else
392          {
393             return null;
394          }
395       }
396       else
397       {
398          int iOffset = tagPrevious.getOffset() + tagPrevious.getLength();
399          //make sure we don't edit outside the formatting region
400 //         iOffset = Math.max(iOffset, region.getOffset());
401          String s = document.get(iOffset, tag.getOffset() - iOffset);
402          String sTrimmed = s.trim();
403          if (sTrimmed.length() > 0)
404          {
405             iOffset += s.indexOf(sTrimmed) + sTrimmed.length();
406             s = s.substring(s.indexOf(sTrimmed) + sTrimmed.length());
407          }
408          String sIndent = FormattingUtils.getIndent(iIndent);
409          return new ReplaceEdit(iOffset, tag.getOffset() - iOffset, "\n" + sIndent);
410 //         return new InsertEdit(tag.getOffset(), "\n" + sIndent);
411       }
412    }
413    
414    /***
415     * Splits a range of text, assumed to be a literal, across multiple lines.
416     * Minimally, a newline and indentation will be added before and after the
417     * literal.  Additional line breaks and indentation will be added within the
418     * literal as necessary according to the user's maximum line length 
419     * preference.
420     * @param document The document containing the literal
421     * @param offset Start offset of the literal being wrapped
422     * @param length Length of the literal being wrapped
423     * @param indent Indentation level of the literal's enclosing tag.
424     * @return A text edit containing all the necessary operations to line-wrap
425     *         the literal.
426     */
427    private TextEdit wrapLiteral(IDocument document, int offset, int length, int indent)
428       throws BadLocationException
429    {
430       MultiTextEdit edit = new MultiTextEdit(offset, length);
431       int iMaxLength = EditorPlugin.getMaximumLineLength();
432       String sIndent = FormattingUtils.getIndent(indent);
433       edit.addChild(new InsertEdit(offset + length, "\n" + sIndent));
434       
435       indent += FormattingUtils.getTagIndentSize();
436       sIndent = FormattingUtils.getIndent(indent);
437       edit.addChild(new InsertEdit(offset, "\n" + sIndent));
438       
439       //adjust line length to reflect characters lost to leading indent
440       iMaxLength -= indent;
441       if (iMaxLength <= 0)
442       {
443          //The indentation level somehow got really out of hand, don't even bother
444          return edit;
445       }
446       
447       while (length > iMaxLength)
448       {
449          String sLiteral = document.get(offset, length);
450          
451          //Find most recent whitespace
452          int i = iMaxLength;
453          for (; i > 0 && !Character.isWhitespace(sLiteral.charAt(i)); i--)
454          {
455             //do nothing
456          }
457          if (i == 0)
458          {
459             //Oops, no whitespace.  Search forward instead to split the line as
460             //close to the intended limit as possible.
461             i = iMaxLength;
462             for (; i < sLiteral.length() && !Character.isWhitespace(sLiteral.charAt(i)); i++)
463             {
464                //do nothing
465             }
466             if (i == sLiteral.length())
467             {
468                //No whitespace there, either; the literal can't be split any further
469                return edit;
470             }
471          }
472          
473          //Now, find the bounds of the whitespace where we're splitting the line
474          //so it can be removed in lieu of newline + indent
475          int iStart = i, iEnd = i;
476          for (; iStart >= 0 && Character.isWhitespace(sLiteral.charAt(iStart)); iStart--)
477          {
478             //do nothing
479          }
480          iStart++;
481          //iStart now indicates the first whitespace character to replace
482          for (; iEnd < sLiteral.length() && Character.isWhitespace(sLiteral.charAt(iEnd)); iEnd++)
483          {
484             //do nothing
485          }
486          //iEnd now indicates the first non-whitespace character that follows the replacement
487          int iLength = iEnd - iStart;
488          iStart += offset;
489          edit.addChild(new ReplaceEdit(iStart, iLength, "\n" + sIndent));
490          offset = iStart + iLength;
491          length -= iEnd;
492       }
493       return edit;
494    }
495 
496    /* (non-Javadoc)
497     * @see org.eclipse.jface.text.formatter.IFormattingStrategyExtension
498     *    #formatterStarts(org.eclipse.jface.text.formatter.IFormattingContext)
499     */
500    public void formatterStarts(IFormattingContext context)
501    {
502       _lRegions.addLast(context.getProperty(FormattingContextProperties.CONTEXT_REGION));
503       _lDocuments.addLast(context.getProperty(FormattingContextProperties.CONTEXT_MEDIUM));
504    }
505 }