PHPDoc Autocompletion

Posted by nathan on 2009-02-05 10:45

Introduction

Let me start off by pointing out that this Macro is still a work in progress and should be considered as a Beta product.

Also note that I am an OK javascript programmer.. but I am no javascript wizard.. so the code is done as best I could at this point.

Description

Basically what this macro does is that which competitive IDE's like Netbeans and Eclipse PDT already do; it automatically adds phpdoc comments to your php file when you type '/**' followed by the ENTER key.

At this point by default it will auto complete to:

/**
*
*/

If the line below the current line is a function, var or class it will check for an Abbreviation and add it's contents to the phpdoc comment.

Installation

Create a new Komodo macro in your Komodo toolbox and:

  • copy the below source code into the macro text area
  • select the macro Triggers tab and check the "Macro should Trigger on a Komodo event", selecting the "On startup" event
  • click okay to save the macro

After installing you will have to either Execute (double-click) the macro or restart Komodo.

I've only tested it on Windows XP but technically it shouldn't make a difference what OS you use.

Usage

Currently you can use the following abbreviations (these will have to be located in the PHP abbreviations folder):

  • phpdoc_function
  • phpdoc_class
  • phpdoc_var

You will have to include the star symbol (*) in front of every line and you'll probably want to check the "Maintain indentation context after insertion" option.

For functions the macro will automatically detect the variables used and add them to the phpdoc comment.

Currently this is all it does. I make very basic use of phpdoc so I don't have any need for further functionality, but please feel free to modify the macro or request new features.

Thanks to

I'd like to thank ToddW for helping me with some of the scintilla / komodo specific features.. this is my first Komodo macro so I'm a bit of a rookie as of yet.

Also thanks to Stan Angeloff for his "TAB trigger for Abbreviations" macro, which this macro is based upon.

Changelog

v0.2.1
(Based on the modifications by ToddW)
- Support for Tabstops (by ToddW)
- Bug fixed: don't parse function variables when a function has no variables
- Bug fixed: indentation issues with personal snippets

v0.2.2 by Michael Kocarek
+ Auto-completes types hinted for function arguments for PHP
* JavaScript @param and @type doc is Komodo code intelligence compatible
! Recognizes PHP functions with modifier keywords
! Recognizes PHP class variables, which are declared with different word than "var "
! Fixed the issue, when pressing Enter at the end of docblock opening line in existing comment caused insertion of new phpDoc block inside existing one.

v0.2.3 by Alexander Kavoun
* Understands "function: name (params)" declaration in javascript
* Fixed adding extra space for snippets
+ Aligns strings after type and name declarations
+ Adds @returns for functions

v0.2.4 by Michael
* Fixed typo in @return

v0.2.5 by ToddW
* Made compatible with Komodo 7
* Dropped view.focus() call - as that mucks up with Komodo 7's fast find

Macro Source Code

/**
 * @fileoverview  Enter trigger for PHPdoc (code based on TAB trigger for Abbreviations by Stan Angeloff)
 * @author        Nathan Rijksen (http://naatan.com/)
 * @contributor   Todd Whiteman
 * @contributor   Michal Kocarek (http://brainbox.cz/)
 * @contributor   Alexander Kavoun (http://takkmoil.com/)
 * @version       0.2.5
 */

if ('undefined' === typeof(extensions)) {
    /**
     * Komodo Extensions Namespace.
     * This namespace was suggested by JeffG. More information is available at:
     *    {@link http://community.activestate.com/forum-topic/extension-s-namespace}
     *
     * @type  Object
     */

    extensions = {};
}

if (extensions.AutoTriggerDoc && extensions.AutoTriggerDoc.onKeyPress) {
    // Remove the existing trigger handler, we'll re-instate it.
    var editor_pane = ko.views.manager.topView;
    editor_pane.removeEventListener('keypress', extensions.AutoTriggerDoc.onKeyPress, true);
}
extensions.AutoTriggerDoc = {};

