ActiveState Community

PHPDoc Autocompletion

Posted by nathan on 2009-02-05 10:45
OS: All / Any

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

After installing you will have to either Execute 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

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

toddw | 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:

/**
 * @fileoverview  Enter trigger for PHPdoc (code based on TAB trigger for Abbreviations by Stan Angeloff)
 * @author        Nathan Rijksen (http://naatan.com/)
 * @contributor   Todd Whiteman
 * @version       0.2
 */

if (typeof(extensions) === 'undefined')
/**
 * 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() {

    var log = ko.logging.getLogger("AutoTriggerDoc");
    //log.setLevel(ko.logging.LOG_DEBUG);

    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 {

                ko.views.manager.currentView.setFocus();
                // Create shorthands for 'currentView'
                var view = ko.views.manager.currentView;
                /**
                 * @type {Components.interfaces.ISciMoz}
                 */

                var editor = view.scimoz;

                // Ensure we don't have have remaining indicators within the document
                // ToddW: Not sure why this was here? Removing it for now.
                //if (view.document.hasTabstopInsertionTable &&
                //    view.document.getTabstopInsertionTable({}).length > 0) {
                //    log.debug("hasTabstopInsertionTable:: length: " + view.document.getTabstopInsertionTable({}).length);
                //    return true;
                //}

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

                var lang = 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);
                log.debug("strLeft: " + strLeft);

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

                    preventDefault = endUndoAction = true;

                    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);
                    var snipType;
                    var snipText = '\n * [[%tabstop:Summary]]\n';
                    var type_tabstop = '[[%tabstop:Type]]';
                    var desc_tabstop = '[[%tabstop:Description]]';

                    var match = nxtLine.match(/\bfunction(\s*[-_a-z0-9]+\s*)?\((.*)\)/i);
                    if (match) {
                        var fvars = match[2].split(',');
                        for (var i = 0; i <= fvars.length - 1; i++) {
                            fvar = fvars[i].replace(/\s*?(\$[-_a-z0-9]*).*/i, "$1");
                            if (lang == "PHP") {
                                snipText += ' * @param ' + type_tabstop + ' ' +
                                            fvar + ' - ' + desc_tabstop + '\n';
                            } else if (lang == "JavaScript") {
                                snipText += ' * @param ' + fvar + ' ' +
                                            type_tabstop + ' - ' + desc_tabstop + '\n';
                            }
                        }
                        snipType = 'function';
                    } else if (nxtLine.match(/^\s*?class/i)) {
                        snipType = 'class';
                    } else if (nxtLine.match(/^\s*?var/i)) {
                        if (lang == "PHP") {
                            snipText += ' * @var ' + type_tabstop + ' - ' + desc_tabstop + '\n';
                        } else if (lang == "JavaScript") {
                            snipText += ' * @type ' + type_tabstop + ' - ' + desc_tabstop + '\n';
                        }
                        snipType = 'var';
                    }
                    if (snipType) {
                        var snippet = ko.abbrev.findAbbrevSnippet('phpdoc_' + snipType);
                        if (snippet) {
                            snipText += snippet.value;
                        }
                    }
                    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);
        }
    }
    var editor_pane = ko.views.manager.topView;
    editor_pane.addEventListener('keypress', this.onKeyPress, true);

}).apply(extensions.AutoTriggerDoc);

/**
 * @fileoverview  Enter trigger for PHPdoc (code based on TAB trigger for Abbreviations by Stan Angeloff)
 * @author        Nathan Rijksen (http://naatan.com/)
 * @contributor   Todd Whiteman
 * @version       0.2
 */

if (typeof(extensions) === 'undefined')
/**
 * 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() {

    var log = ko.logging.getLogger("AutoTriggerDoc");
    //log.setLevel(ko.logging.LOG_DEBUG);

    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 {

                ko.views.manager.currentView.setFocus();
                // Create shorthands for 'currentView'
                var view = ko.views.manager.currentView;
                /**
                 * @type {Components.interfaces.ISciMoz}
                 */
                var editor = view.scimoz;

                // Ensure we don't have have remaining indicators within the document
                // ToddW: Not sure why this was here? Removing it for now.
                //if (view.document.hasTabstopInsertionTable &&
                //    view.document.getTabstopInsertionTable({}).length > 0) {
                //    log.debug("hasTabstopInsertionTable:: length: " + view.document.getTabstopInsertionTable({}).length);
                //    return true;
                //}

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

                var lang = 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);
                log.debug("strLeft: " + strLeft);

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

                    preventDefault = endUndoAction = true;

                    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);
                    var snipType;
                    var snipText = '\n * [[%tabstop:Summary]]\n';
                    var type_tabstop = '[[%tabstop:Type]]';
                    var desc_tabstop = '[[%tabstop:Description]]';

                    var match = nxtLine.match(/\bfunction(\s*[-_a-z0-9]+\s*)?\((.*)\)/i);
                    if (match) {
                        var fvars = match[2].split(',');
                        for (var i = 0; i <= fvars.length - 1; i++) {
                            fvar = fvars[i].replace(/\s*?(\$[-_a-z0-9]*).*/i, "$1");
                            if (lang == "PHP") {
                                snipText += ' * @param ' + type_tabstop + ' ' +
                                            fvar + ' - ' + desc_tabstop + '\n';
                            } else if (lang == "JavaScript") {
                                snipText += ' * @param ' + fvar + ' ' +
                                            type_tabstop + ' - ' + desc_tabstop + '\n';
                            }
                        }
                        snipType = 'function';
                    } else if (nxtLine.match(/^\s*?class/i)) {
                        snipType = 'class';
                    } else if (nxtLine.match(/^\s*?var/i)) {
                        if (lang == "PHP") {
                            snipText += ' * @var ' + type_tabstop + ' - ' + desc_tabstop + '\n';
                        } else if (lang == "JavaScript") {
                            snipText += ' * @type ' + type_tabstop + ' - ' + desc_tabstop + '\n';
                        }
                        snipType = 'var';
                    }
                    if (snipType) {
                        var snippet = ko.abbrev.findAbbrevSnippet('phpdoc_' + snipType);
                        if (snippet) {
                            snipText += snippet.value;
                        }
                    }
                    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);
        }
    }
    var editor_pane = ko.views.manager.topView;
    editor_pane.addEventListener('keypress', this.onKeyPress, true);

}).apply(extensions.AutoTriggerDoc);

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 | 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(',');
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 :)