diff options
Diffstat (limited to 'objectapp/static/objectapp/js/Gnowmacs/src/js/ymacs-mode-lisp.js')
-rw-r--r-- | objectapp/static/objectapp/js/Gnowmacs/src/js/ymacs-mode-lisp.js | 419 |
1 files changed, 419 insertions, 0 deletions
diff --git a/objectapp/static/objectapp/js/Gnowmacs/src/js/ymacs-mode-lisp.js b/objectapp/static/objectapp/js/Gnowmacs/src/js/ymacs-mode-lisp.js new file mode 100644 index 00000000..dede250e --- /dev/null +++ b/objectapp/static/objectapp/js/Gnowmacs/src/js/ymacs-mode-lisp.js @@ -0,0 +1,419 @@ +//> This file is part of Ymacs, an Emacs-like editor for the Web +//> http://www.ymacs.org/ +//> +//> Copyright (c) 2009-2010, Mihai Bazon, Dynarch.com. All rights reserved. +//> +//> Redistribution and use in source and binary forms, with or without +//> modification, are permitted provided that the following conditions are +//> met: +//> +//> * Redistributions of source code must retain the above copyright +//> notice, this list of conditions and the following disclaimer. +//> +//> * Redistributions in binary form must reproduce the above copyright +//> notice, this list of conditions and the following disclaimer in +//> the documentation and/or other materials provided with the +//> distribution. +//> +//> * Neither the name of Dynarch.com nor the names of its contributors +//> may be used to endorse or promote products derived from this +//> software without specific prior written permission. +//> +//> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY +//> EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +//> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +//> PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE +//> FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +//> CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +//> SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +//> INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +//> CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +//> ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +//> THE POSSIBILITY OF SUCH DAMAGE. + +// @require ymacs-tokenizer.js + +(function(){ + + Ymacs_Buffer.newCommands({ + + lisp_open_paren: Ymacs_Interactive(function(what) { + if (what == null) + what = "("; + what += isOpenParen(what); + this.cmd("insert", what); + this.cmd("backward_char"); + }), + + lisp_close_paren: Ymacs_Interactive(function(what) { + var re = new RegExp("\\s*\\" + what, "ig"); + if (this.cmd("looking_at", re)) + this._deleteText(this.point(), this.matchData.after); + this.cmd("insert", what); + }), + + lisp_close_all_parens: Ymacs_Interactive(function() { + var p = this.tokenizer.getParserForLine(this._rowcol.row); + if (p) { + // this kind of sucks, we need to rewind the stream to that location.. + var s = this.tokenizer.stream; + s.line = this._rowcol.row; + s.col = 0; + try { + while (s.col < this._rowcol.col) + p.next(); + } catch(ex) {} + p = p.copy().context.parens; // these are still-to-close + p.r_foreach(function(p){ + this.cmd("lisp_close_paren", isOpenParen(p.type)); + }, this); + } + }) + + }); + + // XXX: much of the parser is actually copied from ymacs-mode-js.js. I should somehow unify + // the duplicate code. + + var SPECIAL_FORMS = "\ +deftype defstruct defclass \ +defmacro defun defmethod defgeneric defpackage in-package defreadtable in-readtable \ +when cond unless etypecase typecase ctypecase \ +lambda let load-time-value quote macrolet \ +progn prog1 prog2 progv go flet the \ +if throw eval-when multiple-value-prog1 unwind-protect let* \ +ignore-errors handler-case case \ +labels function symbol-macrolet block tagbody catch locally \ +return return-from setq multiple-value-call".qw().toHash(); + + var COMMON_MACROS = "loop do while".qw().toHash(); + + var CONSTANTS = "t nil".qw().toHash(); + + var OPEN_PAREN = { + "(" : ")", + "{" : "}", + "[" : "]" + }; + + var CLOSE_PAREN = { + ")" : "(", + "}" : "{", + "]" : "[" + }; + + var DEFINES_FUNCTION = "defun defgeneric defmethod".qw().toHash(); + + var DEFINES_TYPE = "deftype defclass defstruct".qw().toHash(); + + var FORM_ARGS = { + "if" : "3+", + "when" : "1*", + "lambda" : "1*", + "unless" : "1*", + "defun" : "2*", + "defgeneric" : "2*", + "defmethod" : "2*", + "defclass" : "2*", + "defmacro" : "2*", + "progn" : "0*", + "prog1" : "0*", + "prog2" : "0*", + "let" : "1*" + }; + + function isOpenParen(ch) { + return OPEN_PAREN[ch]; + }; + + function isCloseParen(ch) { + return CLOSE_PAREN[ch]; + }; + + function isConstituent(ch) { + return ch.toLowerCase() != ch.toUpperCase() || + /^[-0-9!#$%&*+./:<=>?@\[\]\^_\{\}~]$/i.test(ch); + }; + + function isConstituentStart(ch) { + return ch != "#" && isConstituent(ch); + }; + + // the tokenizer function + Ymacs_Tokenizer.define("lisp", function(stream, tok){ + + var $cont = [], + $inString = false, + $inComment = false, + $quote = null, + $parens = [], + $passedParens = [], + $backList = [], + $list = [], + PARSER = { next: next, copy: copy, indentation: indentation }; + + function copy() { + var context = restore.context = { + cont : $cont.slice(0), + quote : $quote, + inString : $inString, + inComment : $inComment, + parens : $parens.slice(0), + passedParens : $passedParens.slice(0), + backList : $backList.slice(0), + list : $list.slice(0) + }; + function restore() { + $cont = context.cont.slice(0); + $inString = context.inString; + $quote = context.quote; + $inComment = context.inComment; + $parens = context.parens.slice(0); + $passedParens = context.passedParens.slice(0); + $backList = context.backList.slice(0), + $list = context.list.slice(0); + return PARSER; + }; + return restore; + }; + + function foundToken(c1, c2, type) { + tok.onToken(stream.line, c1, c2, type); + }; + + function newArg(what) { + if (what == null) + what = { c1: stream.col }; + $list.push(what); + }; + + function INDENT_LEVEL() { return stream.buffer.getq("indent_level"); }; + + function readName() { + var col = stream.col, ch = stream.get(), + name = ch; + while (!stream.eol()) { + ch = stream.peek(); + if (!isConstituent(ch)) + break; + name += ch; + stream.nextCol(); + } + return ch && { line: stream.line, c1: col, c2: stream.col, id: name.toLowerCase() }; + }; + + function readString(end, type) { + var ch, esc = false, start = stream.col; + while (!stream.eol()) { + ch = stream.peek(); + if (ch === end && !esc) { + $cont.pop(); + $inString = null; + foundToken(start, stream.col, type); + foundToken(stream.col, ++stream.col, type + "-stopper"); + return true; + } + esc = !esc && ch === "\\"; + stream.nextCol(); + } + foundToken(start, stream.col, type); + }; + + function readComment() { + var line = stream.lineText(), pos = line.indexOf("|#", stream.col); + var m = /^\s*\|+/.exec(line.substr(stream.col)); + if (m) { + foundToken(stream.col, stream.col += m[0].length, "mcomment-starter"); + } + if (pos >= 0) { + $cont.pop(); + $inComment = null; + foundToken(stream.col, pos, "mcomment"); + foundToken(pos, pos += 2, "mcomment-stopper"); + stream.col = pos; + } else { + foundToken(stream.col, line.length, "mcomment"); + stream.col = line.length; + } + }; + + function isForm(form) { + var f = $list && $list.length > 0 && $list[0].id; + if (f) { + f = f.toLowerCase(); + if (form == null) + return f; + return typeof form == "string" ? f == form : f in form; + } + }; + + function next() { + stream.checkStop(); + if ($cont.length > 0) + return $cont.peek()(); + var ch = stream.peek(), tmp; + if ((tmp = stream.lookingAt(/^#\\(Space|Newline|.?)/i))) { + newArg(); + foundToken(stream.col, stream.col += tmp[0].length, "constant"); + } + else if (stream.lookingAt(/^#\x27[^(]/)) { + newArg(); + stream.col += 2; + tmp = readName(); + foundToken(tmp.c1, tmp.c2, "function-name"); + } + else if (stream.lookingAt("#|")) { + $inComment = { line: stream.line, c1: stream.col }; + foundToken(stream.col, stream.col += 2, "mcomment-starter"); + $cont.push(readComment); + } + else if ((tmp = stream.lookingAt(/^;+/))) { + foundToken(stream.col, stream.col += tmp[0].length, "comment-starter"); + foundToken(stream.col, stream.col = stream.lineLength(), "comment"); + } + else if (ch === '"') { + newArg(); + $inString = { line: stream.line, c1: stream.col }; + foundToken(stream.col, ++stream.col, "string-starter"); + $cont.push(readString.$C(ch, "string")); + } + else if ((tmp = stream.lookingAt(/^[+-]?(#x[0-9a-f]+|#o[0-7]+|#b[01]+|[0-9]*\.?[0-9]+e?[0-9]*)(\x2f(#x[0-9a-f]+|#o[0-7]+|#b[01]+|[0-9]*\.?[0-9]+e?[0-9]*))?/))) { // Dude, WTF... + newArg(); + foundToken(stream.col, stream.col += tmp[0].length, "number"); + } + else if ((tmp = isOpenParen(ch))) { + newArg(); + $backList.push($list); + $list = []; + $parens.push({ line: stream.line, col: stream.col, type: ch }); + foundToken(stream.col, ++stream.col, "open-paren"); + } + else if ((tmp = isCloseParen(ch))) { + var p = $parens.pop(); + if (!p || p.type != tmp) { + foundToken(stream.col, ++stream.col, "error"); + } else { + p.closed = { line: stream.line, col: stream.col, opened: p }; + $passedParens.push(p); + $list = $backList.pop(); + foundToken(stream.col, ++stream.col, "close-paren"); + } + } + else if (isConstituentStart(ch) && (tmp = readName())) { + var type = ch == ":" ? "lisp-keyword" + : tmp.id in SPECIAL_FORMS ? "keyword" + : tmp.id in COMMON_MACROS ? "builtin" + : tmp.id in CONSTANTS ? "constant" + : null; + if (!type) { + // perhaps function name? + if (isForm(DEFINES_FUNCTION) && $list.length == 1) { + type = "function-name"; + } + else if (isForm(DEFINES_TYPE) && $list.length == 1) { + type = "type"; + } + // there are a lot of macros starting with "with-", so let's highlight this + else if (/^with-/i.test(tmp.id)) { + type = "builtin"; + } + } + newArg(tmp); + foundToken(tmp.c1, tmp.c2, type); + } + else { + foundToken(stream.col, ++stream.col, null); + } + }; + + function indentation() { + // no indentation for continued strings + if ($inString) + return 0; + + var currentLine = stream.lineText(); + var indent = 0; + + var p = $parens.peek(); + if (p) { + var line = stream.lineText(p.line); + indent = p.col + 1; + var nextNonSpace; + if (isConstituentStart(line.charAt(indent))) { + indent = p.col + INDENT_LEVEL(); + var re = /\s\S/g; + re.lastIndex = p.col; + nextNonSpace = re.exec(line); + if (nextNonSpace) { + nextNonSpace = nextNonSpace.index + 1; + } + } + if ($list && $list.length) { + // console.log($list); + var currentForm = isForm(); + if (currentForm) { + currentForm = currentForm.replace(/\*$/, ""); + var formArgs = FORM_ARGS[currentForm]; + if (!formArgs && /^with/.test(currentForm)) { + // "with" macros usually take one argument, then &body + formArgs = "1*"; + } + if (!formArgs) { + formArgs = "1+"; // kind of sucky now + } + if (formArgs) { + var n = parseInt(formArgs, 10); + var hasRest = /\+$/.test(formArgs); + var hasBody =/\*$/.test(formArgs); + // console.log("Expecting %d arguments, got %d already (rest=%o, body=%o)", n, $list.length - 1, hasRest, hasBody); + if ($list.length - 1 < n || hasRest) { + // still in the arguments + if (nextNonSpace) + indent = nextNonSpace; + else + indent += INDENT_LEVEL(); + } + } + } + } + } + + return indent; + }; + + return PARSER; + }); + +})(); + +DEFINE_SINGLETON("Ymacs_Keymap_LispMode", Ymacs_Keymap, function(D, P){ + + D.KEYS = { + "ENTER" : "newline_and_indent", + "(" : [ "lisp_open_paren", "(" ], + ")" : [ "lisp_close_paren", ")" ], + "C-c ] && C-c C-]" : "lisp_close_all_parens" + }; + +}); + +Ymacs_Buffer.newMode("lisp_mode", function() { + + var tok = this.tokenizer; + this.setTokenizer(new Ymacs_Tokenizer({ buffer: this, type: "lisp" })); + var changed_vars = this.setq({ + indent_level: 2 + }); + var keymap = Ymacs_Keymap_LispMode(); + this.pushKeymap(keymap); + var was_paren_match = this.cmd("paren_match_mode", true); + + return function() { + this.setTokenizer(tok); + this.setq(changed_vars); + this.popKeymap(keymap); + if (!was_paren_match) + this.cmd("paren_match_mode", false); + }; + +}); |