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_EMPTY_TAG_CLOSE;
19 import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_END_TAG_OPEN;
20 import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_CLOSE;
21 import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_NAME;
22 import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_OPEN;
23 
24 import org.eclipse.jface.text.IDocument;
25 import org.eclipse.jface.text.IRegion;
26 import org.eclipse.jface.text.Region;
27 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
28 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
29 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
30 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
31 import org.eclipse.wst.xml.ui.internal.text.XMLDocumentRegionEdgeMatcher;
32 
33 /**
34  * Custom version of the character matcher for XML files which adds the ability to
35  * jump between open and close tags in the XML file.
36  */
37 @SuppressWarnings("restriction")
38 public class AndroidXmlCharacterMatcher extends XMLDocumentRegionEdgeMatcher {
39     /**
40      * Constructs a new character matcher for Android XML files
41      */
AndroidXmlCharacterMatcher()42     public AndroidXmlCharacterMatcher() {
43     }
44 
45     @Override
match(IDocument doc, int offset)46     public IRegion match(IDocument doc, int offset) {
47         if (offset < 0 || offset >= doc.getLength()) {
48             return null;
49         }
50 
51         IRegion match = findOppositeTag(doc, offset);
52         if (match != null) {
53             return match;
54         }
55 
56         return super.match(doc, offset);
57     }
58 
findOppositeTag(IDocument document, int offset)59     private IRegion findOppositeTag(IDocument document, int offset) {
60         if (!(document instanceof IStructuredDocument)) {
61             return null;
62         }
63         IStructuredDocument doc = (IStructuredDocument) document;
64 
65         IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset);
66         if (region == null) {
67             return null;
68         }
69 
70         ITextRegion subRegion = region.getRegionAtCharacterOffset(offset);
71         if (subRegion == null) {
72             return null;
73         }
74         ITextRegionList subRegions = region.getRegions();
75         int index = subRegions.indexOf(subRegion);
76 
77         String type = subRegion.getType();
78         boolean isOpenTag = false;
79         boolean isCloseTag = false;
80 
81         if (type.equals(XML_TAG_OPEN)) {
82             isOpenTag = true;
83         } else if (type.equals(XML_END_TAG_OPEN)) {
84             isCloseTag = true;
85         } else if (!(type.equals(XML_TAG_CLOSE) || type.equals(XML_TAG_NAME)) &&
86                 (subRegion.getStart() + region.getStartOffset() == offset)) {
87             // Look to the left one character; we may have the case where you're
88             // pointing to the right of a tag, e.g.
89             //     <foo>^text
90             offset--;
91             region = doc.getRegionAtCharacterOffset(offset);
92             if (region == null) {
93                 return null;
94             }
95             subRegion = region.getRegionAtCharacterOffset(offset);
96             if (subRegion == null) {
97                 return null;
98             }
99             type = subRegion.getType();
100 
101             subRegions = region.getRegions();
102             index = subRegions.indexOf(subRegion);
103         }
104 
105         if (type.equals(XML_TAG_CLOSE) || type.equals(XML_TAG_NAME)) {
106             for (int i = index; i >= 0; i--) {
107                 subRegion = subRegions.get(i);
108                 type = subRegion.getType();
109                 if (type.equals(XML_TAG_OPEN)) {
110                     isOpenTag = true;
111                     break;
112                 } else if (type.equals(XML_END_TAG_OPEN)) {
113                     isCloseTag = true;
114                     break;
115                 }
116             }
117         }
118 
119         if (isOpenTag) {
120             // Find closing tag
121             int target = findTagForwards(doc, subRegion.getStart() + region.getStartOffset(), 0);
122             // Note - there is no point in looking up the whole region for the matching
123             // tag, because even if you pass a length greater than 1 here, the paint highlighter
124             // will only highlight a single character -- the *last* character of the region,
125             // not the whole region itself.
126             return new Region(target, 1);
127         } else if (isCloseTag) {
128             // Find open tag
129             int target = findTagBackwards(doc, subRegion.getStart() + region.getStartOffset(), -1);
130             return new Region(target, 1);
131         }
132 
133         return null;
134     }
135 
136     /**
137      * Finds the corresponding open tag by searching backwards until the tag balance
138      * reaches a given target.
139      *
140      * @param doc the document
141      * @param offset the ending offset (where the search begins searching backwards from)
142      * @param targetTagBalance the balance to end the search at
143      * @return the offset of the beginning of the open tag
144      */
findTagBackwards(IStructuredDocument doc, int offset, int targetTagBalance)145     public static int findTagBackwards(IStructuredDocument doc, int offset, int targetTagBalance) {
146         // Balance of open and closing tags
147         int tagBalance = 0;
148         // Balance of open and closing brackets
149         IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset);
150         if (region != null) {
151             boolean inEmptyTag = true;
152 
153             while (region != null) {
154                 int regionStart = region.getStartOffset();
155                 ITextRegionList subRegions = region.getRegions();
156                 for (int i = subRegions.size() - 1; i >= 0; i--) {
157                     ITextRegion subRegion = subRegions.get(i);
158                     int subRegionStart = regionStart + subRegion.getStart();
159                     if (subRegionStart >= offset) {
160                         continue;
161                     }
162                     String type = subRegion.getType();
163 
164                     // Iterate backwards and keep track of the tag balance such that
165                     // we can find the corresponding opening tag
166 
167                     if (XML_TAG_OPEN.equals(type)) {
168                         if (!inEmptyTag) {
169                             tagBalance--;
170                         }
171                         if (tagBalance == targetTagBalance) {
172                             return subRegionStart;
173                         }
174                     } else if (XML_END_TAG_OPEN.equals(type)) {
175                         tagBalance++;
176                     } else if (XML_EMPTY_TAG_CLOSE.equals(type)) {
177                         inEmptyTag = true;
178                     } else if (XML_TAG_CLOSE.equals(type)) {
179                         inEmptyTag = false;
180                     }
181                 }
182 
183                 region = region.getPrevious();
184             }
185         }
186 
187         return -1;
188     }
189 
190     /**
191      * Finds the corresponding closing tag by searching forwards until the tag balance
192      * reaches a given target.
193      *
194      * @param doc the document
195      * @param start the starting offset (where the search begins searching forwards from)
196      * @param targetTagBalance the balance to end the search at
197      * @return the offset of the beginning of the closing tag
198      */
findTagForwards(IStructuredDocument doc, int start, int targetTagBalance)199     public static int findTagForwards(IStructuredDocument doc, int start, int targetTagBalance) {
200         int tagBalance = 0;
201         IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(start);
202 
203         if (region != null) {
204             while (region != null) {
205                 int regionStart = region.getStartOffset();
206                 ITextRegionList subRegions = region.getRegions();
207                 for (int i = 0, n = subRegions.size(); i < n; i++) {
208                     ITextRegion subRegion = subRegions.get(i);
209                     int subRegionStart = regionStart + subRegion.getStart();
210                     int subRegionEnd = regionStart + subRegion.getEnd();
211                     if (subRegionEnd < start) {
212                         continue;
213                     }
214                     String type = subRegion.getType();
215 
216                     if (XML_TAG_OPEN.equals(type)) {
217                         tagBalance++;
218                     } else if (XML_END_TAG_OPEN.equals(type)) {
219                         tagBalance--;
220                         if (tagBalance == targetTagBalance) {
221                             return subRegionStart;
222                         }
223                     } else if (XML_EMPTY_TAG_CLOSE.equals(type)) {
224                         tagBalance--;
225                         if (tagBalance == targetTagBalance) {
226                             // We don't jump to matching tags within a self-closed tag
227                             return -1;
228                         }
229                     }
230                 }
231 
232                 region = region.getNext();
233             }
234         }
235 
236         return -1;
237     }
238 }
239