/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is NetBeans. The Initial Developer of the Original * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun * Microsystems, Inc. All Rights Reserved. */ package org.netbeans.editor.ext; import org.netbeans.editor.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.BadLocationException; import javax.swing.text.JTextComponent; import java.util.HashMap; import java.util.Map; /** * Support methods for syntax analyzes * * @author Miloslav Metelka * @version 1.00 */ public class ExtSyntaxSupport extends SyntaxSupport { // used in ExtKit /** Schedule content update making completion visible. */ public static final int COMPLETION_POPUP = 0; /** Cancel request without changing completion visibility. */ public static final int COMPLETION_CANCEL = 1; /** Update content immediatelly if it's currently visible. */ public static final int COMPLETION_REFRESH = 2; /** Schedule content update if it's currently visible. */ public static final int COMPLETION_POST_REFRESH = 3; /** Hide completion. */ public static final int COMPLETION_HIDE = 4; private static final TokenID[] EMPTY_TOKEN_ID_ARRAY = new TokenID[0]; /** Listens for the changes on the document. Children can override * the documentModified() method to perform some processing. */ private DocumentListener docL; /** Map holding the [position, local-variable-map] pairs */ private HashMap localVarMaps = new HashMap(); /** Map holding the [position, global-variable-map] pairs */ private HashMap globalVarMaps = new HashMap(); public ExtSyntaxSupport(BaseDocument doc) { super(doc); // Create listener to listen on document changes docL = new DocumentListener() { public void insertUpdate(DocumentEvent evt) { documentModified(evt); } public void removeUpdate(DocumentEvent evt) { documentModified(evt); } public void changedUpdate(DocumentEvent evt) { } }; getDocument().addDocumentListener(docL); } /** Get the chain of the tokens for the given block of text. * The returned chain of token-items reflects the tokens * as they occur in the text and therefore the first token * can start at the slightly lower position than the requested one. * The chain itself can be extended automatically when * reaching the first chain item and calling <tt>getPrevious()</tt> * on it. Another chunk of the tokens will be parsed and * the head of the chain will be extended. However this happens * only in case there was no modification performed to the document * between the creation of the chain and this moment. Otherwise * this call throws <tt>IllegalStateException</tt>. * * @param startOffset starting position of the block * @param endOffset ending position of the block * @return the first item of the token-item chain or null if there are * no tokens in the given area or the area is so small that it lays * inside one token. To prevent this provide the area that spans a new-line. */ public TokenItem getTokenChain(int startOffset, int endOffset) throws BadLocationException { TokenItem chain = null; BaseDocument doc = getDocument(); doc.readLock(); try { int docLen = doc.getLength(); if( startOffset < docLen ) { TokenItemTP tp = new TokenItemTP(); tp.targetOffset = endOffset; tokenizeText(tp, startOffset, endOffset, false); chain = tp.getTokenChain(); } } finally { doc.readUnlock(); } return chain; } /** Called when the document was modified by either the insert or removal. * @param evt event received with the modification notification. getType() * can be used to obtain the type of the event. */ protected void documentModified(DocumentEvent evt) { // Invalidate variable maps localVarMaps.clear(); globalVarMaps.clear(); } /** Get the bracket finder that will search for the matching bracket * or null if the bracket character doesn't belong to bracket * characters. */ protected BracketFinder getMatchingBracketFinder(char bracketChar) { BracketFinder bf = new BracketFinder(bracketChar); if (bf.moveCount == 0) { // not valid bracket char bf = null; } return bf; } /** Find matching bracket or more generally block * that matches with the current position. * @param offset position of the starting bracket * @param simple whether the search should skip comment and possibly other areas. * This can be useful when the speed is critical, because the simple * search is faster. * @return array of integers containing starting and ending position * of the block in the document. Null is returned if there's * no matching block. */ public int[] findMatchingBlock(int offset, boolean simpleSearch) throws BadLocationException { char bracketChar = getDocument().getChars(offset, 1)[0]; int foundPos = -1; final BracketFinder bf = getMatchingBracketFinder(bracketChar); if (bf != null) { // valid finder if (!simpleSearch) { TokenID tokenID = getTokenID(offset); TokenID[] bst = getBracketSkipTokens(); for (int i = bst.length - 1; i >= 0; i--) { if (tokenID == bst[i]) { simpleSearch = true; // turn to simple search break; } } } if (simpleSearch) { // don't exclude comments etc. if (bf.isForward()) { foundPos = getDocument().find(bf, offset, -1); } else { foundPos = getDocument().find(bf, offset + 1, 0); } } else { // exclude comments etc. from the search TextBatchProcessor tbp = new TextBatchProcessor() { public int processTextBatch(BaseDocument doc, int startPos, int endPos, boolean lastBatch) { try { int[] blks = getTokenBlocks(startPos, endPos, getBracketSkipTokens()); return findOutsideBlocks(bf, startPos, endPos, blks); } catch (BadLocationException e) { return -1; } } }; if (bf.isForward()) { foundPos = getDocument().processText(tbp, offset, -1); } else { foundPos = getDocument().processText(tbp, offset + 1, 0); } } } return (foundPos != -1) ? new int[] { foundPos, foundPos + 1 } : null; } /** Get the array of token IDs that should be skipped when * searching for matching bracket. It usually includes comments * and character and string constants. Returns empty array by default. */ protected TokenID[] getBracketSkipTokens() { return EMPTY_TOKEN_ID_ARRAY; } /** Gets the token-id of the token at the given position. * @param offset position at which the token should be returned * @return token-id of the token at the requested position. If there's no more * tokens in the text, the <tt>Syntax.INVALID</tt> is returned. */ public TokenID getTokenID(int offset) throws BadLocationException { FirstTokenTP fttp = new FirstTokenTP(); tokenizeText(fttp, offset, getDocument().getLength(), true); return fttp.getTokenID(); } /** Is the identifier at the position a function call? * It first checks whether there is a identifier under * the cursor and then it searches for the function call * character - usually '('. * @param identifierBlock int[2] block delimiting the identifier * @return int[2] block or null if there's no function call */ public int[] getFunctionBlock(int[] identifierBlock) throws BadLocationException { if (identifierBlock != null) { int nwPos = Utilities.getFirstNonWhiteFwd(getDocument(), identifierBlock[1]); if ((nwPos >= 0) && (getDocument().getChars(nwPos, 1)[0] == '(')) { return new int[] { identifierBlock[0], nwPos + 1 }; } } return null; } public int[] getFunctionBlock(int offset) throws BadLocationException { return getFunctionBlock(Utilities.getIdentifierBlock(getDocument(), offset)); } public boolean isWhitespaceToken(TokenID tokenID, char[] buffer, int offset, int tokenLength) { return Analyzer.isWhitespace(buffer, offset, tokenLength); } public boolean isCommentOrWhitespace(int startPos, int endPos) throws BadLocationException { CommentOrWhitespaceTP tp= new CommentOrWhitespaceTP(getCommentTokens()); tokenizeText(tp, startPos, endPos, true); return !tp.nonEmpty; } /** Gets the last non-blank and non-comment character on the given line. */ public int getRowLastValidChar(int offset) throws BadLocationException { return Utilities.getRowLastNonWhite(getDocument(), offset); } /** Does the line contain some valid code besides of possible white space * and comments? */ public boolean isRowValid(int offset) throws BadLocationException { return Utilities.isRowWhite(getDocument(), offset); } /** Get the array of token IDs that denote the comments. * Returns empty array by default. */ public TokenID[] getCommentTokens() { return EMPTY_TOKEN_ID_ARRAY; } /** Get the blocks consisting of comments in a specified document area. * @param doc document to work with * @param startPos starting position of the searched document area * @param endPos ending position of the searched document area */ public int[] getCommentBlocks(int startPos, int endPos) throws BadLocationException { return getTokenBlocks(startPos, endPos, getCommentTokens()); } /** Find the type of the variable. The default behavior is to first * search for the local variable declaration and then possibly for * the global declaration and if the declaration position is found * to get the first word on that position. * @return it returns Object to enable the custom implementations * to return the appropriate instances. */ public Object findType(String varName, int varPos) { Object type = null; Map varMap = getLocalVariableMap(varPos); // first try local vars if (varMap != null) { type = varMap.get(varName); } if (type == null) { varMap = getGlobalVariableMap(varPos); // try global vars if (varMap != null) { type = varMap.get(varName); } } return type; } public Map getLocalVariableMap(int offset) { Integer posI = new Integer(offset); Map varMap = (Map)localVarMaps.get(posI); if (varMap == null) { varMap = buildLocalVariableMap(offset); localVarMaps.put(posI, varMap); } return varMap; } protected Map buildLocalVariableMap(int offset) { int methodStartPos = getMethodStartPosition(offset); if (methodStartPos >= 0 && methodStartPos < offset) { VariableMapTokenProcessor vmtp = createVariableMapTokenProcessor(methodStartPos, offset); try { tokenizeText(vmtp, methodStartPos, offset, true); return vmtp.getVariableMap(); } catch (BadLocationException e) { // will default null } } return null; } public Map getGlobalVariableMap(int offset) { Integer posI = new Integer(offset); Map varMap = (Map)globalVarMaps.get(posI); if (varMap == null) { varMap = buildGlobalVariableMap(offset); globalVarMaps.put(posI, varMap); } return varMap; } protected Map buildGlobalVariableMap(int offset) { int docLen = getDocument().getLength(); VariableMapTokenProcessor vmtp = createVariableMapTokenProcessor(0, docLen); if (vmtp != null) { try { tokenizeText(vmtp, 0, docLen, true); return vmtp.getVariableMap(); } catch (BadLocationException e) { // will default null } } return null; } /** Get the start position of the method or the area * where the declaration can start. */ protected int getMethodStartPosition(int offset) { return 0; // return begining of the document by default } /** Find either the local or global declaration position. First * try the local declaration and if it doesn't succeed, then * try the global declaration. */ public int findDeclarationPosition(String varName, int varPos) { int offset = findLocalDeclarationPosition(varName, varPos); if (offset < 0) { offset = findGlobalDeclarationPosition(varName, varPos); } return offset; } public int findLocalDeclarationPosition(String varName, int varPos) { int methodStartPos = getMethodStartPosition(varPos); if (methodStartPos >= 0 && methodStartPos < varPos) { return findDeclarationPositionImpl(varName, methodStartPos, varPos); } return -1; } /** Get the position of the global declaration of a given variable. * By default it's implemented to use the same token processor as for the local * variables but the whole file is searched. */ public int findGlobalDeclarationPosition(String varName, int varPos) { return findDeclarationPositionImpl(varName, 0, getDocument().getLength()); } private int findDeclarationPositionImpl(String varName, int startPos, int endPos) { DeclarationTokenProcessor dtp = createDeclarationTokenProcessor(varName, startPos, endPos); if (dtp != null) { try { tokenizeText(dtp, startPos, endPos, true); return dtp.getDeclarationPosition(); } catch (BadLocationException e) { // will default to -1 } } return -1; } protected DeclarationTokenProcessor createDeclarationTokenProcessor( String varName, int startPos, int endPos) { return null; } protected VariableMapTokenProcessor createVariableMapTokenProcessor( int startPos, int endPos) { return null; } /** Check and possibly popup, hide or refresh the completion */ public int checkCompletion(JTextComponent target, String typedText, boolean visible ) { return visible ? COMPLETION_HIDE : COMPLETION_CANCEL; } /** Check if sources for code completion are already available */ public boolean isPrepared(){ return true; } /** Token processor extended to get declaration position * of the given variable. */ public interface DeclarationTokenProcessor extends TokenProcessor { /** Get the declaration position. */ public int getDeclarationPosition(); } public interface VariableMapTokenProcessor extends TokenProcessor { /** Get the map that contains the pairs [variable-name, variable-type]. */ public Map getVariableMap(); } /** Finder for the matching bracket. It gets the original bracket char * and searches for the appropriate matching bracket character. */ public class BracketFinder extends FinderFactory.GenericFinder { /** Original bracket char */ protected char bracketChar; /** Matching bracket char */ protected char matchChar; /** Depth of original brackets */ private int depth; /** Will it be a forward finder +1 or backward finder -1 or 0 when * the given character is not bracket character. */ protected int moveCount; /** * @param bracketChar bracket char */ public BracketFinder(char bracketChar) { this.bracketChar = bracketChar; updateStatus(); forward = (moveCount > 0); } /** Check whether the bracketChar really contains * the bracket character. If so assign the matchChar * and moveCount variables. */ protected boolean updateStatus() { boolean valid = true; switch (bracketChar) { case '(': matchChar = ')'; moveCount = +1; break; case ')': matchChar = '('; moveCount = -1; break; case '{': matchChar = '}'; moveCount = +1; break; case '}': matchChar = '{'; moveCount = -1; break; case '[': matchChar = ']'; moveCount = +1; break; case ']': matchChar = '['; moveCount = -1; break; default: valid = false; } return valid; } protected int scan(char ch, boolean lastChar) { if (ch == bracketChar) { depth++; } else if (ch == matchChar) { if (--depth == 0) { found = true; return 0; } } return moveCount; } } /** Create token-items */ final class TokenItemTP implements TokenProcessor { private Item firstItem; private Item lastItem; private DocumentListener docL; private boolean docModified; private int fwdBatchLineCnt; private int bwdBatchLineCnt; private char[] buffer; private int bufferStartPos; /** Target position corresponding to the begining of the token * that is already chained if searching for backward tokens, * or, the last token that should be scanned if searching * in forward direction. */ int targetOffset; TokenItemTP() { fwdBatchLineCnt = bwdBatchLineCnt = ((Integer)getDocument().getProperty( SettingsNames.LINE_BATCH_SIZE)).intValue(); // Start listening on document changes docL = new DocumentListener() { public void insertUpdate(DocumentEvent evt) { if (lastItem != null && evt.getOffset() < lastItem.getOffset() + lastItem.getImage().length() ) { docModified = true; getDocument().removeDocumentListener(this); docL = null; } } public void removeUpdate(DocumentEvent evt) { if (lastItem != null && evt.getOffset() < lastItem.getOffset() + lastItem.getImage().length() ) { docModified = true; getDocument().removeDocumentListener(this); docL = null; } } public void changedUpdate(DocumentEvent evt) { } }; } protected void finalize() throws Throwable { if (docL != null) { getDocument().removeDocumentListener(docL); docL = null; } super.finalize(); } public TokenItem getTokenChain() { return firstItem; } public boolean token(TokenID tokenID, TokenContextPath tokenContextPath, int tokenBufferOffset, int tokenLength) { if (bufferStartPos + tokenBufferOffset >= targetOffset) { // stop scanning return false; } lastItem = new Item(tokenID, tokenContextPath, bufferStartPos + tokenBufferOffset, new String(buffer, tokenBufferOffset, tokenLength), lastItem ); if (firstItem == null) { // not yet assigned firstItem = lastItem; } return true; } public int eot(int offset) { return ((Integer)getDocument().getProperty(SettingsNames.MARK_DISTANCE)).intValue(); } public void nextBuffer(char[] buffer, int offset, int len, int startPos, int preScan, boolean lastBuffer) { this.buffer = buffer; bufferStartPos = startPos - offset; } Item getNextChunk(Item i) { if (docModified) { throw new IllegalStateException(); } BaseDocument doc = getDocument(); int itemEndPos = i.getOffset() + i.getImage().length(); int docLen = doc.getLength(); if (itemEndPos == docLen) { return null; } int endPos; try { endPos = Utilities.getRowStart(doc, itemEndPos, fwdBatchLineCnt); } catch (BadLocationException e) { return null; } if (endPos == -1) { // past end of doc endPos = docLen; } fwdBatchLineCnt *= 2; // larger batch in next call Item nextChunkHead = null; Item fit = firstItem; Item lit = lastItem; try { // Simulate initial conditions firstItem = null; lastItem = null; targetOffset = endPos; tokenizeText(this, itemEndPos, endPos, false); nextChunkHead = firstItem; } catch (BadLocationException e) { } finally { // Link previous last with the current first if (firstItem != null) { lit.next = firstItem; firstItem.previous = lit; } firstItem = fit; if (lastItem == null) { // restore in case of no token or crash lastItem = lit; } } return nextChunkHead; } Item getPreviousChunk(Item i) { if (docModified) { throw new IllegalStateException(); } BaseDocument doc = getDocument(); int itemStartPos = i.getOffset(); if (itemStartPos == 0) { return null; } int startPos; try { startPos = Utilities.getRowStart(doc, itemStartPos, -bwdBatchLineCnt); } catch (BadLocationException e) { return null; } if (startPos == -1) { // before begining of doc startPos = 0; } bwdBatchLineCnt *= 2; Item previousChunkLast = null; Item fit = firstItem; Item lit = lastItem; try { // Simulate initial conditions firstItem = null; lastItem = null; targetOffset = itemStartPos; tokenizeText(this, startPos, itemStartPos, false); previousChunkLast = lastItem; } catch (BadLocationException e) { } finally { // Link previous last if (lastItem != null) { fit.previous = lastItem; lastItem.next = fit; } lastItem = lit; if (firstItem == null) { // restore in case of no token or crash firstItem = fit; } } return previousChunkLast; } final class Item extends TokenItem.AbstractItem { Item previous; TokenItem next; Item(TokenID tokenID, TokenContextPath tokenContextPath, int offset, String image, Item previous) { super(tokenID, tokenContextPath, offset, image); if (previous != null) { this.previous = previous; previous.next = this; } } public TokenItem getNext() { if (next == null) { next = getNextChunk(this); } return next; } public TokenItem getPrevious() { if (previous == null) { previous = getPreviousChunk(this); } return previous; } } } /** Token processor that matches either the comments or whitespace */ class CommentOrWhitespaceTP implements TokenProcessor { private char[] buffer; private TokenID[] commentTokens; boolean nonEmpty; CommentOrWhitespaceTP(TokenID[] commentTokens) { this.commentTokens = commentTokens; } public boolean token(TokenID tokenID, TokenContextPath tokenContextPath, int offset, int tokenLength) { for (int i = 0; i < commentTokens.length; i++) { if (tokenID == commentTokens[i]) { return true; // comment token found } } boolean nonWS = isWhitespaceToken(tokenID, buffer, offset, tokenLength); if (nonWS) { nonEmpty = true; } return nonWS; } public int eot(int offset) { return 0; } public void nextBuffer(char[] buffer, int offset, int len, int startPos, int preScan, boolean lastBuffer) { this.buffer = buffer; } } class FirstTokenTP implements TokenProcessor { private TokenID tokenID; public TokenID getTokenID() { return tokenID; } public boolean token(TokenID tokenID, TokenContextPath tokenContextPath, int offset, int tokenLen) { this.tokenID = tokenID; return false; // no more tokens } public int eot(int offset) { return 0; } public void nextBuffer(char[] buffer, int offset, int len, int startPos, int preScan, boolean lastBuffer) { } } }