/** * This file Copyright (c) 2005-2008 Aptana, Inc. This program is * dual-licensed under both the Aptana Public License and the GNU General * Public license. You may elect to use one or the other of these licenses. * * This program is distributed in the hope that it will be useful, but * AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or * NONINFRINGEMENT. Redistribution, except as permitted by whichever of * the GPL or APL you select, is prohibited. * * 1. For the GPL license (GPL), you can redistribute and/or modify this * program under the terms of the GNU General Public License, * Version 3, as published by the Free Software Foundation. You should * have received a copy of the GNU General Public License, Version 3 along * with this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Aptana provides a special exception to allow redistribution of this file * with certain Eclipse Public Licensed code and certain additional terms * pursuant to Section 7 of the GPL. You may view the exception and these * terms on the web at http://www.aptana.com/legal/gpl/. * * 2. For the Aptana Public License (APL), this program and the * accompanying materials are made available under the terms of the APL * v1.0 which accompanies this distribution, and is available at * http://www.aptana.com/legal/apl/. * * You may view the GPL, Aptana's exception and additional terms, and the * APL in the file titled license.html at the root of the corresponding * plugin containing this source file. * * Any modifications to this file must keep this entire header intact. */ package com.aptana.ide.editor.html.formatting; import org.eclipse.jface.preference.IPreferenceStore; 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.source.ISourceViewer; import org.eclipse.jface.text.source.SourceViewerConfiguration; import com.aptana.ide.core.IdeLog; import com.aptana.ide.editor.html.HTMLPairFinder; import com.aptana.ide.editor.html.HTMLPlugin; import com.aptana.ide.editor.html.lexing.HTMLTokenTypes; import com.aptana.ide.editor.html.parsing.HTMLMimeType; import com.aptana.ide.editor.html.parsing.HTMLParseState; import com.aptana.ide.editor.html.parsing.HTMLUtils; import com.aptana.ide.editor.html.preferences.IPreferenceConstants; import com.aptana.ide.editors.unified.EditorFileContext; import com.aptana.ide.editors.unified.IPreferenceClient; import com.aptana.ide.editors.unified.PairMatch; import com.aptana.ide.lexer.Lexeme; import com.aptana.ide.lexer.LexemeList; import com.aptana.ide.parsing.IParseState; /** * Auto-edit strategy responsible for modifying the pair tag. * @author Denis Denisenko */ public class HTMLPairTagModifyStrategy implements IAutoEditStrategy, IPreferenceClient { /** * configuration */ protected SourceViewerConfiguration configuration; /** * sourceViewer */ protected ISourceViewer sourceViewer; /** * context */ protected EditorFileContext context; /** * HTMLPairTagModifyStrategy * * @param context - file context. * @param configuration - source viewer configuration. * @param sourceViewer - source viewer. */ public HTMLPairTagModifyStrategy(EditorFileContext context, SourceViewerConfiguration configuration, ISourceViewer sourceViewer) { this.context = context; this.configuration = configuration; this.sourceViewer = sourceViewer; } /** * {@inheritDoc} */ public void customizeDocumentCommand(IDocument document, DocumentCommand command) { IPreferenceStore store = HTMLPlugin.getDefault().getPreferenceStore(); IParseState parseState = context.getParseState(); HTMLParseState htmlParseState = (HTMLParseState) parseState.getParseState(HTMLMimeType.MimeType); //modifies pair tag modifyPairTag(document, command, htmlParseState, store); } /** * {@inheritDoc} */ public IPreferenceStore getPreferenceStore() { return HTMLPlugin.getDefault().getPreferenceStore(); } /** * Modifies pair tag if any. * @param document - document. * @param command - original document command. * @param parseState - parse state. * @param store - preference store. */ private void modifyPairTag(IDocument document, DocumentCommand command, IParseState parseState, IPreferenceStore store) { if (store == null || !store.getBoolean(IPreferenceConstants.AUTO_MODIFY_PAIR_TAG)) { return; } Lexeme cursorLexeme = getTagLexeme(parseState, command.offset); if (cursorLexeme != null) { HTMLPairFinder finder = new HTMLPairFinder(); PairMatch match = finder.findPairMatch(command.offset, parseState, cursorLexeme, 2); if (match != null) { Lexeme pairLexeme; if (match.endStart > command.offset) { pairLexeme = getTagLexeme(parseState, match.endStart); } else { pairLexeme = getTagLexeme(parseState, match.beginStart); } //if pair lexeme is not found, match is invalid if (pairLexeme == null) { return; } //checking the match if (!checkMatch(cursorLexeme, pairLexeme)) { return; } //checking if current change is full tag removal //if so we need to completely delete it's pair if(checkFullDelete(command, parseState.getLexemeList(), cursorLexeme, pairLexeme)) { return; } //checking if current change is going to destroy the tag. //such a change should not be propagated if(destroysTag(cursorLexeme, parseState.getLexemeList(), command.offset, command.length)) { return; } //filtering replace length int replaceLength = filterReplaceLength(command, cursorLexeme); if (replaceLength == -1) { return; } //filtering replace text String replaceText = filterReplaceText( command.text); if (replaceLength == 0 && replaceText.length() == 0) { return; } try { //saving caret position int caretPosition = command.offset + command.length; command.caretOffset = caretPosition; command.shiftsCaret = true; int offsetDif = command.offset - cursorLexeme.offset; //checking if current match is upper or lower if (match.endStart > command.offset) { //match is lower command.addCommand(match.endStart + offsetDif + 1, replaceLength, replaceText, command.owner); } else { //match is upper command.addCommand(match.beginStart + offsetDif - 1, replaceLength, replaceText, command.owner); } command.doit = false; } catch (BadLocationException e) { //can happen when user deletes both starting and closing tag simultaneously } } } } /** * Checks if current change is full tag removal and * modifies command to completely delete its pair if so. * @param command - document command. * @param lexemes - lexemes. * @param cursorLexeme - cursor lexeme. * @param pairLexeme - pair lexeme. * @return true if full removal, false otherwise. */ private boolean checkFullDelete(DocumentCommand command, LexemeList lexemes, Lexeme cursorLexeme, Lexeme pairLexeme) { Lexeme cursorEndingLexeme = getTagEndingLexeme(cursorLexeme, lexemes); Lexeme pairEndingLexeme = getTagEndingLexeme(pairLexeme, lexemes); if (cursorEndingLexeme == null || pairEndingLexeme == null) { return false; } if (cursorLexeme.getStartingOffset() == command.offset && cursorEndingLexeme.getEndingOffset() == command.offset + command.length) { //removing the whole tag try { //saving caret position int caretPosition = command.offset + command.length; command.caretOffset = caretPosition; command.shiftsCaret = true; command.addCommand(pairLexeme.getStartingOffset(), pairEndingLexeme.getEndingOffset() - pairLexeme.getStartingOffset(), "", null); //$NON-NLS-1$ command.doit = false; return true; } catch (BadLocationException e) { //should not happen IdeLog.logError(HTMLPlugin.getDefault(), Messages.HTMLPairTagModifyStrategy_ERR_TagRemoval, e); } } return false; } /** * Checks whether current change is going to destroy the tag. * @param startLexeme - tag start lexeme. * @param lexemes - lexemes. * @param offset - replace change offset. * @param length - replace change length. * @return true if destroys, false otherwise. */ private boolean destroysTag(Lexeme startLexeme, LexemeList lexemes, int offset, int length) { Lexeme closing = getTagEndingLexeme(startLexeme, lexemes); //nothing to destroy, if closing lexeme (">") is not found. if (closing == null) { return false; } //if the replace change end is after the closing lexeme start, tag would be destroyed return offset + length > closing.getStartingOffset(); } /** * Gest tag ending lexeme. * @param startLexeme - starting lexeme. * @param lexemes - lexemes. * @return tag ending lexeme, or null if not found. */ private Lexeme getTagEndingLexeme(Lexeme startLexeme, LexemeList lexemes) { int startIndex = lexemes.getLexemeIndex(startLexeme); if ((startLexeme.typeIndex != HTMLTokenTypes.START_TAG && startLexeme.typeIndex != HTMLTokenTypes.END_TAG) || startIndex < 0) { throw new IllegalArgumentException("Wrong start lexeme"); //$NON-NLS-1$ } Lexeme closing = HTMLUtils.getFirstLexemeBreaking(lexemes, startIndex + 1, new int[]{HTMLTokenTypes.GREATER_THAN}, new int[]{HTMLTokenTypes.START_TAG, HTMLTokenTypes.END_TAG, HTMLTokenTypes.START_TAG,} ); return closing; } /** * Checks if match is really correct. * * @param firstLexeme - first lexeme in the match. * @param secondLexeme - second lexeme in the match. * * @return true if match is correct, false otherwise */ private boolean checkMatch(Lexeme firstLexeme, Lexeme secondLexeme) { int firstTagStart = getTagNameStartOffset(firstLexeme); if (firstTagStart == -1) { return false; } int secondTagStart = getTagNameStartOffset(secondLexeme); if (secondTagStart == -1) { return false; } String firstName = firstLexeme.getText().substring(firstTagStart - firstLexeme.getStartingOffset()); String secondName = secondLexeme.getText().substring(secondTagStart - secondLexeme.getStartingOffset()); return firstName.equalsIgnoreCase(secondName); } /** * Filters replace length to only allow the part that replaces the tag lexeme. * * @param originalCommand - original command. * @param tagLexeme - tag lexeme. * * @return allowed replace length, or -1 if replace is denied. */ private int filterReplaceLength(DocumentCommand originalCommand, Lexeme tagLexeme) { //checking command replace start. //Replacing "<" and "</" symbols is not allowed. int tagNameStartOffset = getTagNameStartOffset(tagLexeme); if (originalCommand.offset < tagNameStartOffset) { return -1; } //starting the replace after the tag start node is also denied if (originalCommand.offset > tagLexeme.getEndingOffset()) { return -1; } //truncating replace length int maxLength = tagLexeme.getEndingOffset() - originalCommand.offset; int resultLength = Math.min(originalCommand.length, maxLength); return resultLength; } private String filterReplaceText(String originalText) { //taking the allowed symbols till meeting the denied one int pos = 0; for(; pos < originalText.length(); pos++) { int ch = originalText.charAt(pos); if (!Character.isLetterOrDigit(ch)) { break; } } return originalText.substring(0, pos); } /** * Get tag name start offset. * * @param tagLexeme - tag lexeme. * * @return name start offset. */ private int getTagNameStartOffset(Lexeme tagLexeme) { if (tagLexeme.typeIndex == HTMLTokenTypes.START_TAG) { return tagLexeme.getStartingOffset() + 1; } else if (tagLexeme.typeIndex == HTMLTokenTypes.END_TAG) { return tagLexeme.getStartingOffset() + 2; } else { throw new IllegalArgumentException("Tag starting lexeme is excpected"); //$NON-NLS-1$ } } /** * Gets tag start or tag end lexeme, if found * @param state - parse state. * @param offset - offset to search in, or before. * @return lexeme, or null if not found */ private Lexeme getTagLexeme(IParseState state, int offset) { LexemeList lexemeList = state.getLexemeList(); Lexeme currentLexeme = lexemeList.getFloorLexeme(offset); if (currentLexeme != null && (currentLexeme.typeIndex == HTMLTokenTypes.START_TAG || currentLexeme.typeIndex == HTMLTokenTypes.END_TAG)) { return currentLexeme; } currentLexeme = lexemeList.getCeilingLexeme(offset); if (currentLexeme == null) { return null; } //checking current lexeme if (currentLexeme.typeIndex == HTMLTokenTypes.START_TAG || currentLexeme.typeIndex == HTMLTokenTypes.END_TAG) { return currentLexeme; } int currentLexemeIndex = lexemeList.getLexemeIndex(currentLexeme); if (currentLexemeIndex <= 0) { return null; } Lexeme previousLexeme = lexemeList.get(currentLexemeIndex - 1); if ((previousLexeme.typeIndex == HTMLTokenTypes.START_TAG || previousLexeme.typeIndex == HTMLTokenTypes.END_TAG) && currentLexeme.typeIndex == HTMLTokenTypes.GREATER_THAN) { return previousLexeme; } return null; } }