(function() {

    /**
     * RegExp for matching function arguments in match $1 for PHP
     */

    var re_function_php = /^\s*function\s*?[-_a-z0-9]*?\s*?\((.*)\).*/i;

    /**
     * RegExp for matching function arguments in match $1 for JavaScript
     */

    var re_function_javascript = /^\s*[(?:(?:abstract|final|static|private|public|protected)\s+?)|[-_a-z0-9]*:\s*?]*?function\s*?[-_a-z0-9]*?\s*?\((.*)\).*/i;

    /**
     * RegExp for matching parameter info in PHP
     */

    var re_param_php = /\s*?(?:([_a-z][_a-z0-9]*)\s+)?(\$[_a-z][_a-z0-9]*).*/i;

    /**
     * RegExp for matching parameter info in JavaScript
     */

    var re_param_javascript = /\s*?([\-_a-z0-9]+).*/i;

    var log = ko.logging.getLogger('AutoTriggerDoc');

    this.onKeyPress = function(e) {
        try {
            // Only trap when ENTER pressed with no modifiers
            if (e.keyCode !== 13 || e.ctrlKey || e.altKey || e.shiftKey) return true;

            //log.debug('onKeyPress:: enter key pressed:: keyCode: ' + e.keyCode);

            var preventDefault = endUndoAction = false;
            try {
                // Create shorthands for 'currentView'
                var view = ko.views.manager.currentView;
                /**
                 * @type {Components.interfaces.ISciMoz}
                 */

                var editor = view.scimoz;

                // Don't do anything if there is a selection within the document
                if (editor.anchor != editor.currentPos) return false;

                var lang = (view.koDoc || view.document).language;

                // Start at cursor position and break at any non-word character
                var currentPos = editor.currentPos;
                var rangeStart = currentPos - 3;
                var strLeft = editor.getTextRange(rangeStart, currentPos);

                // If we have a matching range, make sure it's a word
                if (strLeft !== null && strLeft == '/**') {

                    var lineno = editor.lineFromPosition(currentPos);
                    var nxt_start = editor.positionFromLine(lineno+1);
                    var nxt_end = editor.getLineEndPosition(lineno+1);
                    var nxtLine = editor.getTextRange(nxt_start, nxt_end);

                    if (nxtLine.match(/^[\t ]*\*/)) { return true; }

                    preventDefault = endUndoAction = true;

                    var snipType;
                    var snipText = '\n * [[%tabstop:Summary]]\n';
                    var type_tabstop = '[[%tabstop:Type]]';
                    var desc_tabstop = '[[%tabstop:Description]]';

                    var nl_inserted = false; // Was newline after summary inserted?

                    if (nxtLine.match(/\bfunction\b/i)) { // Is word function on the line?
                        if (lang == 'PHP') {
                            var fvars = nxtLine.replace(re_function_php, '$1');
                            fvars = fvars.split(',');
                            var matches = [];
                            var len = [0,7,0];

                            // Matching all params and determinig the longest
                            for (var i = 0; i < fvars.length; ++i) {
                                matches[i] = fvars[i].match(re_param_php);
                                if (!matches[i]) continue;
                                for (var j = 0; j < matches[i].length; j++) {
                                    if (undefined == matches[i][j]) continue;
                                    if (len[j] < matches[i][j].length) { len[j] = matches[i][j].length; }
                                }
                            }

                            for (var i = 0; i < matches.length; ++i) {
                                if (!matches[i]) continue;
                                if (!nl_inserted) { snipText += ' * \n'; nl_inserted = true; }
                                snipText +=
                                    ' * @param ' + type_tabstop.replace('Type', matches[i][1] || 'unknown') +
                                    new Array(len[1] - (matches[i][1] ? matches[i][1].length : 7) + 1).join(' ') + ' ' +
                                    matches[i][2] + new Array(len[2] - matches[i][2].length + 1).join(' ') + ' ' + desc_tabstop + '\n';
                            }

                            snipText += ' * \n * @return ' + type_tabstop + new Array(len[1] - 3).join(' ') + ' ' + desc_tabstop + '\n';
                        } else if (lang == "JavaScript") {
                            var fvars = nxtLine.replace(re_function_javascript, '$1');
                            fvars = fvars.split(',');
                            var matches = [];
                            var len = [0,0,0];

                            // Matching all params and determinig the longest
                            for (var i = 0; i < fvars.length; ++i) {
                                matches[i] = fvars[i].match(re_param_javascript);
                                if (!matches[i]) continue;
                                for (var j = 0; j < matches[i].length; j++) {
                                    if (undefined == matches[i][j]) continue;
                                    if (len[j] < matches[i][j].length) { len[j] = matches[i][j].length; }
                                }
                            }

                            for (var i = 0; i < fvars.length; ++i) {
                                if (!matches[i]) continue;
                                if (!nl_inserted) { snipText += ' * \n'; nl_inserted = true; }
                                snipText += ' * @param   {' + type_tabstop + '} ' + matches[i][1] + new Array(len[1] - matches[i][1].length + 1).join(' ') + ' ' + desc_tabstop + '\n';
                            }
                            snipText += ' * \n * @returns {' + type_tabstop + '} ' + desc_tabstop + '\n';
                        }
                        snipType = 'function';
                    } else if (nxtLine.match(/^\s*?class/i)) {
                        snipType = 'class';
                    } else if (nxtLine.match(/^\s*?(?:var|private|public|protected)/i)) {
                        if (lang == 'PHP') {
                            snipText = '\n * @var ' + type_tabstop.replace('unknown') + ' ' + desc_tabstop + '\n';
                        } else if (lang == 'JavaScript') {
                            snipText = '\n * @type ' + type_tabstop + ' ' + desc_tabstop + '\n';
                        }
                        snipType = 'var';
                    }
                    if (snipType) {
                        var snippet = ko.abbrev.findAbbrevSnippet('phpdoc_' + snipType);
                        if (snippet) { snipText += snippet.value + '\n'; }
                    }
                    snipText += ' */';
                    var fakeSnippet = {
                        hasAttribute: function(name) {
                            return name in this;
                        },
                        getStringAttribute: function(name) {
                            return this[name];
                        },
                        name: 'autodoc snippet',
                        indent_relative: 'true',
                        value: snipText
                    };
                    //log.debug('snipText: ' + snipText);
                    ko.projects.snippetInsert(fakeSnippet);
                }
            } finally {
                if (preventDefault) {
                    e.preventDefault();
                    e.stopPropagation();
                }
            }
        } catch(ex) {
            log.exception(ex);
        }
        return null;
    }
    var editor_pane = ko.views.manager.topView;
    editor_pane.addEventListener('keypress', this.onKeyPress, true);

}).apply(extensions.AutoTriggerDoc);

