| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Eclipse Public License, Version 1.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.eclipse.org/org/documents/epl-v10.php |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.ide.eclipse.adt.internal.editors; |
| |
| import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_EMPTY_TAG_CLOSE; |
| import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_END_TAG_OPEN; |
| import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_CLOSE; |
| import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_NAME; |
| import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_OPEN; |
| |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.Region; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList; |
| import org.eclipse.wst.xml.ui.internal.text.XMLDocumentRegionEdgeMatcher; |
| |
| /** |
| * Custom version of the character matcher for XML files which adds the ability to |
| * jump between open and close tags in the XML file. |
| */ |
| @SuppressWarnings("restriction") |
| public class AndroidXmlCharacterMatcher extends XMLDocumentRegionEdgeMatcher { |
| /** |
| * Constructs a new character matcher for Android XML files |
| */ |
| public AndroidXmlCharacterMatcher() { |
| } |
| |
| @Override |
| public IRegion match(IDocument doc, int offset) { |
| if (offset < 0 || offset >= doc.getLength()) { |
| return null; |
| } |
| |
| IRegion match = findOppositeTag(doc, offset); |
| if (match != null) { |
| return match; |
| } |
| |
| return super.match(doc, offset); |
| } |
| |
| private IRegion findOppositeTag(IDocument document, int offset) { |
| if (!(document instanceof IStructuredDocument)) { |
| return null; |
| } |
| IStructuredDocument doc = (IStructuredDocument) document; |
| |
| IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset); |
| if (region == null) { |
| return null; |
| } |
| |
| ITextRegion subRegion = region.getRegionAtCharacterOffset(offset); |
| if (subRegion == null) { |
| return null; |
| } |
| ITextRegionList subRegions = region.getRegions(); |
| int index = subRegions.indexOf(subRegion); |
| |
| String type = subRegion.getType(); |
| boolean isOpenTag = false; |
| boolean isCloseTag = false; |
| |
| if (type.equals(XML_TAG_OPEN)) { |
| isOpenTag = true; |
| } else if (type.equals(XML_END_TAG_OPEN)) { |
| isCloseTag = true; |
| } else if (!(type.equals(XML_TAG_CLOSE) || type.equals(XML_TAG_NAME)) && |
| (subRegion.getStart() + region.getStartOffset() == offset)) { |
| // Look to the left one character; we may have the case where you're |
| // pointing to the right of a tag, e.g. |
| // <foo>^text |
| offset--; |
| region = doc.getRegionAtCharacterOffset(offset); |
| if (region == null) { |
| return null; |
| } |
| subRegion = region.getRegionAtCharacterOffset(offset); |
| if (subRegion == null) { |
| return null; |
| } |
| type = subRegion.getType(); |
| |
| subRegions = region.getRegions(); |
| index = subRegions.indexOf(subRegion); |
| } |
| |
| if (type.equals(XML_TAG_CLOSE) || type.equals(XML_TAG_NAME)) { |
| for (int i = index; i >= 0; i--) { |
| subRegion = subRegions.get(i); |
| type = subRegion.getType(); |
| if (type.equals(XML_TAG_OPEN)) { |
| isOpenTag = true; |
| break; |
| } else if (type.equals(XML_END_TAG_OPEN)) { |
| isCloseTag = true; |
| break; |
| } |
| } |
| } |
| |
| if (isOpenTag) { |
| // Find closing tag |
| int target = findTagForwards(doc, subRegion.getStart() + region.getStartOffset(), 0); |
| // Note - there is no point in looking up the whole region for the matching |
| // tag, because even if you pass a length greater than 1 here, the paint highlighter |
| // will only highlight a single character -- the *last* character of the region, |
| // not the whole region itself. |
| return new Region(target, 1); |
| } else if (isCloseTag) { |
| // Find open tag |
| int target = findTagBackwards(doc, subRegion.getStart() + region.getStartOffset(), -1); |
| return new Region(target, 1); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Finds the corresponding open tag by searching backwards until the tag balance |
| * reaches a given target. |
| * |
| * @param doc the document |
| * @param offset the ending offset (where the search begins searching backwards from) |
| * @param targetTagBalance the balance to end the search at |
| * @return the offset of the beginning of the open tag |
| */ |
| public static int findTagBackwards(IStructuredDocument doc, int offset, int targetTagBalance) { |
| // Balance of open and closing tags |
| int tagBalance = 0; |
| // Balance of open and closing brackets |
| IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset); |
| if (region != null) { |
| boolean inEmptyTag = true; |
| |
| while (region != null) { |
| int regionStart = region.getStartOffset(); |
| ITextRegionList subRegions = region.getRegions(); |
| for (int i = subRegions.size() - 1; i >= 0; i--) { |
| ITextRegion subRegion = subRegions.get(i); |
| int subRegionStart = regionStart + subRegion.getStart(); |
| if (subRegionStart >= offset) { |
| continue; |
| } |
| String type = subRegion.getType(); |
| |
| // Iterate backwards and keep track of the tag balance such that |
| // we can find the corresponding opening tag |
| |
| if (XML_TAG_OPEN.equals(type)) { |
| if (!inEmptyTag) { |
| tagBalance--; |
| } |
| if (tagBalance == targetTagBalance) { |
| return subRegionStart; |
| } |
| } else if (XML_END_TAG_OPEN.equals(type)) { |
| tagBalance++; |
| } else if (XML_EMPTY_TAG_CLOSE.equals(type)) { |
| inEmptyTag = true; |
| } else if (XML_TAG_CLOSE.equals(type)) { |
| inEmptyTag = false; |
| } |
| } |
| |
| region = region.getPrevious(); |
| } |
| } |
| |
| return -1; |
| } |
| |
| /** |
| * Finds the corresponding closing tag by searching forwards until the tag balance |
| * reaches a given target. |
| * |
| * @param doc the document |
| * @param start the starting offset (where the search begins searching forwards from) |
| * @param targetTagBalance the balance to end the search at |
| * @return the offset of the beginning of the closing tag |
| */ |
| public static int findTagForwards(IStructuredDocument doc, int start, int targetTagBalance) { |
| int tagBalance = 0; |
| IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(start); |
| |
| if (region != null) { |
| while (region != null) { |
| int regionStart = region.getStartOffset(); |
| ITextRegionList subRegions = region.getRegions(); |
| for (int i = 0, n = subRegions.size(); i < n; i++) { |
| ITextRegion subRegion = subRegions.get(i); |
| int subRegionStart = regionStart + subRegion.getStart(); |
| int subRegionEnd = regionStart + subRegion.getEnd(); |
| if (subRegionEnd < start) { |
| continue; |
| } |
| String type = subRegion.getType(); |
| |
| if (XML_TAG_OPEN.equals(type)) { |
| tagBalance++; |
| } else if (XML_END_TAG_OPEN.equals(type)) { |
| tagBalance--; |
| if (tagBalance == targetTagBalance) { |
| return subRegionStart; |
| } |
| } else if (XML_EMPTY_TAG_CLOSE.equals(type)) { |
| tagBalance--; |
| if (tagBalance == targetTagBalance) { |
| // We don't jump to matching tags within a self-closed tag |
| return -1; |
| } |
| } |
| } |
| |
| region = region.getNext(); |
| } |
| } |
| |
| return -1; |
| } |
| } |