/** * 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 other free and open source software ("FOSS") 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.js.context; import com.aptana.ide.editor.js.lexing.JSTokenTypes; import com.aptana.ide.editor.js.parsing.JSMimeType; import com.aptana.ide.lexer.Lexeme; import com.aptana.ide.lexer.LexemeList; import com.aptana.ide.lexer.TokenCategories; /** * @author Robin Debreuil */ public class JSLexemeUtils { private LexemeList lexemeList; private Lexeme currentLexeme; private int currentLexemeIndex; /** * Constructs a new File environment. There is one such object per file, and * this takes care of parsing it and keeping its environment up to date. * @param lexemeList */ public JSLexemeUtils(LexemeList lexemeList) { this.lexemeList = lexemeList; } /** * Get the current list of lexemes and synchronize the command tree. * Performance warning: this method will trigger a full parse if necessary, * if you do not need access to the parser's command nodes, then use * getLexemeListFast(). * * @return The lexeme list */ public LexemeList getLexemeList() { return lexemeList; } /** * Gets the cached current Lexeme based on the current offset of the * document. * * @return Returns the current lexeme. */ public Lexeme getCurrentLexeme() { return currentLexeme; } /** * Gets the cached current Lexeme index based on the offset in the current * document. * * @return Returns the current lexeme index. */ public int getCurrentLexemeIndex() { return currentLexemeIndex; } /** * Calculates and returns the Lexeme index at the current document offset. * Note that document offsets are one greater that lexeme offsets. * Use getCurrentLexemeInde if querying for the current caret offset. * * @param offset * The offset in the document to check at. * @return Returns the index of the Lexeme at the current offset. */ public int getLexemeIndexFromDocumentOffset(int offset) { if (offset < 0 || lexemeList.size() == 0) { return -1; } int index = lexemeList.getLexemeIndex(offset - 1); if (index < 1) // zero is a special case { if (index < -1) { index = -index - 2; } else { index = 0; } } return index; } /** * Gets the lexeme floor index from a given offset (lexeme offset, not document offset). * If this hits a whitespace character (which has no lexeme), it will return the previous (lower) lexeme. * This is a convenience method that directly calls getLexemeFloorIndex on lexemelist. * @param offset * @return Returns the lexeme floor index from a given lexeme offset. */ public int getLexemeFloorIndex(int offset) { return lexemeList.getLexemeFloorIndex(offset); } /** * Gets the lexeme ceiling index from a given offset (lexeme offset, not document offset). * If this hits a whitespace character (which has no lexeme), it will return the next (higher) lexeme. * This is a convenience method that directly calls getLexemeCeilingIndex on lexemelist. * @param offset * @return Returns the lexeme floor index from a given lexeme offset. */ public int getLexemeCeilingIndex(int offset) { return lexemeList.getLexemeCeilingIndex(offset); } /** * Calculates and returns the Lexeme at a document based offset. Use * getCurrentLexeme if querying for the current caret offset. * * @param offset * The offset in the document to check at. * @return Returns the Lexeme at the current offset. */ public Lexeme getLexemeFromDocumentOffset(int offset) { int index = getLexemeIndexFromDocumentOffset(offset); if (index > -1) { return lexemeList.get(index); } else { return null; } } /** * Calculates the index and lexeme that the given offset is within and * caches it. This accounts for whitespace areas by setting the result to * the previous lexeme if available. * * @param offset */ public void calculateCurrentLexeme(int offset) { // get rid of impossible offsets currentLexemeIndex = getLexemeIndexFromDocumentOffset(offset); if (currentLexemeIndex > -1) { currentLexeme = lexemeList.get(currentLexemeIndex); } else { currentLexeme = null; } } /** * Returns the next IDENTIFIER Lexeme from the starting 'index' position * * @param index * @return Returns the next Ident lexeme */ public Lexeme getNextIdentifier(int index) { for (int i = index; i < lexemeList.size(); i++) { Lexeme lexeme = lexemeList.get(i); if(lexeme.typeIndex != TokenCategories.WHITESPACE && lexeme.typeIndex != JSTokenTypes.IDENTIFIER) { return null; } if (lexeme.typeIndex == JSTokenTypes.IDENTIFIER) { return lexeme; } } return null; } /** * getPreviousIdentifier * * @param index * @return Lexeme */ public Lexeme getPreviousIdentifier(int index) { for (int i = index; i >= 0; i--) { Lexeme lexeme = lexemeList.get(i); if(lexeme.typeIndex != TokenCategories.WHITESPACE && lexeme.typeIndex != JSTokenTypes.IDENTIFIER) { return null; } if (lexeme.typeIndex == JSTokenTypes.IDENTIFIER) { return lexeme; } } return null; } /** * getNextTypeIdentifier * * @param startIndex * @return String */ public String getNextTypeIdentifier(int startIndex) { String name = ""; //$NON-NLS-1$ int index = startIndex; int size = lexemeList.size(); Lexeme lexeme = lexemeList.get(index); while(index < size && (lexeme.typeIndex == JSTokenTypes.IDENTIFIER || lexeme.typeIndex == JSTokenTypes.DOT)) { name += lexeme.getText(); index++; if(index < size) { lexeme = lexemeList.get(index); } } return name.equals("") ? null : name; //$NON-NLS-1$ } /** * Find 'foo' in a 'var a = new foo();' statement, but also account for comments. * @param index * @return Returns 'foo' in a 'var a = new foo();' statement, but also account for comments. */ public String getTypeAfterEqualNew(int index) { int equalsIndex = findNextTokenType(index, JSTokenTypes.EQUAL); if(equalsIndex == -1) { return null; } int newIndex = findNextTokenType(equalsIndex+1, JSTokenTypes.NEW); if(newIndex == -1) { return null; } int identifierIndex = findNextTokenType(equalsIndex+1, JSTokenTypes.IDENTIFIER); if(identifierIndex == -1) { return null; } return getNextTypeIdentifier(identifierIndex); } /** * getIndexAfterTypeIdentifier * * @param index * @return int */ public int getIndexAfterTypeIdentifier(int index) { // move to the next index index++; int equalsIndex = findNextTokenType(index, JSTokenTypes.EQUAL); if(equalsIndex == -1) { return -1; } int identifierIndex = findNextTokenType(equalsIndex+1, JSTokenTypes.IDENTIFIER); return identifierIndex; } /** * findNextTokenType * * @param startIndex * @param typeIndex * @return int */ public int findNextTokenType(int startIndex, int typeIndex) { int index = startIndex; int size = lexemeList.size(); if(index >= size) { return -1; } Lexeme lexeme = lexemeList.get(index); while(index < size || lexeme.getCategoryIndex() == TokenCategories.WHITESPACE) { if(lexeme.typeIndex == typeIndex) { return index; } index++; if(index < size) { lexeme = lexemeList.get(index); } else { break; } } return -1; } /** * isNextTokenType * * @param startIndex * @param typeIndex * @return int */ public int isNextTokenType(int startIndex, int typeIndex) { int index = startIndex; int size = lexemeList.size(); if(index >= size || index < 0) { return -1; } Lexeme lexeme = lexemeList.get(index); while(index < size && lexeme.getCategoryIndex() == TokenCategories.WHITESPACE) { index++; if(index < size) { lexeme = lexemeList.get(index); } else { break; } } if(lexeme.typeIndex == typeIndex) { return lexeme.offset; } else { return -1; } } /** * isPrevTokenType * * @param startIndex * @param typeIndex * @return int */ public int isPrevTokenType(int startIndex, int typeIndex) { int index = startIndex; int size = lexemeList.size(); if(index >= size || index < 0) { return -1; } Lexeme lexeme = lexemeList.get(index); while(index <= 0 && lexeme.getCategoryIndex() == TokenCategories.WHITESPACE) { index--; if(index >= 0) { lexeme = lexemeList.get(index); } else { break; } } if(lexeme.typeIndex == typeIndex) { return lexeme.offset; } else { return -1; } } /** * Looks for the name of the function in the following formats: * * TYPE 1: function foo() {} * TYPE 2: foo = function() {} * TYPE 3: bar.foo = function() {} * TYPE 4: foo = { "bar" : function() {}, bar2 : function() {}, ... } * TYPE 5: function foo() { bar=function() {} } * * @param currentIndex * The current index of the lexemes to look at. * @return Returns the name of the function */ public JSFunctionInfo getFunctionInfo(int currentIndex) { return getFunctionInfo(currentIndex, false); } private JSFunctionInfo getFunctionInfo(int currentIndex, boolean recurse) { for (int i = currentIndex; i < lexemeList.size(); i++) { Lexeme lexeme = lexemeList.get(i); // TYPE 1 if (lexeme.typeIndex == JSTokenTypes.IDENTIFIER) { String params = findParameters(i); String parent = null; if (!recurse) { parent = findParentFunction(i); } if (parent == null) { parent = ""; //$NON-NLS-1$ } else { parent = parent + "."; //$NON-NLS-1$ } JSFunctionInfo fi = new JSFunctionInfo(parent + lexeme.getText(), lexeme.offset, params); //fi.docOffset = findDocOffset(i-1); fi.nameOffset = lexeme.offset; return fi; } // TYPE 2,3,4 if (lexeme.typeIndex == JSTokenTypes.LPAREN) { String params = findParameters(i - 1); int listIdx = currentIndex - 1; for (; listIdx > 0; --listIdx) { Lexeme lx1 = lexemeList.get(listIdx); // TYPE 2 & 3, Find '=' if (lx1.typeIndex == JSTokenTypes.EQUAL) { String parent = null; if (!recurse) { parent = findParentFunction(listIdx); } JSFunctionInfo fi = findIdentifierBeforeEqual(listIdx); if (fi != null) { if (parent != null) { fi.name = parent + "." + fi.name; //$NON-NLS-1$ } fi.params = params; //fi.docOffset = findDocOffset(getLexemeIndexFromDocumentOffset(fi.offset + 1)); return fi; } else { return null; } } // TYPE 4: foo = { "bar" : function() {}, "bar2" : // function() {}, ... } else if (lx1.typeIndex == JSTokenTypes.COLON) { // Find the first identifier (going backwards) for (; listIdx > 0; --listIdx) { Lexeme lx2 = lexemeList.get(listIdx); if (lx2.typeIndex == JSTokenTypes.STRING || lx2.typeIndex == JSTokenTypes.IDENTIFIER) { String name = lx2.getText(); if (lx2.typeIndex == JSTokenTypes.STRING) { name = name.substring(1, name.length() - 1); } String parentName = null; if (!recurse) { parentName = findParentFunction(listIdx, 1); } if (parentName != null) { JSFunctionInfo fi = new JSFunctionInfo(parentName + "." + name, lx2.offset, params); //$NON-NLS-1$ //fi.docOffset = findDocOffset(getLexemeIndexFromDocumentOffset(lx2.offset)+1); fi.nameOffset = lx2.offset; return fi; } else { JSFunctionInfo fi = new JSFunctionInfo(name, lx2.offset, params); //fi.docOffset = findDocOffset(getLexemeIndexFromDocumentOffset(lx2.offset)+1); fi.nameOffset = lx2.offset; return fi; } } } } } } } return null; } /** * findDocOffset * * @param currentIndex * @return int */ public int findDocOffset(int currentIndex) { if(currentIndex > 0) { Lexeme lx = lexemeList.get(--currentIndex); if (lx.typeIndex == JSTokenTypes.DOCUMENTATION) { return lx.offset; } } return -1; } // public FunctionDocumentation getFunctionDocumentation(int index) // { // DocumentationManager dm = JSEnvironment.getDocumentationManager(); // String src = getLexemeFromDocumentOffset(index+1).getText(); // FunctionDocumentation doc = dm.parseJSFunctionDocumentation(src); // // return doc; // } /** * Finds the parameter names of a function through lexeme lookup. * * @param index * The index in the lexeme list to start looking from (should be * the open paren). * @return Finds the parameter names of a function through lexeme lookup. */ private String findParameters(int index) { // todo: maybe this can now query the environment. String params = ""; //$NON-NLS-1$ boolean found = false; for (int i = index; i < lexemeList.size(); i++) { Lexeme lexeme = lexemeList.get(i); if (lexeme.typeIndex == JSTokenTypes.LPAREN) { while (lexeme.typeIndex != JSTokenTypes.RPAREN && i < lexemeList.size()) { lexeme = lexemeList.get(i++); params += lexeme.getText().trim(); found = true; } if (found) { break; } } } if (!found || params.trim().length() == 0) { return "()"; //$NON-NLS-1$ } else { return params; } } private String findParentFunction(int currentIndex) { // todo: maybe this can now query the environment. return findParentFunction(currentIndex, 0); } private String findParentFunction(int currentIndex, int startDepth) { // todo: maybe this can now query the environment. int level = startDepth; // Find 'foo' in --> foo = { "bar" : function() {}, "bar2" : function() // {}, ... } while (currentIndex > 0) { Lexeme lx = lexemeList.get(--currentIndex); if (lx.getLanguage().equals(JSMimeType.MimeType) == false) { continue; } if (lx.typeIndex == JSTokenTypes.RCURLY) { level++; } else if (lx.typeIndex == JSTokenTypes.LCURLY) { level--; } if (level < startDepth) // level == 0) { // Find '=' for (; currentIndex > 0; --currentIndex) { Lexeme lx1 = lexemeList.get(currentIndex); if (lx.getLanguage().equals(JSMimeType.MimeType) == false) { continue; } if (lx1.typeIndex == JSTokenTypes.EQUAL) { String id = findIdentifierBeforeEqual(currentIndex).name; String parent = findParentFunction(currentIndex); if (parent != null) { return parent + "." + id; //$NON-NLS-1$ } else { return id; } } else if (lx1.typeIndex == JSTokenTypes.FUNCTION) { String name = getFunctionInfo(currentIndex, true).name; String parent = findParentFunction(currentIndex); if (parent != null) { return parent + "." + name; //$NON-NLS-1$ } else { return name; } } } } } return null; } /** * findIdentifierBeforeEqual * * @param currentIndex * @return JSFunctionInfo */ public JSFunctionInfo findIdentifierBeforeEqual(int currentIndex) { // todo: maybe this can now query the environment. // Find the first identifier (going backwards) while (currentIndex >= 0) { Lexeme lx2 = lexemeList.get(currentIndex--); if (lx2.typeIndex == JSTokenTypes.IDENTIFIER) { String name = ""; //$NON-NLS-1$ int offset = -1; while (lx2.typeIndex == JSTokenTypes.IDENTIFIER || lx2.typeIndex == JSTokenTypes.DOT || lx2.typeIndex == JSTokenTypes.THIS) { if (lx2.typeIndex == JSTokenTypes.THIS) { name = name.substring(1); // remove the dot } else { name = lx2.getText() + name; } offset = lx2.offset; int index = currentIndex--; if(lx2.isAfterEOL()) { break; } if (index >= 0) { lx2 = lexemeList.get(index); } else { break; } } JSFunctionInfo fi = new JSFunctionInfo(name, offset); fi.nameOffset = offset; return fi; } } return null; } // /** // * Gets a doc object from a scriptdoc lexeme. // * @param docLexeme // * @return Returns a doc object from a scriptdoc lexeme. // */ // public IDocumentation getDoumenatationFromLexeme(Lexeme docLexeme) // { // if( !(docLexeme.typeIndex == JSTokenTypes.DOCUMENTATION) ) // { // return null; // } // String docSrc = docLexeme.getText(); // IDocumentation doc = JSEnvironment.getDocumentationManager().parseScriptDoc(docSrc); // return doc; // } }