AttachmentSize
ENTER trigger for PHPDoc v0.1.kpz5.23 KB
ENTER trigger for PHPDoc v0.1.1.kpz5.23 KB
ENTER trigger for PHPDoc v0.1.2.kpz5.38 KB
ENTER trigger for PHPDoc v0.2.1.kpz6.74 KB
ENTER trigger for PHPDoc v0.2.2.zip9.89 KB
ENTER trigger for PHPDoc v0.2.3.zip10.63 KB
ENTER trigger for PHPDoc v0.2.4.zip10.63 KB
ENTER trigger for PHPDoc v0.2.5.zip2.66 KB

toddw
ActiveState Staff
Fri, 2009-02-06 15:37

I had installed your snippet and forgot about it, but I soon gratified to see a completion in JavaScript after typing "/**[Enter". I was impressed!

The format for JSDoc is a little different, so I decided to modify your macro a little to include JS support, and also I couldn't resist a little refactoring of the insert line routine to use Komodo snippet's functionality, which allowed me to insert tabstops as well.

Here is the modified version of your code (merged in to Nathan's latest code):

Thanks for creating such a useful macro :D

Cheers,
Todd

nathan | Fri, 2009-02-06 16:03

Thanks so much for posting your modifications Todd :) Your input is very much appreciated.

There were some things like tabstops that I did not know enough about to implement them myself but I was definitely thinking about it, glad you made it happen :)

mjh_ca | Sun, 2009-03-15 02:46

This is helpful - thanks!

One change - within PHP class blocks, the functions are usually prefixed with public/private/protected and possibly the static keyword.. i.e.

class foo {
    public function bar() {}
}

If you update your regular expressions as follows then it will also work for those functions:

