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