add latex syntax checking

This commit is contained in:
Brian Gough 2016-10-06 14:46:34 +01:00
parent 8c7d712738
commit fe866e54fc
2 changed files with 471 additions and 57 deletions

View file

@ -205,35 +205,88 @@ var LatexFoldMode = require("./folding/latex").FoldMode;
var Range = require("../range").Range;
var WorkerClient = require("ace/worker/worker_client").WorkerClient;
var Mode = function() {
this.HighlightRules = LatexHighlightRules;
this.foldingRules = new LatexFoldMode();
this.createWorker = function(session) {
var worker = new WorkerClient(["ace"], "ace/mode/latex_worker", "LatexWorker");
var doc = session.getDocument();
var selection = session.getSelection();
var savedRange = {};
var suppressions = [];
var hints = [];
var changeHandler = null;
worker.attachToDocument(session.getDocument());
var worker = new WorkerClient(["ace"], "ace/mode/latex_worker", "LatexWorker");
worker.attachToDocument(doc);
worker.on("lint", function(results) {
doc.on("change", function () {
if(changeHandler) {
clearTimeout(changeHandler);
changeHandler = null;
}
});
selection.on("changeCursor", function () {
if(suppressions.length > 0) {
changeHandler = setTimeout(function () {
updateMarkers();
suppressions = [];
changeHandler = null;
}, 100);
}
});
var updateMarkers = function () {
var annotations = [];
var newRange = {};
for (var i = 0; i<results.data.length; i++) {
var start_row = results.data[i].start_row;
var end_row = results.data[i].end_row;
var key = start_row + ":" + end_row;
newRange[key] = results.data[i];
var cursor = selection.getCursor();
suppressions = [];
for (var i = 0; i<hints.length; i++) {
var data = hints[i];
var start_row = data.start_row;
var start_col = data.start_col;
var end_row = data.end_row;
var end_col = data.end_col;
if (data.suppressIfEditing &&
((cursor.row === start_row && cursor.column == start_col+1)
|| (cursor.row === end_row && (cursor.column+1) == end_col))) {
suppressions.push([start_row, start_col, end_row, end_col]);
continue;
}
var suppress = false;
for (var j = 0; j < suppressions.length; j++) {
var e=suppressions[j];
var fromRow=e[0], fromCol=e[1], toRow=e[2], toCol=e[3];
if (start_row == fromRow && start_col >= fromCol && start_row === toRow && start_col <= toCol) {
suppress = true;
break;
}
}
if(suppress) { continue; };
var key = "(" + start_row + "," + start_col + ")" + ":" + "(" + end_row + "," + end_col + ")";
newRange[key] = data;
annotations.push(data);
}
var newKeys = Object.keys(newRange);
var oldKeys = Object.keys(savedRange);
var changes = 0;
for (i = 0; i < newKeys.length; i++) {
key = newKeys[i];
if (!savedRange[key]) {
var new_range = newRange[key];
var range = new Range(new_range.start_row, 0, new_range.end_row, Infinity);
range.id = session.addMarker(range, "ace_error-marker", "fullLine");
var a = doc.createAnchor(new_range.start_row, new_range.start_col);
var b = doc.createAnchor(new_range.end_row, new_range.end_col);
var range = new Range();
range.start = a;
range.end = b;
range.id = session.addMarker(range, "ace_error-marker", "text");
savedRange[key] = range;
changes++;
}
}
@ -241,11 +294,27 @@ var Mode = function() {
key = oldKeys[i];
if (!newRange[key]) {
range = savedRange[key];
range.start.detach();
range.end.detach();
session.removeMarker(range.id);
delete savedRange[key];
changes++;
}
}
if (changes>0) {
session.setAnnotations(annotations);
};
};
worker.on("lint", function(results) {
hints = results.data;
if (hints.length > 100) {
hints = hints.slice(0, 100); // limit to 100 errors
};
updateMarkers();
});
worker.on("terminate", function() {
var oldKeys = Object.keys(savedRange);
for (var i = 0; i < oldKeys.length; i++) {
@ -254,7 +323,7 @@ var Mode = function() {
session.removeMarker(range.id);
delete savedRange[key];
}
});
return worker;
};
@ -263,7 +332,7 @@ oop.inherits(Mode, TextMode);
(function() {
this.type = "text";
this.lineCommentStart = "%";
this.$id = "ace/mode/latex";

View file

@ -1414,67 +1414,412 @@ var Mirror = require("../worker/mirror").Mirror;
var LatexWorker = exports.LatexWorker = function(sender) {
Mirror.call(this, sender);
this.setTimeout(500);
this.setTimeout(250);
};
oop.inherits(LatexWorker, Mirror);
var Parse = function (text) {
var lines = text.split('\n');
var state = [], errors = [];
var i;
var regex = /^\s*\\(begin|end)\{(\w+\*?)\}/; // keep the regex outside the loop for performance
for (i = 0; i < lines.length; i++) {
var line = lines[i];
var result;
var error = null;
regex.lastIndex = 0;
if ((result = regex.exec(line))) {
var type = result[1];
var env = result[2];
if (type == "begin") {
state.push({"env":env, "row": i});
} else if (type == "end") {
var last_open = state.pop();
if (last_open && last_open.env == env) {
} else if (last_open && last_open.env != env) {
error = {"type":"info", "text": "end " + env + "with begin " + last_open.env, "start_row": last_open.row, "end_row":i };
} else if (!last_open) {
error = {"type":"info", "text": "end without begin " + type, "start_row": 0, "end_row": i};
var errors = [];
var Comments = [];
var Tokens = [];
var Environments = [];
var pos = -1;
var SPECIAL = /[\\\{\}\$\&\#\^\_\~\%]/g;
var CS = /[^a-zA-Z]/g;
var idx = 0;
var lineNumber = 0;
var linePosition = [];
linePosition[0] = 0;
var TokenError = function (token, message) {
var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4];
var start_col = start - linePosition[line];
var end_col = end - linePosition[line] + 1;
errors.push({row: line,
column: start_col,
start_row:line,
start_col: start_col,
end_row:line,
end_col: end_col,
type:"error",
text:message,
suppressIfEditing:true});
};
var TokenErrorFromTo = function (fromToken, toToken, message) {
var fromLine = fromToken[0], fromStart = fromToken[2], fromEnd = fromToken[3];
var toLine = toToken[0], toStart = toToken[2], toEnd = toToken[3];
if (!toEnd) { toEnd = toStart + 1;};
var start_col = fromStart - linePosition[fromLine];
var end_col = toEnd - linePosition[toLine] + 1;
errors.push({row: line,
column: start_col,
start_row: fromLine,
start_col: start_col,
end_row: toLine,
end_col: end_col,
type:"error",
text:message,
suppressIfEditing:true});
};
var EnvErrorFromTo = function (fromEnv, toEnv, message, options) {
if(!options) { options = {} ; };
var fromToken = fromEnv.token, toToken = toEnv.closeToken || toEnv.token;
var fromLine = fromToken[0], fromStart = fromToken[2], fromEnd = fromToken[3], fromSeq = fromToken[4];
if (!toToken) {toToken = fromToken;};
var toLine = toToken[0], toStart = toToken[2], toEnd = toToken[3], toSeq = toToken[4];
if (!toEnd) { toEnd = toStart + 1;};
var start_col = fromStart - linePosition[fromLine];
var end_col = toEnd - linePosition[toLine] + 1;
errors.push({row:toLine,
column:end_col,
start_row:fromLine,
start_col: start_col,
end_row:toLine,
end_col: end_col,
type:"error",
text:message,
suppressIfEditing:options.suppressIfEditing});
};
var EnvErrorTo = function (toEnv, message) {
var token = toEnv.closeToken || toEnv.token;
var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4];
if (!end) { end = start + 1; };
var end_col = end - linePosition[line] + 1;
var err = {row: line,
column: end_col,
start_row:0,
start_col: 0,
end_row: line,
end_col: end_col,
type:"error",
text:message};
errors.push(err);
};
var EnvErrorFrom = function (env, message) {
var token = env.token;
var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4];
var start_col = start - linePosition[line];
var end_col = Infinity;
errors.push({row: line,
column: start_col,
start_row:line,
start_col: start_col,
end_row: lineNumber,
end_col: end_col,
type:"error",
text:message});
};
var checkingDisabled = false;
while (true) {
var result = SPECIAL.exec(text);
if (result == null) {
if (idx < text.length) {
Tokens.push([lineNumber, "Text", idx, text.length]);
}
break;
}
if (result && result.index <= pos) {
break;
};
pos = result.index;
var newIdx = SPECIAL.lastIndex;
if (pos > idx) {
Tokens.push([lineNumber, "Text", idx, pos]);
}
for (var i = idx; i < pos; i++) {
if (text[i] === "\n") {
lineNumber++;
linePosition[lineNumber] = i+1;
}
}
idx = newIdx;
var code = result[0];
if (code === "%") {
var newLinePos = text.indexOf("\n", idx);
if (newLinePos === -1) {
newLinePos = text.length;
};
var commentString = text.substring(idx, newLinePos);
if (commentString.indexOf("%novalidate") === 0) {
return [];
} else if(!checkingDisabled && commentString.indexOf("%begin novalidate") === 0) {
checkingDisabled = true;
} else if (checkingDisabled && commentString.indexOf("%end novalidate") === 0) {
checkingDisabled = false;
};
idx = SPECIAL.lastIndex = newLinePos + 1;
Comments.push([lineNumber, idx, newLinePos]);
lineNumber++;
linePosition[lineNumber] = idx;
} else if (checkingDisabled) {
continue;
} else if (code === '\\') {
CS.lastIndex = idx;
var controlSequence = CS.exec(text);
var nextSpecialPos = controlSequence === null ? idx : controlSequence.index;
if (nextSpecialPos === idx) {
Tokens.push([lineNumber, code, pos, idx + 1, text[idx]]);
idx = SPECIAL.lastIndex = idx + 1;
char = text[nextSpecialPos];
if (char === '\n') { lineNumber++; linePosition[lineNumber] = nextSpecialPos;};
} else {
Tokens.push([lineNumber, code, pos, nextSpecialPos, text.slice(idx, nextSpecialPos)]);
var char;
while ((char = text[nextSpecialPos]) === ' ' || char === '\t' || char === '\r' || char === '\n') {
nextSpecialPos++;
if (char === '\n') { lineNumber++; linePosition[lineNumber] = nextSpecialPos;};
}
idx = SPECIAL.lastIndex = nextSpecialPos;
}
} else if (code === "{") {
Tokens.push([lineNumber, code, pos]);
} else if (code === "}") {
Tokens.push([lineNumber, code, pos]);
} else if (code === "$") {
if (text[idx] === "$") {
idx = SPECIAL.lastIndex = idx + 1;
Tokens.push([lineNumber, "$$", pos]);
} else {
Tokens.push([lineNumber, code, pos]);
}
} else if (code === "&") {
Tokens.push([lineNumber, code, pos]);
} else if (code === "#") {
Tokens.push([lineNumber, code, pos]);
} else if (code === "^") {
Tokens.push([lineNumber, code, pos]);
} else if (code === "_") {
Tokens.push([lineNumber, code, pos]);
} else if (code === "~") {
Tokens.push([lineNumber, code, pos]);
} else {
throw "unrecognised character " + code;
}
}
var read1arg = function (k) {
var open = Tokens[k+1];
var env = Tokens[k+2];
var close = Tokens[k+3];
var envName;
if(open && open[1] === "\\") {
envName = open[4];
return k + 1;
} else if(open && open[1] === "{" && env && env[1] === "\\" && close && close[1] === "}") {
envName = env[4];
return k + 3;
} else {
return null;
}
};
var read1name = function (k) {
var open = Tokens[k+1];
var env = Tokens[k+2];
var close = Tokens[k+3];
if(open && open[1] === "{" && env && env[1] === "Text" && close && close[1] === "}") {
var envName = text.substring(env[2], env[3]);
return k + 3;
} else {
return null;
}
};
var readOptionalParams = function(k) {
var params = Tokens[k+1];
if(params && params[1] === "Text") {
var paramNum = text.substring(params[2], params[3]);
if (paramNum.match(/^\[\d+\]$/)) {
return k + 1;
};
};
if (error) {
errors.push(error);
};
return null;
};
if (state.length > 0) {
for (i = 0; i < state.length; i++) {
var unclosed = state[i];
error = {"type":"info", "text": "begin without end " + unclosed.type, "start_row": unclosed.row, "end_row": lines.length-1};
errors.push(error);
};
var readDefinition = function(k) {
k = k + 1;
var count = 0;
var nextToken = Tokens[k];
while (nextToken && nextToken[1] === "Text") {
var start = nextToken[2], end = nextToken[3];
for (i = start; i < end; i++) {
var char = text[i];
if (char === ' ' || char === '\t' || char === '\r' || char === '\n') { continue; }
return null;
}
k++;
nextToken = Tokens[k];
}
if (nextToken && nextToken[1] === "{") {
count++;
while (count>0) {
k++;
nextToken = Tokens[k];
if(!nextToken) { break; };
if (nextToken[1] === "}") { count--; }
if (nextToken[1] === "{") { count++; }
}
return k;
}
return null;
};
if (errors.length) {
return [errors[0]];
};
for (var _j = 0, _len = Tokens.length; _j < _len; _j++) {
var token = Tokens[_j];
var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4];
if (type === "\\") {
if (seq === "begin" || seq === "end") {
var open = Tokens[_j+1];
var env = Tokens[_j+2];
var close = Tokens[_j+3];
if(open && open[1] === "{" && env && env[1] === "Text" && close && close[1] === "}") {
var envName = text.substring(env[2], env[3]);
Environments.push({command: seq, name: envName, token: token, closeToken: close});
_j = _j + 3; // advance past these tokens
} else {
var endToken = null;
if (open && open[1] === "{") {
endToken = open;
if (env && env[1] === "Text") {
endToken = env.slice();
start = endToken[2]; end = endToken[3];
for (i = start; i < end; i++) {
char = text[i];
if (char === ' ' || char === '\t' || char === '\r' || char === '\n') { break; }
}
endToken[3] = i;
};
};
if (endToken) {
TokenErrorFromTo(token, endToken, "invalid environment command" + text.substring(token[2], endToken[3] || endToken[2]));
} else {
TokenError(token, "invalid environment command");
};
}
} else if (seq === "newcommand" || seq === "renewcommand" || seq === "def" || seq === "DeclareRobustCommand") {
var newPos = read1arg(_j);
if (newPos === null) { continue; } else {_j = newPos;};
newPos = readOptionalParams(_j);
if (newPos === null) { /* do nothing */ } else {_j = newPos;};
newPos = readDefinition(_j);
if (newPos === null) { /* do nothing */ } else {_j = newPos;};
} else if (seq === "newenvironment") {
newPos = read1name(_j);
if (newPos === null) { continue; } else {_j = newPos;};
newPos = readOptionalParams(_j);
if (newPos === null) { /* do nothing */ } else {_j = newPos;};
newPos = readDefinition(_j);
if (newPos === null) { /* do nothing */ } else {_j = newPos;};
newPos = readDefinition(_j);
if (newPos === null) { /* do nothing */ } else {_j = newPos;};
}
} else if (type === "{") {
Environments.push({command:"{", token:token});
} else if (type === "}") {
Environments.push({command:"}", token:token});
};
if ((start != null) && (end != null) && (seq != null)) {
} else if ((start != null) && (end != null)) {
} else if (start != null) {
} else {
}
}
var state = [];
for (i = 0; i < Environments.length; i++) {
var thisEnv = Environments[i];
if(thisEnv.command === "begin" || thisEnv.command === "{") {
state.push(thisEnv);
} else if (thisEnv.command === "end" || thisEnv.command === "}") {
var lastEnv = state.pop();
if (!lastEnv) {
if (thisEnv.command === "}") {
EnvErrorTo(thisEnv, "unexpected end group }");
} else if (thisEnv.command === "end") {
EnvErrorTo(thisEnv, "unexpected \\end{" + thisEnv.name + "}");
}
} else if (lastEnv.command === "{" && thisEnv.command === "}") {
continue; // closed group correctly
} else if (lastEnv.name === thisEnv.name) {
continue; // closed environment correctly
} else if (thisEnv.command === "}") {
EnvErrorFromTo(lastEnv, thisEnv, "unexpected end group } after \\begin{" + lastEnv.name +"}");
state.push(lastEnv);
} else if (lastEnv.command === "{" && thisEnv.command === "end") {
EnvErrorFromTo(lastEnv, thisEnv, "unexpected \\end{" + thisEnv.name + "} inside group {", {suppressIfEditing:true});
i--;
} else if (lastEnv.command === "begin" && thisEnv.command === "end") {
EnvErrorFromTo(lastEnv, thisEnv, "unexpected \\end{" + thisEnv.name + "} after \\begin{" + lastEnv.name + "}");
for (var j = i + 1; j < Environments.length; j++) {
var futureEnv = Environments[j];
if (futureEnv.command === "end" && futureEnv.name === lastEnv.name) {
state.push(lastEnv);
continue;
}
}
lastEnv = state.pop();
if(lastEnv) {
if (thisEnv.name === lastEnv.name) {
continue;
} else {
state.push(lastEnv);
}
}
}
}
}
while (state.length > 0) {
thisEnv = state.pop();
if (thisEnv.command === "{") {
EnvErrorFrom(thisEnv, "unclosed group {");
} else if (thisEnv.command === "begin") {
EnvErrorFrom(thisEnv, "unclosed environment \\begin{" + thisEnv.name + "}");
};
}
return errors;
};
(function() {
var disabled = false;
this.onUpdate = function() {
var value = this.doc.getValue();
var errors = [];
if (disabled) { return ; };
var value = this.doc.getValue();
var errors = [];
try {
if (value)
errors = Parse(value);
} catch (e) {
console.log("latex worker error",e);
}
this.sender.emit("lint", errors);
if (value)
errors = Parse(value);
} catch (e) {
disabled = true;
errors = [];
}
this.sender.emit("lint", errors);
};
}).call(LatexWorker.prototype);