aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlAutoEditStrategy.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlAutoEditStrategy.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlAutoEditStrategy.java460
1 files changed, 460 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlAutoEditStrategy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlAutoEditStrategy.java
new file mode 100644
index 000000000..8a078efc2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlAutoEditStrategy.java
@@ -0,0 +1,460 @@
+/*
+ * 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_CONTENT;
+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 com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
+import com.android.utils.Pair;
+
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.DocumentCommand;
+import org.eclipse.jface.text.IAutoEditStrategy;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.TextUtilities;
+import org.eclipse.ui.texteditor.ITextEditor;
+import org.eclipse.ui.texteditor.ITextEditorExtension3;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+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;
+
+/**
+ * Edit strategy for Android XML files. It attempts a number of edit
+ * enhancements:
+ * <ul>
+ * <li> Auto indentation. The default XML indentation scheme is to just copy the
+ * indentation of the previous line. This edit strategy improves on that situation
+ * by considering the tag and bracket balance on the current line and using it
+ * to determine whether the next line should be indented or use the same
+ * indentation as the parent, or even the indentation of an earlier line
+ * (when for example the current line closes an element which was started on an
+ * earlier line.)
+ * <li> Newline handling. In addition to indenting, it can also adjust the following text
+ * appropriately when a newline is inserted. For example, it will reformat
+ * the following (where | represents the caret position):
+ * <pre>
+ * {@code <item name="a">|</item>}
+ * </pre>
+ * into
+ * <pre>
+ * {@code <item name="a">}
+ * |
+ * {@code </item>}
+ * </pre>
+ * </ul>
+ * In the future we might consider other editing enhancements here as well, such as
+ * refining the comment handling, or reindenting when you type the / of a closing tag,
+ * or even making the bracket matcher more resilient.
+ */
+@SuppressWarnings("restriction") // XML model
+public class AndroidXmlAutoEditStrategy implements IAutoEditStrategy {
+
+ @Override
+ public void customizeDocumentCommand(IDocument document, DocumentCommand c) {
+ if (!isSmartInsertMode()) {
+ return;
+ }
+
+ if (!(document instanceof IStructuredDocument)) {
+ // This shouldn't happen unless this strategy is used on an invalid document
+ return;
+ }
+ IStructuredDocument doc = (IStructuredDocument) document;
+
+ // Handle newlines/indentation
+ if (c.length == 0 && c.text != null
+ && TextUtilities.endsWith(doc.getLegalLineDelimiters(), c.text) != -1) {
+
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ IStructuredModel model = modelManager.getModelForRead(doc);
+ if (model != null) {
+ try {
+ final int offset = c.offset;
+ int lineStart = findLineStart(doc, offset);
+ int textStart = findTextStart(doc, lineStart, offset);
+
+ IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(textStart);
+ if (region != null && region.getType().equals(XML_TAG_NAME)) {
+ Pair<Integer,Integer> balance = getBalance(doc, textStart, offset);
+ int tagBalance = balance.getFirst();
+ int bracketBalance = balance.getSecond();
+
+ String lineIndent = ""; //$NON-NLS-1$
+ if (textStart > lineStart) {
+ lineIndent = doc.get(lineStart, textStart - lineStart);
+ }
+
+ // We only care if tag or bracket balance is greater than 0;
+ // we never *dedent* on negative balances
+ boolean addIndent = false;
+ if (bracketBalance < 0) {
+ // Handle
+ // <foo
+ // ></foo>^
+ // and
+ // <foo
+ // />^
+ ITextRegion left = getRegionAt(doc, offset, true /*biasLeft*/);
+ if (left != null
+ && (left.getType().equals(XML_TAG_CLOSE)
+ || left.getType().equals(XML_EMPTY_TAG_CLOSE))) {
+
+ // Find the corresponding open tag...
+ // The org.eclipse.wst.xml.ui.gotoMatchingTag frequently
+ // doesn't work, it just says "No matching brace found"
+ // (or I would use that here).
+
+ int targetBalance = 0;
+ ITextRegion right = getRegionAt(doc, offset, false /*biasLeft*/);
+ if (right != null && right.getType().equals(XML_END_TAG_OPEN)) {
+ targetBalance = -1;
+ }
+ int openTag = AndroidXmlCharacterMatcher.findTagBackwards(doc,
+ offset, targetBalance);
+ if (openTag != -1) {
+ // Look up the indentation of the given line
+ lineIndent = AndroidXmlEditor.getIndentAtOffset(doc, openTag);
+ }
+ }
+ } else if (tagBalance > 0 || bracketBalance > 0) {
+ // Add indentation
+ addIndent = true;
+ }
+
+ StringBuilder sb = new StringBuilder(c.text);
+ sb.append(lineIndent);
+ String oneIndentUnit = EclipseXmlFormatPreferences.create().getOneIndentUnit();
+ if (addIndent) {
+ sb.append(oneIndentUnit);
+ }
+
+ // Handle
+ // <foo>^</foo>
+ // turning into
+ // <foo>
+ // ^
+ // </foo>
+ ITextRegion left = getRegionAt(doc, offset, true /*biasLeft*/);
+ ITextRegion right = getRegionAt(doc, offset, false /*biasLeft*/);
+ if (left != null && right != null
+ && left.getType().equals(XML_TAG_CLOSE)
+ && right.getType().equals(XML_END_TAG_OPEN)) {
+ // Move end tag
+ if (tagBalance > 0 && bracketBalance < 0) {
+ sb.append(oneIndentUnit);
+ }
+ c.caretOffset = offset + sb.length();
+ c.shiftsCaret = false;
+ sb.append(TextUtilities.getDefaultLineDelimiter(doc));
+ sb.append(lineIndent);
+ }
+ c.text = sb.toString();
+ } else if (region != null && region.getType().equals(XML_CONTENT)) {
+ // Indenting in text content. If you're in the middle of editing
+ // text, just copy the current line indentation.
+ // However, if you're editing in leading whitespace (e.g. you press
+ // newline on a blank line following say an element) then figure
+ // out the indentation as if the newline had been pressed at the
+ // end of the element, and insert that amount of indentation.
+ // In this case we need to also make sure to subtract any existing
+ // whitespace on the current line such that if we have
+ //
+ // <foo>
+ // ^ <bar/>
+ // </foo>
+ //
+ // you end up with
+ //
+ // <foo>
+ //
+ // ^<bar/>
+ // </foo>
+ //
+ String text = region.getText();
+ int regionStart = region.getStartOffset();
+ int delta = offset - regionStart;
+ boolean inWhitespacePrefix = true;
+ for (int i = 0, n = Math.min(delta, text.length()); i < n; i++) {
+ char ch = text.charAt(i);
+ if (!Character.isWhitespace(ch)) {
+ inWhitespacePrefix = false;
+ break;
+ }
+ }
+ if (inWhitespacePrefix) {
+ IStructuredDocumentRegion previous = region.getPrevious();
+ if (previous != null && previous.getType() == XML_TAG_NAME) {
+ ITextRegionList subRegions = previous.getRegions();
+ ITextRegion last = subRegions.get(subRegions.size() - 1);
+ if (last.getType() == XML_TAG_CLOSE ||
+ last.getType() == XML_EMPTY_TAG_CLOSE) {
+ // See if the last tag was a closing tag
+ boolean wasClose = last.getType() == XML_EMPTY_TAG_CLOSE;
+ if (!wasClose) {
+ // Search backwards to see if the XML_TAG_CLOSE
+ // is the end of an </endtag>
+ for (int i = subRegions.size() - 2; i >= 0; i--) {
+ ITextRegion current = subRegions.get(i);
+ String type = current.getType();
+ if (type != XML_TAG_NAME) {
+ wasClose = type == XML_END_TAG_OPEN;
+ break;
+ }
+ }
+ }
+
+ int begin = AndroidXmlCharacterMatcher.findTagBackwards(doc,
+ previous.getStartOffset() + last.getStart(), 0);
+ int prevLineStart = findLineStart(doc, begin);
+ int prevTextStart = findTextStart(doc, prevLineStart, begin);
+
+ String lineIndent = ""; //$NON-NLS-1$
+ if (prevTextStart > prevLineStart) {
+ lineIndent = doc.get(prevLineStart,
+ prevTextStart - prevLineStart);
+ }
+ StringBuilder sb = new StringBuilder(c.text);
+ sb.append(lineIndent);
+
+ // See if there is whitespace on the insert line that
+ // we should also remove
+ for (int i = delta, n = text.length(); i < n; i++) {
+ char ch = text.charAt(i);
+ if (ch == ' ') {
+ c.length++;
+ } else {
+ break;
+ }
+ }
+
+ boolean addIndent = (last.getType() == XML_TAG_CLOSE)
+ && !wasClose;
+
+ // Is there just whitespace left of this text tag
+ // until we reach an end tag?
+ boolean whitespaceToEndTag = true;
+ for (int i = delta; i < text.length(); i++) {
+ char ch = text.charAt(i);
+ if (ch == '\n' || !Character.isWhitespace(ch)) {
+ whitespaceToEndTag = false;
+ break;
+ }
+ }
+ if (whitespaceToEndTag) {
+ IStructuredDocumentRegion next = region.getNext();
+ if (next != null && next.getType() == XML_TAG_NAME) {
+ String nextType = next.getRegions().get(0).getType();
+ if (nextType == XML_END_TAG_OPEN) {
+ addIndent = false;
+ }
+ }
+ }
+
+ if (addIndent) {
+ sb.append(EclipseXmlFormatPreferences.create()
+ .getOneIndentUnit());
+ }
+ c.text = sb.toString();
+
+ return;
+ }
+ }
+ }
+ copyPreviousLineIndentation(doc, c);
+ } else {
+ copyPreviousLineIndentation(doc, c);
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the offset of the start of the line (which might be whitespace)
+ *
+ * @param document the document
+ * @param offset an offset for a character anywhere on the line
+ * @return the offset of the first character on the line
+ * @throws BadLocationException if the offset is invalid
+ */
+ public static int findLineStart(IDocument document, int offset) throws BadLocationException {
+ offset = Math.max(0, Math.min(offset, document.getLength() - 1));
+ IRegion info = document.getLineInformationOfOffset(offset);
+ return info.getOffset();
+ }
+
+ /**
+ * Finds the first non-whitespace character on the given line
+ *
+ * @param document the document to search
+ * @param lineStart the offset of the beginning of the line
+ * @param lineEnd the offset of the end of the line, or the maximum position on the
+ * line to search
+ * @return the offset of the first non whitespace character, or the maximum position,
+ * whichever is smallest
+ * @throws BadLocationException if the offsets are invalid
+ */
+ public static int findTextStart(IDocument document, int lineStart, int lineEnd)
+ throws BadLocationException {
+ for (int offset = lineStart; offset < lineEnd; offset++) {
+ char c = document.getChar(offset);
+ if (c != ' ' && c != '\t') {
+ return offset;
+ }
+ }
+
+ return lineEnd;
+ }
+
+ /**
+ * Indent the new line the same way as the current line.
+ *
+ * @param doc the document to indent in
+ * @param command the document command to customize
+ * @throws BadLocationException if the offsets are invalid
+ */
+ private void copyPreviousLineIndentation(IDocument doc, DocumentCommand command)
+ throws BadLocationException {
+
+ if (command.offset == -1 || doc.getLength() == 0) {
+ return;
+ }
+
+ int lineStart = findLineStart(doc, command.offset);
+ int textStart = findTextStart(doc, lineStart, command.offset);
+
+ StringBuilder sb = new StringBuilder(command.text);
+ if (textStart > lineStart) {
+ sb.append(doc.get(lineStart, textStart - lineStart));
+ }
+
+ command.text = sb.toString();
+ }
+
+
+ /**
+ * Returns the subregion at the given offset, with a bias to the left or a bias to the
+ * right. In other words, if | represents the caret position, in the XML
+ * {@code <foo>|</bar>} then the subregion with bias left is the closing {@code >} and
+ * the subregion with bias right is the opening {@code </}.
+ *
+ * @param doc the document
+ * @param offset the offset in the document
+ * @param biasLeft whether we should look at the token on the left or on the right
+ * @return the subregion at the given offset, or null if not found
+ */
+ private static ITextRegion getRegionAt(IStructuredDocument doc, int offset,
+ boolean biasLeft) {
+ if (biasLeft) {
+ offset--;
+ }
+ IStructuredDocumentRegion region =
+ doc.getRegionAtCharacterOffset(offset);
+ if (region != null) {
+ return region.getRegionAtCharacterOffset(offset);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a pair of (tag-balance,bracket-balance) for the range textStart to offset.
+ *
+ * @param doc the document
+ * @param start the offset of the starting character (inclusive)
+ * @param end the offset of the ending character (exclusive)
+ * @return the balance of tags and brackets
+ */
+ private static Pair<Integer, Integer> getBalance(IStructuredDocument doc,
+ int start, int end) {
+ // Balance of open and closing tags
+ // <foo></foo> has tagBalance = 0, <foo> has tagBalance = 1
+ int tagBalance = 0;
+ // Balance of open and closing brackets
+ // <foo attr1="value1"> has bracketBalance = 1, <foo has bracketBalance = 1
+ int bracketBalance = 0;
+ IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(start);
+
+ if (region != null) {
+ boolean inOpenTag = true;
+ while (region != null && region.getStartOffset() < end) {
+ 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 || subRegionStart >= end) {
+ continue;
+ }
+ String type = subRegion.getType();
+
+ if (XML_TAG_OPEN.equals(type)) {
+ bracketBalance++;
+ inOpenTag = true;
+ } else if (XML_TAG_CLOSE.equals(type)) {
+ bracketBalance--;
+ if (inOpenTag) {
+ tagBalance++;
+ } else {
+ tagBalance--;
+ }
+ } else if (XML_END_TAG_OPEN.equals(type)) {
+ bracketBalance++;
+ inOpenTag = false;
+ } else if (XML_EMPTY_TAG_CLOSE.equals(type)) {
+ bracketBalance--;
+ }
+ }
+
+ region = region.getNext();
+ }
+ }
+
+ return Pair.of(tagBalance, bracketBalance);
+ }
+
+ /**
+ * Determine if we're in smart insert mode (if so, don't do any edit magic)
+ *
+ * @return true if the editor is in smart mode (or if it's an unknown editor type)
+ */
+ private static boolean isSmartInsertMode() {
+ ITextEditor textEditor = AdtUtils.getActiveTextEditor();
+ if (textEditor instanceof ITextEditorExtension3) {
+ ITextEditorExtension3 editor = (ITextEditorExtension3) textEditor;
+ return editor.getInsertMode() == ITextEditorExtension3.SMART_INSERT;
+ }
+
+ return true;
+ }
+}