Home | History | Annotate | Download | only in editors
      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      */
     42     public AndroidXmlCharacterMatcher() {
     43     }
     44 
     45     @Override
     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 
     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      */
    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      */
    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