/******************************************************************************* * Copyright (c) 2010 Bruno Medeiros and other Contributors. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Bruno Medeiros - initial API and implementation *******************************************************************************/ package melnorme.lang.ide.core.text; import static melnorme.utilbox.core.Assert.AssertNamespace.assertFail; import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; /** * A scanner to parse block tokens, and determine the balance of open vs. close tokens. * The blocks are specified by pairs of characters (must be one char in length each). * The scanning is partition aware, it only parse partitions of a given type. * * The scanner is heuristic in that the block balance may not be 100% accurate according to * the underlying language sematics of the source being scanned. */ public class BlockHeuristicsScannner extends AbstractDocumentScanner { public static final class BlockTokenRule { public final char open; public final char close; public BlockTokenRule(char open, char close) { this.open = open; this.close = close; } } protected final BlockTokenRule[] blockRules; protected final BlockTokenRule[] blockRulesReversed; public BlockHeuristicsScannner(IDocument document, String partitioning, String contentType, BlockTokenRule... blockRules) { super(document, partitioning, contentType); this.blockRules = blockRules; blockRulesReversed = new BlockTokenRule[blockRules.length]; for (int i = 0; i < blockRules.length; i++) { BlockTokenRule blockRule = blockRules[i]; blockRulesReversed[i] = new BlockTokenRule(blockRule.close, blockRule.open); } } public char getClosingPeer(char openChar) { return getMatchingPeer(openChar, blockRules); } public char getOpeningPeer(char closeChar) { return getMatchingPeer(closeChar, blockRulesReversed); } public boolean isClosingBrace(char closeChar) { for (BlockTokenRule blockTokenRule : blockRules) { if(closeChar == blockTokenRule.close) { return true; } } return false; } public static char getMatchingPeer(char openChar, BlockTokenRule[] blockTokenRules) { for (int i = 0; i < blockTokenRules.length; i++) { BlockTokenRule blockRule = blockTokenRules[i]; if(blockRule.open == openChar){ return blockRule.close; } } throw assertFail(); } protected int getPriorityOfBlockToken(char blockToken) { for (int i = 0; i < blockRules.length; i++) { BlockTokenRule blockRule = blockRules[i]; if(blockRule.open == blockToken || blockRule.close == blockToken) { return i; } } throw assertFail(); } /*-------------------*/ public static class BlockBalanceResult { public int unbalancedOpens = 0; public int unbalancedCloses = 0; public int rightmostUnbalancedBlockCloseOffset = -1; public int rightmostUnbalancedBlockOpenOffset = -1; } /** Calculate the block balance in given range. */ public BlockBalanceResult calculateBlockBalances(int beginPos, int endPos) throws BadLocationException { // Calculate backwards setScanRange(endPos, beginPos); // Ideally we would fully parse the code to figure the delta. // But ATM we just estimate using number of blocks BlockBalanceResult result = new BlockBalanceResult(); while(readPreviousCharacter() != TOKEN_EOF) { for (int i = 0; i < blockRules.length; i++) { BlockTokenRule blockRule = blockRules[i]; if(token == blockRule.close) { int blockCloseOffset = getPosition(); // do a subscan int balance = scanToBlockPeer(i, prevTokenFn, blockRules); if(balance > 0) { // block start not found result.unbalancedCloses = balance; result.rightmostUnbalancedBlockCloseOffset = blockCloseOffset; return result; } break; } if(token == blockRule.open) { result.unbalancedOpens++; if(result.rightmostUnbalancedBlockOpenOffset == -1) { result.rightmostUnbalancedBlockOpenOffset = getPosition(); } break; } } } return result; } public abstract class FnTokenAdvance { public abstract int advanceToken() ; public abstract void revertToken() ; } public final FnTokenAdvance prevTokenFn = new FnTokenAdvance() { @Override public int advanceToken() { return readPreviousCharacter(); } @Override public void revertToken() { revertPreviousCharacter(); } }; public final FnTokenAdvance nextTokenFn = new FnTokenAdvance() { @Override public int advanceToken() { return readNextCharacter(); } @Override public void revertToken() { revertNextCharacter(); } }; public int scanToBlockStart(int blockCloseOffset) throws BadLocationException { char blockClose = document.getChar(blockCloseOffset); return scanToBlockStart(blockCloseOffset, blockClose); } public int scanToBlockStart(int blockCloseOffset, char blockClose) { setPosition(blockCloseOffset); posLimit = 0; return scanToBlockStartForChar(blockClose, prevTokenFn, blockRules); } public int scanToBlockEnd(int blockOpenOffset) throws BadLocationException { setScanRange(blockOpenOffset+1, document.getLength()); char blockOpen = document.getChar(blockOpenOffset); return scanToBlockEnd(blockOpen); } protected int scanToBlockEnd(char blockOpen) { return scanToBlockStartForChar(blockOpen, nextTokenFn, blockRulesReversed); } protected int scanToBlockStartForChar(char blockClose, FnTokenAdvance fnAdvance, BlockTokenRule[] blockTkRules) { int ix = getPriorityOfBlockToken(blockClose); return scanToBlockPeer(ix, fnAdvance, blockTkRules); } /** Scans in search of a block peer (open/close). * Stops on EOF, or when block peer is found (balance is 0) * @return 0 if block peer token was found (even if assumed by a syntax correction), * or a count of how many blocks were left open. */ protected int scanToBlockPeer(int expectedTokenIx, FnTokenAdvance fnAdvance, BlockTokenRule[] blockTkRules) { assertTrue(expectedTokenIx >= 0 && expectedTokenIx < blockTkRules.length); while(fnAdvance.advanceToken() != TOKEN_EOF) { for (int i = 0; i < blockTkRules.length; i++) { BlockTokenRule blockRule = blockTkRules[i]; if(token == blockRule.close) { int pendingBlocks = scanToBlockPeer(i, fnAdvance, blockTkRules); if(pendingBlocks > 0) { return pendingBlocks + 1; } break; } if(token == blockRule.open) { if(i == expectedTokenIx){ return 0; } else { // syntax error if(i < expectedTokenIx) { // Stronger rule takes precedence. // Assume syntax correction, as if blockRule[expectedTokenIx].open was found: fnAdvance.revertToken(); token = TOKEN_INVALID; return 0; } else { // ignore token } } break; } } } return 1; // Balance is 1 if we reached the end without finding peer } /** Finds the offset where starts the blocks whose end token is at given blockCloseOffset */ public int findBlockStart(int blockCloseOffset) throws BadLocationException { scanToBlockStart(blockCloseOffset); return getPosition(); } public int findBlockStart(int blockCloseOffset, char blockClose) { scanToBlockStart(blockCloseOffset, blockClose); return getPosition(); } public boolean shouldCloseBlock(int blockOpenOffset) { assertTrue(blockOpenOffset != -1); char primaryBlockOpen = source.charAt(blockOpenOffset); int primaryBlockPriority = getPriorityOfBlockToken(primaryBlockOpen); char blockOpen = primaryBlockOpen; int leftOffset = blockOpenOffset; int rightOffset = blockOpenOffset+1; while(true) { assertTrue(getPriorityOfBlockToken(blockOpen) == primaryBlockPriority); setScanRange(rightOffset, document.getLength()); int balance = scanToBlockEnd(blockOpen); if(balance == 0 && token == TOKEN_INVALID) { return true; // a block close is necessary } if(balance > 0) { return true; } // Otherwise look for unmatched block opens on left, that are at least as important at the primary block rightOffset = getPosition(); // save value for later iterations setScanRange(leftOffset, 0); int balanceToTheLeft = findUnmatchedOpen(primaryBlockPriority); leftOffset = getPosition(); // save value for later iterations if(balanceToTheLeft <= 0) { return false; // relevant balance is zero or less, should not close } else { // Got an unmatched open blockOpen = (char) token; if(getPriorityOfBlockToken(blockOpen) < primaryBlockPriority) { // opening is from syntax-dominant block, doesn't matter the rest of the balance return false; } continue; } } } protected int findUnmatchedOpen(int requiredPriority) { while(prevTokenFn.advanceToken() != TOKEN_EOF) { for (int i = 0; i < blockRules.length; i++) { BlockTokenRule blockRule = blockRules[i]; if(token == blockRule.close) { // do a subscan if(scanToBlockPeer(i, prevTokenFn, blockRules) > 0) { // block start not found, so there is an unmatched close return -1; } break; } if(token == blockRule.open && (getPriorityOfBlockToken((char) token) <= requiredPriority)) { return 1; } } } return 0; } }