diff --git a/moo.js b/moo.js index cf3a27e..3552200 100644 --- a/moo.js +++ b/moo.js @@ -53,6 +53,38 @@ } } + function pad(s, length) { + if (s.length > length) { + return s + } + return Array(length - s.length + 1).join(" ") + s + } + + function lastNLines(string, numLines) { + var position = string.length + var lineBreaks = 0; + while (true) { + var idx = string.lastIndexOf("\n", position - 1) + if (idx === -1) { + break; + } else { + lineBreaks++ + } + position = idx + if (lineBreaks === numLines) { + break; + } + if (position === 0) { + break; + } + } + var startPosition = + lineBreaks < numLines ? + 0 : + position + 1 + return string.substring(startPosition).split("\n") + } + function objectToRules(object) { var keys = Object.getOwnPropertyNames(object) var result = [] @@ -530,7 +562,8 @@ // throw, if no rule with {error: true} if (group.shouldThrow) { - throw new Error(this.formatError(token, "invalid syntax")) + var err = new Error(this.formatError(token, "invalid syntax")) + throw err; } if (group.pop) this.popState() @@ -571,13 +604,28 @@ col: this.col, } } - var start = Math.max(0, token.offset - token.col + 1) - var eol = token.lineBreaks ? token.text.indexOf('\n') : token.text.length - var firstLine = this.buffer.substring(start, token.offset + eol) - message += " at line " + token.line + " col " + token.col + ":\n\n" - message += " " + firstLine + "\n" - message += " " + Array(token.col).join(" ") + "^" - return message + + var numLinesAround = 2 + var firstDisplayedLine = Math.max(token.line - numLinesAround, 1) + var lastDisplayedLine = token.line + numLinesAround + var lastLineDigits = String(lastDisplayedLine).length + var displayedLines = lastNLines( + this.buffer, + (this.line - token.line) + numLinesAround + 1 + ) + .slice(0, 5) + var errorLines = [] + errorLines.push(message + " at line " + token.line + " col " + token.col + ":") + errorLines.push("") + for (var i = 0; i < displayedLines.length; i++) { + var line = displayedLines[i] + var lineNo = firstDisplayedLine + i + errorLines.push(pad(String(lineNo), lastLineDigits) + " " + line); + if (lineNo === token.line) { + errorLines.push(pad("", lastLineDigits + token.col + 1) + "^") + } + } + return errorLines.join("\n") } Lexer.prototype.clone = function() { diff --git a/test/test.js b/test/test.js index 2e20d18..f3300b0 100644 --- a/test/test.js +++ b/test/test.js @@ -905,8 +905,9 @@ describe('errors', () => { expect(lexer.next()).toMatchObject({value: '456'}) expect(() => lexer.next()).toThrow( "invalid syntax at line 2 col 4:\n\n" + - " 456baa\n" + - " ^" + "1 123\n" + + "2 456baa\n" + + " ^" ) }) @@ -921,8 +922,12 @@ describe('errors', () => { expect(tok).toMatchObject({type: 'error', value: ' 12\n345\n6', lineBreaks: 2}) expect(lexer.formatError(tok, "numbers!")).toBe( "numbers! at line 3 col 2:\n\n" + - " g 12\n" + - " ^" + "1 abc\n" + + "2 def\n" + + "3 g 12\n" + + " ^\n" + + "4 345\n" + + "5 6" ) }) @@ -937,8 +942,9 @@ describe('errors', () => { expect(lexer.col).toBe(9) expect(lexer.formatError(undefined, "EOF!")).toBe( "EOF! at line 2 col 9:\n\n" + - " def quxx\n" + - " ^" + "1 abc\n" + + "2 def quxx\n" + + " ^" ) }) @@ -954,8 +960,10 @@ describe('errors', () => { expect(lexer.col).toBe(1) expect(lexer.formatError(undefined, "oh no!")).toBe( "oh no! at line 2 col 1:\n\n" + - " def quxx\n" + - " ^" + "1 abc\n" + + "2 def quxx\n" + + " ^\n" + + "3 bar" ) })