Old lines:

                    if (nxtLine.match(/^\s*?function/i)) {
                        var fvars = nxtLine.replace(/^\s*function\s*?[-_a-z0-9]*?\s*?\((.*)\).*/i, '$1');

New lines:

                    if (nxtLine.match(/^\s*(?:static )?(?:public |private |protected )?function/i)) {
                        var fvars = nxtLine.replace(/^\s*(?:static )?(?:public |private |protected )?function\s*?[-_a-z0-9]*?\s*?\((.*)\).*/i, '$1');

toddw
ActiveState Staff
Mon, 2009-03-16 11:47

Agreed, though I myself made it even more relaxed still (not bothering to match the start of the line, just find "function" in the next line is enough), as JavaScript can have some strange function patterns like:
var f= function(arg);
(function(arg) {})();

this is my regex pattern handling I'm now using:

var match = nxtLine.match(/\bfunction(\s*[-_a-z0-9]+\s*)?\((.*)\)/i);
if (match) {
    var fvars = match[2].split(',');

nathan | Fri, 2009-03-20 07:31

I ran into the issue as well and fixed it almost exactly the same way you did, I forgot to update this thread tho :) So thanks!

I've updated my own macro with Todd's snippet, thanks Todd! :)

cyberwolf | Wed, 2009-05-20 07:16

It would be nice if PHP type hints got supported in the next version of this macro.

function myFunc(MyClass $object) {...}

would result in:

@param MyClass $object

idanmashaal | Sat, 2009-06-27 09:02

Hi,

I use Komodo, but never tried using macros.

I installed your macro, and restarted Komodo and it works nicely (on a Mac).

What do you mean by abbreviations ?

I opened the Toolbox, and saw the PHP abbreviations folder under samples 5.1.4
Then, I found for example the 'array' abbreviations - I use it on OS X by typing "array" and Meta+T

But I didn't figure out how to add the ones you stated: phpdoc_function, phpdoc_class, phpdoc_var.

Help will be appreciated,
Thanks.
Idan.

nathan | Tue, 2009-06-30 06:28

Please check my reply below (in reply to devians question)

devians | Sun, 2009-06-28 21:23

where could one find the abbreviations this is meant to use? they dont seem to be included with the examples.

nathan | Tue, 2009-06-30 06:27

The macro simply checks if they exist, if they do then the contents will be added to the parsed phpdoc.

For example. I have a snippet for "phpdoc_function" with the following content

* @author Nathan Rijksen

This would result in the following phpdoc being parsed:

         /**
         * Summary
         * @param Type $file
         * @author Nathan Rijksen
         */
        function view($file) {

These snippets need to be under Abbreviations/PHP

idanmashaal | Tue, 2009-06-30 07:20

Thanks :)

michal.kocarek | Wed, 2010-02-10 11:49

I am posting updated version of the macro.

Changelog:
[+] Auto-completes types hinted for function arguments for PHP
[*] JavaScript @param and @type doc is Komodo code intelligence compatible
[!] Recognizes PHP functions with modifier keywords
[!] Recognizes PHP class variables, which are declared with different word than "var "
[!] Fixed the issue, when pressing Enter at the end of docblock opening line in existing comment caused insertion of new phpDoc block inside existing one.

These code changes were merged in as version 0.2.2.

streetdaddy | Thu, 2010-02-18 03:28

Thanks to the author and all contributors, this is a very handy addition. However, I've not really used abbreviations at all and wondering if I'm missing out on something with this macro if I've not got them setup as described in the 'Usage' instructions.

Can you please describe exactly what I need to add for each abbreviation?

nathan | Fri, 2010-08-06 07:32

Simply create snippets with the following names:

* phpdoc_function
* phpdoc_class
* phpdoc_var

The contents of these snippets will be appended to the phpdoc.

So if you want to add an @author tag to classes you create a snippet "phpdoc_class" with contents: " * @author Bill"

phazei | Thu, 2010-03-11 14:38

nathan | Fri, 2010-08-06 07:34

This macro will automatically write most of the phpdoc code for you when you start typing it, so that you only have to change the relative values.

takkmoil | Mon, 2010-08-02 08:26

[*] Understands "function: name (params)" declaration in javascript
[*] Fixed adding extra space for snippets
[+] Aligns strings after type and name declarations
[+] Adds @returns for functions

These changes were merged into version 0.2.3.

nathan | Fri, 2010-08-06 07:36

Thanks Michal and Alexander for your contributions! This is really why I love open-source :) I created a very straight forward macro and the rest of the community kicks in to turn it into something far more powerful. I really hope Activestate will adopt this functionality as a native feature of Komodo at some point.

_michael | Fri, 2010-09-24 05:36

Hi,

I've been using Nathan's macro for quite some time now - very helpful, and the improved version is even better. Thanks folks!

There was a minor typo in the PHPDoc branch (@returns should really be @return). So here is an update with just this one typo corrected ;-)

Code was merged in as version 0.2.4.

Cheers,
Michael

nathan | Fri, 2010-09-24 10:18

Thanks for the suggestion, I added them as attachments, note that the ZIP's are for Komodo 6. I don't have Komodo 5 installed so I can't generate the KPZ's.

p.scheit | Fri, 2010-08-20 03:16

Hi, thanks for this script.

Unfortunately i'm having trouble with triggering this macro. I enabled the loggin and see every keypress in the logfile. But online if I type something like
"/*" or "/" or "/***" but not "/** "

