• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.ide.eclipse.adt.internal.editors;
17 
18 import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_CONTENT;
19 import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_EMPTY_TAG_CLOSE;
20 import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_END_TAG_OPEN;
21 import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_CLOSE;
22 import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_NAME;
23 import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_OPEN;
24 
25 import com.android.ide.eclipse.adt.AdtPlugin;
26 import com.android.ide.eclipse.adt.AdtUtils;
27 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
28 import com.android.utils.Pair;
29 
30 import org.eclipse.jface.text.BadLocationException;
31 import org.eclipse.jface.text.DocumentCommand;
32 import org.eclipse.jface.text.IAutoEditStrategy;
33 import org.eclipse.jface.text.IDocument;
34 import org.eclipse.jface.text.IRegion;
35 import org.eclipse.jface.text.TextUtilities;
36 import org.eclipse.ui.texteditor.ITextEditor;
37 import org.eclipse.ui.texteditor.ITextEditorExtension3;
38 import org.eclipse.wst.sse.core.StructuredModelManager;
39 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
40 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
41 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
42 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
43 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
44 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
45 
46 /**
47  * Edit strategy for Android XML files. It attempts a number of edit
48  * enhancements:
49  * <ul>
50  *   <li> Auto indentation. The default XML indentation scheme is to just copy the
51  *        indentation of the previous line. This edit strategy improves on that situation
52  *        by considering the tag and bracket balance on the current line and using it
53  *        to determine whether the next line should be indented or use the same
54  *        indentation as the parent, or even the indentation of an earlier line
55  *        (when for example the current line closes an element which was started on an
56  *        earlier line.)
57  *   <li> Newline handling. In addition to indenting, it can also adjust the following text
58  *        appropriately when a newline is inserted. For example, it will reformat
59  *        the following (where | represents the caret position):
60  *    <pre>
61  *       {@code <item name="a">|</item>}
62  *    </pre>
63  *    into
64  *    <pre>
65  *       {@code <item name="a">}
66  *           |
67  *       {@code </item>}
68  *    </pre>
69  * </ul>
70  * In the future we might consider other editing enhancements here as well, such as
71  * refining the comment handling, or reindenting when you type the / of a closing tag,
72  * or even making the bracket matcher more resilient.
73  */
74 @SuppressWarnings("restriction") // XML model
75 public class AndroidXmlAutoEditStrategy implements IAutoEditStrategy {
76 
77     @Override
customizeDocumentCommand(IDocument document, DocumentCommand c)78     public void customizeDocumentCommand(IDocument document, DocumentCommand c) {
79         if (!isSmartInsertMode()) {
80             return;
81         }
82 
83         if (!(document instanceof IStructuredDocument)) {
84             // This shouldn't happen unless this strategy is used on an invalid document
85             return;
86         }
87         IStructuredDocument doc = (IStructuredDocument) document;
88 
89         // Handle newlines/indentation
90         if (c.length == 0 && c.text != null
91                 && TextUtilities.endsWith(doc.getLegalLineDelimiters(), c.text) != -1) {
92 
93             IModelManager modelManager = StructuredModelManager.getModelManager();
94             IStructuredModel model = modelManager.getModelForRead(doc);
95             if (model != null) {
96                 try {
97                     final int offset = c.offset;
98                     int lineStart = findLineStart(doc, offset);
99                     int textStart = findTextStart(doc, lineStart, offset);
100 
101                     IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(textStart);
102                     if (region != null && region.getType().equals(XML_TAG_NAME)) {
103                         Pair<Integer,Integer> balance = getBalance(doc, textStart, offset);
104                         int tagBalance = balance.getFirst();
105                         int bracketBalance = balance.getSecond();
106 
107                         String lineIndent = ""; //$NON-NLS-1$
108                         if (textStart > lineStart) {
109                             lineIndent = doc.get(lineStart, textStart - lineStart);
110                         }
111 
112                         // We only care if tag or bracket balance is greater than 0;
113                         // we never *dedent* on negative balances
114                         boolean addIndent = false;
115                         if (bracketBalance < 0) {
116                             // Handle
117                             //    <foo
118                             //        ></foo>^
119                             // and
120                             //    <foo
121                             //        />^
122                             ITextRegion left = getRegionAt(doc, offset, true /*biasLeft*/);
123                             if (left != null
124                                     && (left.getType().equals(XML_TAG_CLOSE)
125                                         || left.getType().equals(XML_EMPTY_TAG_CLOSE))) {
126 
127                                 // Find the corresponding open tag...
128                                 // The org.eclipse.wst.xml.ui.gotoMatchingTag frequently
129                                 // doesn't work, it just says "No matching brace found"
130                                 // (or I would use that here).
131 
132                                 int targetBalance = 0;
133                                 ITextRegion right = getRegionAt(doc, offset, false /*biasLeft*/);
134                                 if (right != null && right.getType().equals(XML_END_TAG_OPEN)) {
135                                     targetBalance = -1;
136                                 }
137                                 int openTag = AndroidXmlCharacterMatcher.findTagBackwards(doc,
138                                         offset, targetBalance);
139                                 if (openTag != -1) {
140                                     // Look up the indentation of the given line
141                                     lineIndent = AndroidXmlEditor.getIndentAtOffset(doc, openTag);
142                                 }
143                             }
144                         } else if (tagBalance > 0 || bracketBalance > 0) {
145                             // Add indentation
146                             addIndent = true;
147                         }
148 
149                         StringBuilder sb = new StringBuilder(c.text);
150                         sb.append(lineIndent);
151                         String oneIndentUnit = EclipseXmlFormatPreferences.create().getOneIndentUnit();
152                         if (addIndent) {
153                             sb.append(oneIndentUnit);
154                         }
155 
156                         // Handle
157                         //     <foo>^</foo>
158                         // turning into
159                         //     <foo>
160                         //         ^
161                         //     </foo>
162                         ITextRegion left = getRegionAt(doc, offset, true /*biasLeft*/);
163                         ITextRegion right = getRegionAt(doc, offset, false /*biasLeft*/);
164                         if (left != null && right != null
165                                 && left.getType().equals(XML_TAG_CLOSE)
166                                 && right.getType().equals(XML_END_TAG_OPEN)) {
167                             // Move end tag
168                             if (tagBalance > 0 && bracketBalance < 0) {
169                                 sb.append(oneIndentUnit);
170                             }
171                             c.caretOffset = offset + sb.length();
172                             c.shiftsCaret = false;
173                             sb.append(TextUtilities.getDefaultLineDelimiter(doc));
174                             sb.append(lineIndent);
175                         }
176                         c.text = sb.toString();
177                     } else if (region != null && region.getType().equals(XML_CONTENT)) {
178                         // Indenting in text content. If you're in the middle of editing
179                         // text, just copy the current line indentation.
180                         // However, if you're editing in leading whitespace (e.g. you press
181                         // newline on a blank line following say an element) then figure
182                         // out the indentation as if the newline had been pressed at the
183                         // end of the element, and insert that amount of indentation.
184                         // In this case we need to also make sure to subtract any existing
185                         // whitespace on the current line such that if we have
186                         //
187                         // <foo>
188                         // ^   <bar/>
189                         // </foo>
190                         //
191                         // you end up with
192                         //
193                         // <foo>
194                         //
195                         //    ^<bar/>
196                         // </foo>
197                         //
198                         String text = region.getText();
199                         int regionStart = region.getStartOffset();
200                         int delta = offset - regionStart;
201                         boolean inWhitespacePrefix = true;
202                         for (int i = 0, n = Math.min(delta, text.length()); i < n; i++) {
203                             char ch = text.charAt(i);
204                             if (!Character.isWhitespace(ch)) {
205                                 inWhitespacePrefix = false;
206                                 break;
207                             }
208                         }
209                         if (inWhitespacePrefix) {
210                             IStructuredDocumentRegion previous = region.getPrevious();
211                             if (previous != null && previous.getType() == XML_TAG_NAME) {
212                                 ITextRegionList subRegions = previous.getRegions();
213                                 ITextRegion last = subRegions.get(subRegions.size() - 1);
214                                 if (last.getType() == XML_TAG_CLOSE ||
215                                         last.getType() == XML_EMPTY_TAG_CLOSE) {
216                                     // See if the last tag was a closing tag
217                                     boolean wasClose = last.getType() == XML_EMPTY_TAG_CLOSE;
218                                     if (!wasClose) {
219                                         // Search backwards to see if the XML_TAG_CLOSE
220                                         // is the end of an </endtag>
221                                         for (int i = subRegions.size() - 2; i >= 0; i--) {
222                                             ITextRegion current = subRegions.get(i);
223                                             String type = current.getType();
224                                             if (type != XML_TAG_NAME) {
225                                                 wasClose = type == XML_END_TAG_OPEN;
226                                                 break;
227                                             }
228                                         }
229                                     }
230 
231                                     int begin = AndroidXmlCharacterMatcher.findTagBackwards(doc,
232                                             previous.getStartOffset() + last.getStart(), 0);
233                                     int prevLineStart = findLineStart(doc, begin);
234                                     int prevTextStart = findTextStart(doc, prevLineStart, begin);
235 
236                                     String lineIndent = ""; //$NON-NLS-1$
237                                     if (prevTextStart > prevLineStart) {
238                                         lineIndent = doc.get(prevLineStart,
239                                                 prevTextStart - prevLineStart);
240                                     }
241                                     StringBuilder sb = new StringBuilder(c.text);
242                                     sb.append(lineIndent);
243 
244                                     // See if there is whitespace on the insert line that
245                                     // we should also remove
246                                     for (int i = delta, n = text.length(); i < n; i++) {
247                                         char ch = text.charAt(i);
248                                         if (ch == ' ') {
249                                             c.length++;
250                                         } else {
251                                             break;
252                                         }
253                                     }
254 
255                                     boolean addIndent = (last.getType() == XML_TAG_CLOSE)
256                                             && !wasClose;
257 
258                                     // Is there just whitespace left of this text tag
259                                     // until we reach an end tag?
260                                     boolean whitespaceToEndTag = true;
261                                     for (int i = delta; i < text.length(); i++) {
262                                         char ch = text.charAt(i);
263                                         if (ch == '\n' || !Character.isWhitespace(ch)) {
264                                             whitespaceToEndTag = false;
265                                             break;
266                                         }
267                                     }
268                                     if (whitespaceToEndTag) {
269                                         IStructuredDocumentRegion next = region.getNext();
270                                         if (next != null && next.getType() == XML_TAG_NAME) {
271                                             String nextType = next.getRegions().get(0).getType();
272                                             if (nextType == XML_END_TAG_OPEN) {
273                                                 addIndent = false;
274                                             }
275                                         }
276                                     }
277 
278                                     if (addIndent) {
279                                         sb.append(EclipseXmlFormatPreferences.create()
280                                                 .getOneIndentUnit());
281                                     }
282                                     c.text = sb.toString();
283 
284                                     return;
285                                 }
286                             }
287                         }
288                         copyPreviousLineIndentation(doc, c);
289                     } else {
290                         copyPreviousLineIndentation(doc, c);
291                     }
292                 } catch (BadLocationException e) {
293                     AdtPlugin.log(e, null);
294                 } finally {
295                     model.releaseFromRead();
296                 }
297             }
298         }
299     }
300 
301     /**
302      * Returns the offset of the start of the line (which might be whitespace)
303      *
304      * @param document the document
305      * @param offset an offset for a character anywhere on the line
306      * @return the offset of the first character on the line
307      * @throws BadLocationException if the offset is invalid
308      */
findLineStart(IDocument document, int offset)309     public static int findLineStart(IDocument document, int offset) throws BadLocationException {
310         offset = Math.max(0, Math.min(offset, document.getLength() - 1));
311         IRegion info = document.getLineInformationOfOffset(offset);
312         return info.getOffset();
313     }
314 
315     /**
316      * Finds the first non-whitespace character on the given line
317      *
318      * @param document the document to search
319      * @param lineStart the offset of the beginning of the line
320      * @param lineEnd the offset of the end of the line, or the maximum position on the
321      *            line to search
322      * @return the offset of the first non whitespace character, or the maximum position,
323      *         whichever is smallest
324      * @throws BadLocationException if the offsets are invalid
325      */
findTextStart(IDocument document, int lineStart, int lineEnd)326     public static int findTextStart(IDocument document, int lineStart, int lineEnd)
327             throws BadLocationException {
328         for (int offset = lineStart; offset < lineEnd; offset++) {
329             char c = document.getChar(offset);
330             if (c != ' ' && c != '\t') {
331                 return offset;
332             }
333         }
334 
335         return lineEnd;
336     }
337 
338     /**
339      * Indent the new line the same way as the current line.
340      *
341      * @param doc the document to indent in
342      * @param command the document command to customize
343      * @throws BadLocationException if the offsets are invalid
344      */
copyPreviousLineIndentation(IDocument doc, DocumentCommand command)345     private void copyPreviousLineIndentation(IDocument doc, DocumentCommand command)
346             throws BadLocationException {
347 
348         if (command.offset == -1 || doc.getLength() == 0) {
349             return;
350         }
351 
352         int lineStart = findLineStart(doc, command.offset);
353         int textStart = findTextStart(doc, lineStart, command.offset);
354 
355         StringBuilder sb = new StringBuilder(command.text);
356         if (textStart > lineStart) {
357             sb.append(doc.get(lineStart, textStart - lineStart));
358         }
359 
360         command.text = sb.toString();
361     }
362 
363 
364     /**
365      * Returns the subregion at the given offset, with a bias to the left or a bias to the
366      * right. In other words, if | represents the caret position, in the XML
367      * {@code <foo>|</bar>} then the subregion with bias left is the closing {@code >} and
368      * the subregion with bias right is the opening {@code </}.
369      *
370      * @param doc the document
371      * @param offset the offset in the document
372      * @param biasLeft whether we should look at the token on the left or on the right
373      * @return the subregion at the given offset, or null if not found
374      */
getRegionAt(IStructuredDocument doc, int offset, boolean biasLeft)375     private static ITextRegion getRegionAt(IStructuredDocument doc, int offset,
376             boolean biasLeft) {
377         if (biasLeft) {
378             offset--;
379         }
380         IStructuredDocumentRegion region =
381                 doc.getRegionAtCharacterOffset(offset);
382         if (region != null) {
383             return region.getRegionAtCharacterOffset(offset);
384         }
385 
386         return null;
387     }
388 
389     /**
390      * Returns a pair of (tag-balance,bracket-balance) for the range textStart to offset.
391      *
392      * @param doc the document
393      * @param start the offset of the starting character (inclusive)
394      * @param end the offset of the ending character (exclusive)
395      * @return the balance of tags and brackets
396      */
getBalance(IStructuredDocument doc, int start, int end)397     private static Pair<Integer, Integer> getBalance(IStructuredDocument doc,
398             int start, int end) {
399         // Balance of open and closing tags
400         // <foo></foo> has tagBalance = 0, <foo> has tagBalance = 1
401         int tagBalance = 0;
402         // Balance of open and closing brackets
403         // <foo attr1="value1"> has bracketBalance = 1, <foo has bracketBalance = 1
404         int bracketBalance = 0;
405         IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(start);
406 
407         if (region != null) {
408             boolean inOpenTag = true;
409             while (region != null && region.getStartOffset() < end) {
410                 int regionStart = region.getStartOffset();
411                 ITextRegionList subRegions = region.getRegions();
412                 for (int i = 0, n = subRegions.size(); i < n; i++) {
413                     ITextRegion subRegion = subRegions.get(i);
414                     int subRegionStart = regionStart + subRegion.getStart();
415                     int subRegionEnd = regionStart + subRegion.getEnd();
416                     if (subRegionEnd < start || subRegionStart >= end) {
417                         continue;
418                     }
419                     String type = subRegion.getType();
420 
421                     if (XML_TAG_OPEN.equals(type)) {
422                         bracketBalance++;
423                         inOpenTag = true;
424                     } else if (XML_TAG_CLOSE.equals(type)) {
425                         bracketBalance--;
426                         if (inOpenTag) {
427                             tagBalance++;
428                         } else {
429                             tagBalance--;
430                         }
431                     } else if (XML_END_TAG_OPEN.equals(type)) {
432                         bracketBalance++;
433                         inOpenTag = false;
434                     } else if (XML_EMPTY_TAG_CLOSE.equals(type)) {
435                         bracketBalance--;
436                     }
437                 }
438 
439                 region = region.getNext();
440             }
441         }
442 
443         return Pair.of(tagBalance, bracketBalance);
444     }
445 
446     /**
447      * Determine if we're in smart insert mode (if so, don't do any edit magic)
448      *
449      * @return true if the editor is in smart mode (or if it's an unknown editor type)
450      */
isSmartInsertMode()451     private static boolean isSmartInsertMode() {
452         ITextEditor textEditor = AdtUtils.getActiveTextEditor();
453         if (textEditor instanceof ITextEditorExtension3) {
454             ITextEditorExtension3 editor = (ITextEditorExtension3) textEditor;
455             return editor.getInsertMode() == ITextEditorExtension3.SMART_INSERT;
456         }
457 
458         return true;
459     }
460 }
461