// this file is part of Notepad++ // Copyright (C)2008 Harry Bruin // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either // version 2 of the License, or (at your option) any later version. // // Note that the GPL places important restrictions on "derived works", yet // it does not provide a detailed definition of that term. To avoid // misunderstandings, we consider an application to constitute a // "derivative work" for the purpose of this license if it does any of the // following: // 1. Integrates source code from Notepad++. // 2. Integrates/includes/aggregates Notepad++ into a proprietary executable // installer, such as those produced by InstallShield. // 3. Links to a library or executes a program that does any of the above. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. #include "FunctionCallTip.h" struct Token { TCHAR * token; int length; bool isIdentifier; Token(TCHAR * tok, int len, bool isID) : token(tok), length(len), isIdentifier(isID) {}; }; struct FunctionValues { int lastIdentifier; int lastFunctionIdentifier; int param; int scopeLevel; FunctionValues() : lastIdentifier(-1), lastFunctionIdentifier(-1), param(0), scopeLevel(-1) {}; }; inline bool lower(TCHAR c) { return (c >= 'a' && c <= 'z'); } inline bool match(TCHAR c1, TCHAR c2) { if (c1 == c2) return true; if (lower(c1)) return ((c1-32) == c2); if (lower(c2)) return ((c2-32) == c1); return false; } //test string case insensitive ala Scintilla //0 if equal, <0 of before, >0 if after (name1 that is) int testNameNoCase(const TCHAR * name1, const TCHAR * name2, int len = -1) { if (len == -1) { len = 1024; //magic value, but it probably fails way before it reaches this } int i = 0; while(match(name1[i], name2[i])) { if (name1[i] == 0 || i == len) { return 0; //equal } ++i; } int subs1 = lower(name1[i])?32:0; int subs2 = lower(name2[i])?32:0; return ( (name1[i]-subs1) - (name2[i]-subs2) ); } void FunctionCallTip::setLanguageXML(TiXmlElement * pXmlKeyword) { if (isVisible()) close(); _pXmlKeyword = pXmlKeyword; // Clear all buffered values, because they may point to freed memory area. reset(); // Also clear _funcName so that next getCursorFunction will call loadFunction to parse XML structure if (_funcName) delete [] _funcName; _funcName = 0; } bool FunctionCallTip::updateCalltip(int ch, bool needShown) { if (not needShown && ch != _start && ch != _param && not isVisible()) //must be already visible return false; _curPos = static_cast(_pEditView->execute(SCI_GETCURRENTPOS)); //recalculate everything if (not getCursorFunction()) { //cannot display calltip (anymore) close(); return false; } showCalltip(); return true; } void FunctionCallTip::showNextOverload() { if (!isVisible()) return; _currentOverload = (_currentOverload+1) % _currentNbOverloads; showCalltip(); } void FunctionCallTip::showPrevOverload() { if (!isVisible()) return; _currentOverload = _currentOverload > 0 ? (_currentOverload-1) : (_currentNbOverloads-1); showCalltip(); } void FunctionCallTip::close() { if (!isVisible() || !_selfActivated) return; _pEditView->execute(SCI_CALLTIPCANCEL); _selfActivated = false; _currentOverload = 0; } bool FunctionCallTip::getCursorFunction() { auto line = _pEditView->execute(SCI_LINEFROMPOSITION, _curPos); int startpos = static_cast(_pEditView->execute(SCI_POSITIONFROMLINE, line)); int endpos = static_cast(_pEditView->execute(SCI_GETLINEENDPOSITION, line)); int len = endpos - startpos + 3; //also take CRLF in account, even if not there int offset = _curPos - startpos; //offset is cursor location, only stuff before cursor has influence const int maxLen = 256; if ((offset < 2) || (len >= maxLen)) { reset(); return false; //cannot be a func, need name and separator } TCHAR lineData[maxLen] = TEXT(""); _pEditView->getLine(line, lineData, len); //line aquired, find the functionname //first split line into tokens to parse //token is identifier or some expression, whitespace is ignored std::vector< Token > tokenVector; int tokenLen = 0; TCHAR ch; for (int i = 0; i < offset; ++i) //we dont care about stuff after the offset { //tokenVector.push_back(pair(lineData+i, len)); ch = lineData[i]; if (isBasicWordChar(ch) || isAdditionalWordChar(ch)) //part of identifier { tokenLen = 0; TCHAR * begin = lineData+i; while ((isBasicWordChar(ch) || isAdditionalWordChar(ch)) && i < offset) { ++tokenLen; ++i; ch = lineData[i]; } tokenVector.push_back(Token(begin, tokenLen, true)); i--; //correct overshooting of while loop } else { if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') //whitespace { //do nothing } else { tokenLen = 1; tokenVector.push_back(Token(lineData+i, tokenLen, false)); } } } size_t vsize = tokenVector.size(); //mind nested funcs, like |blblb a (x, b(), c);| //therefore, use stack std::vector valueVec; FunctionValues curValue, newValue; int scopeLevel = 0; for (size_t i = 0; i < vsize; ++i) { Token & curToken = tokenVector.at(i); if (curToken.isIdentifier) { curValue.lastIdentifier = static_cast(i); } else { if (curToken.token[0] == _start) { ++scopeLevel; newValue = curValue; valueVec.push_back(newValue); //store the current settings, so when this new function doesnt happen to be the 'real' one, we can restore everything curValue.scopeLevel = scopeLevel; if (i > 0 && curValue.lastIdentifier == static_cast(i) - 1) { //identifier must be right before (, else we have some expression like "( x + y() )" curValue.lastFunctionIdentifier = curValue.lastIdentifier; curValue.param = 0; } else { //some expression curValue.lastFunctionIdentifier = -1; } } else if (curToken.token[0] == _param && curValue.lastFunctionIdentifier > -1) { ++curValue.param; } else if (curToken.token[0] == _stop) { if (scopeLevel) //scope cannot go below -1 scopeLevel--; if (valueVec.size() > 0) { //only pop level if scope was of actual function curValue = valueVec.back(); valueVec.pop_back(); } else { //invalidate curValue curValue = FunctionValues(); } } else if (curToken.token[0] == _terminal) { //invalidate everything valueVec.clear(); curValue = FunctionValues(); } } } bool res = false; if (curValue.lastFunctionIdentifier == -1) { //not in direct function. Start popping the stack untill we empty it, or a func IS found while(curValue.lastFunctionIdentifier == -1 && valueVec.size() > 0) { curValue = valueVec.back(); valueVec.pop_back(); } } if (curValue.lastFunctionIdentifier > -1) { Token funcToken = tokenVector.at(curValue.lastFunctionIdentifier); funcToken.token[funcToken.length] = 0; _currentParam = curValue.param; bool same = false; if (_funcName) { if(_ignoreCase) same = testNameNoCase(_funcName, funcToken.token, lstrlen(_funcName)) == 0; else same = generic_strncmp(_funcName, funcToken.token, lstrlen(_funcName)) == 0; } if (!same) { //check if we need to reload data if (_funcName) { delete [] _funcName; } _funcName = new TCHAR[funcToken.length+1]; lstrcpy(_funcName, funcToken.token); res = loadFunction(); } else { res = true; } } return res; } /* Find function in XML structure and parse it */ bool FunctionCallTip::loadFunction() { reset(); //set everything back to 0 //The functions should be ordered, but linear search because we cant access like array _curFunction = NULL; //Iterate through all keywords and find the correct function keyword TiXmlElement *funcNode = _pXmlKeyword; for (; funcNode; funcNode = funcNode->NextSiblingElement(TEXT("KeyWord"))) { const TCHAR * name = NULL; name = funcNode->Attribute(TEXT("name")); if (!name) //malformed node continue; int compVal = 0; if (_ignoreCase) compVal = testNameNoCase(name, _funcName); //lstrcmpi doesnt work in this case else compVal = lstrcmp(name, _funcName); if (!compVal) //found it! { const TCHAR * val = funcNode->Attribute(TEXT("func")); if (val) { if (!lstrcmp(val, TEXT("yes"))) { //what we've been looking for _curFunction = funcNode; break; } else { //name matches, but not a function, abort the entire procedure return false; } } } } //Nothing found if (!_curFunction) return false; stringVec paramVec; TiXmlElement *overloadNode = _curFunction->FirstChildElement(TEXT("Overload")); TiXmlElement *paramNode = NULL; for (; overloadNode ; overloadNode = overloadNode->NextSiblingElement(TEXT("Overload")) ) { const TCHAR * retVal = overloadNode->Attribute(TEXT("retVal")); if (!retVal) continue; //malformed node _retVals.push_back(retVal); const TCHAR * description = overloadNode->Attribute(TEXT("descr")); if (description) _descriptions.push_back(description); else _descriptions.push_back(TEXT("")); //"no description available" paramNode = overloadNode->FirstChildElement(TEXT("Param")); for (; paramNode ; paramNode = paramNode->NextSiblingElement(TEXT("Param")) ) { const TCHAR * param = paramNode->Attribute(TEXT("name")); if (!param) continue; //malformed node paramVec.push_back(param); } _overloads.push_back(paramVec); paramVec.clear(); ++_currentNbOverloads; } _currentNbOverloads = _overloads.size(); if (_currentNbOverloads == 0) //malformed node return false; return true; } void FunctionCallTip::showCalltip() { if (_currentNbOverloads == 0) { //ASSERT return; } //Check if the current overload still holds. If the current param exceeds amounti n overload, see if another one fits better (enough params) stringVec & params = _overloads.at(_currentOverload); size_t psize = params.size()+1; if ((size_t)_currentParam >= psize) { size_t osize = _overloads.size(); for(size_t i = 0; i < osize; ++i) { psize = _overloads.at(i).size()+1; if ((size_t)_currentParam < psize) { _currentOverload = static_cast(i); break; } } } generic_stringstream callTipText; if (_currentNbOverloads > 1) { callTipText << TEXT("\001") << _currentOverload + 1 << TEXT(" of ") << _currentNbOverloads << TEXT("\002"); } callTipText << _retVals.at(_currentOverload) << TEXT(' ') << _funcName << TEXT(' ') << _start; int highlightstart = 0; int highlightend = 0; size_t nbParams = params.size(); for (size_t i = 0; i < nbParams; ++i) { if (int(i) == _currentParam) { highlightstart = static_cast(callTipText.str().length()); highlightend = highlightstart + lstrlen(params.at(i)); } callTipText << params.at(i); if (i < nbParams - 1) callTipText << _param << TEXT(' '); } callTipText << _stop; if (_descriptions.at(_currentOverload)[0]) { callTipText << TEXT("\n") << _descriptions.at(_currentOverload); } if (isVisible()) _pEditView->execute(SCI_CALLTIPCANCEL); else _startPos = _curPos; _pEditView->showCallTip(_startPos, callTipText.str().c_str()); _selfActivated = true; if (highlightstart != highlightend) { _pEditView->execute(SCI_CALLTIPSETHLT, highlightstart, highlightend); } } void FunctionCallTip::reset() { _currentOverload = 0; _currentParam = 0; //_curPos = 0; _startPos = 0; _overloads.clear(); _currentNbOverloads = 0; _retVals.clear(); _descriptions.clear(); } void FunctionCallTip::cleanup() { reset(); if (_funcName) delete [] _funcName; _funcName = 0; _pEditView = NULL; }