my thought was that some other script ist getting the Event and is preventing the macro to run?

Do you have an idea which script this is?
Komodo Edit, version 5.2.4, build 4343, platform win32-x86.

-----------------
oh. Nevermind. I got it. I think i have run an old version and the current version of this macro and they conflicted (restarting komodo helped)

michal.kocarek | Fri, 2010-08-20 03:23

Hi,

macro is filling the autocompletion only after /** sequence. This is because /** is the only one sequence allowed as phpDoc and JSDoc starting sequence. You shouldn't use //, /* nor /*** as phpDoc or JSDoc starting sequence.

dosboy | Thu, 2010-12-23 03:33

Hi guys. I just discovered this and I think it's great. But I'm having some issues with JSDoc and Komodo, and figured someone in here might be able to help.

First off, does Komodo support the JSDoc syntax for overloaded functions (example here)? For example, I'm trying to put together a file for Raphael, and currently it looks like:

/**
 * @name Raphael
 * Option 1: Creates a canvas object on which to draw. You must do this first, as all future calls to drawing methods from this instance will be bound to this canvas.
 *
 * @param   {element|string} container    Description
 * @param   {number} width  Description
 * @param   {number} height Description
 *
 * @returns {object} Description
 */

/**
 * @name Raphael^2
 * Option 2: Creates a canvas object on which to draw. You must do this first, as all future calls to drawing methods from this instance will be bound to this canvas.
 *
 * @param   {number} x    Description
 * @param   {number} y      Description
 * @param   {number} width  Description
 * @param   {number} height Description
 *
 * @returns {object} Description
 */

var Raphael = function (x, y, width, height){}

The calltip that pops up when I try creating an instance of Raphael() is only referencing the description from the first @name instance, and the function parameters are showing (x, y, width, height) as that's what's defined in the actual function. Is it even possible to achieve what I'm trying to, or is it just wishful thinking?

nathan | Thu, 2010-12-23 09:09

This macro just provides autocompletion for phpdoc / jsdoc. It does not provide any of the additional phpdoc integration that Komodo sports (such as tooltip hinting).

Generally the activestate guys pretty much read all the posts (from my impression) but you might have better luck asking your question in the support forum.

envision | Thu, 2011-03-17 18:45

Your "Installation" section begins with:
"After installing ..."

OK...
Sorry, but I have no idea what comes before "after installing". I downloaded the latest attached file, unzipped it and end up with something without filename extension. What do I do with it?

szczepan | Sun, 2011-05-01 08:53

Same problem here. How do I actually install this thing?

nathan | Wed, 2011-05-25 11:51

There are no special installation steps, as with any macro just drag it to your toolbox.

toddw
ActiveState Staff
Thu, 2011-08-11 11:42

Hi Nathan,

I've updated the page to include copy/paste installation instructions and included the macro source code (version 0.2.5), and dropped all the pasted source code updates from contributors (as the code has already been merged into your macro).

I also tweaked the macro to make it compatible with Komodo 7:
* replaced "view.document" with a check for "(view.koDoc || view.document)"
* dropped the "view.focus()" call, as I don't think that's needed and it mucks up with Komodo 7's fast find

Cheers,
Todd

takkmoil | Wed, 2012-01-11 05:08

Hello!

I've made little fix to support multi-line function declarations:

function create_user($token, $invite,
                     $name, $type,
                     $priveleged = false)
{
...
}

Works both for PHP and javaScript. Ready for installation macro available here: http://dl.dropbox.com/u/6467250/ko/ENTER_trigger_for_PHPDoc_v0.2.6.zip

nathanri
ActiveState Staff
Fri, 2013-10-11 09:06

Thanks for the contribution, unfortunately I've not been maintaining / following this macro anymore, so apologies for the very late response. Any chance you could repost your contribution as your dropbox link is giving a 404.

I'll likely be integrating this code into Komodo 9 so it will be properly maintained.

damo.clark | Sat, 2014-01-11 19:08

I've been using this macro for a couple years and it is fantastic. I'm surprised nobody has integrated into Komodo by now. Hope it makes it's way into Komodo 9.

toddw
ActiveState Staff
Fri, 2014-01-24 15:06

We've actually had it on the roadmap for a few releases, but other items got in the way.

It certainly would be a good *permanent* feature for Komodo.

Cheers,
Todd

damo.clark | Fri, 2014-01-24 21:20

So does this mean its priority is raised for version 9 Todd?

Regards,
Damien.