lint.js (8530B)
1 // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 // Distributed under an MIT license: http://codemirror.net/LICENSE 3 4 (function(mod) { 5 if (typeof exports == "object" && typeof module == "object") // CommonJS 6 mod(require("../../lib/codemirror")); 7 else if (typeof define == "function" && define.amd) // AMD 8 define(["../../lib/codemirror"], mod); 9 else // Plain browser env 10 mod(CodeMirror); 11 })(function(CodeMirror) { 12 "use strict"; 13 var GUTTER_ID = "CodeMirror-lint-markers"; 14 15 function showTooltip(e, content) { 16 var tt = document.createElement("div"); 17 tt.className = "CodeMirror-lint-tooltip"; 18 tt.appendChild(content.cloneNode(true)); 19 document.body.appendChild(tt); 20 21 function position(e) { 22 if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position); 23 tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + "px"; 24 tt.style.left = (e.clientX + 5) + "px"; 25 } 26 CodeMirror.on(document, "mousemove", position); 27 position(e); 28 if (tt.style.opacity != null) tt.style.opacity = 1; 29 return tt; 30 } 31 function rm(elt) { 32 if (elt.parentNode) elt.parentNode.removeChild(elt); 33 } 34 function hideTooltip(tt) { 35 if (!tt.parentNode) return; 36 if (tt.style.opacity == null) rm(tt); 37 tt.style.opacity = 0; 38 setTimeout(function() { rm(tt); }, 600); 39 } 40 41 function showTooltipFor(e, content, node) { 42 var tooltip = showTooltip(e, content); 43 function hide() { 44 CodeMirror.off(node, "mouseout", hide); 45 if (tooltip) { hideTooltip(tooltip); tooltip = null; } 46 } 47 var poll = setInterval(function() { 48 if (tooltip) for (var n = node;; n = n.parentNode) { 49 if (n && n.nodeType == 11) n = n.host; 50 if (n == document.body) return; 51 if (!n) { hide(); break; } 52 } 53 if (!tooltip) return clearInterval(poll); 54 }, 400); 55 CodeMirror.on(node, "mouseout", hide); 56 } 57 58 function LintState(cm, options, hasGutter) { 59 this.marked = []; 60 this.options = options; 61 this.timeout = null; 62 this.hasGutter = hasGutter; 63 this.onMouseOver = function(e) { onMouseOver(cm, e); }; 64 this.waitingFor = 0 65 } 66 67 function parseOptions(_cm, options) { 68 if (options instanceof Function) return {getAnnotations: options}; 69 if (!options || options === true) options = {}; 70 return options; 71 } 72 73 function clearMarks(cm) { 74 var state = cm.state.lint; 75 if (state.hasGutter) cm.clearGutter(GUTTER_ID); 76 for (var i = 0; i < state.marked.length; ++i) 77 state.marked[i].clear(); 78 state.marked.length = 0; 79 } 80 81 function makeMarker(labels, severity, multiple, tooltips) { 82 var marker = document.createElement("div"), inner = marker; 83 marker.className = "CodeMirror-lint-marker-" + severity; 84 if (multiple) { 85 inner = marker.appendChild(document.createElement("div")); 86 inner.className = "CodeMirror-lint-marker-multiple"; 87 } 88 89 if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) { 90 showTooltipFor(e, labels, inner); 91 }); 92 93 return marker; 94 } 95 96 function getMaxSeverity(a, b) { 97 if (a == "error") return a; 98 else return b; 99 } 100 101 function groupByLine(annotations) { 102 var lines = []; 103 for (var i = 0; i < annotations.length; ++i) { 104 var ann = annotations[i], line = ann.from.line; 105 (lines[line] || (lines[line] = [])).push(ann); 106 } 107 return lines; 108 } 109 110 function annotationTooltip(ann) { 111 var severity = ann.severity; 112 if (!severity) severity = "error"; 113 var tip = document.createElement("div"); 114 tip.className = "CodeMirror-lint-message-" + severity; 115 if (typeof ann.messageHTML != 'undefined') { 116 tip.innerHTML = ann.messageHTML; 117 } else { 118 tip.appendChild(document.createTextNode(ann.message)); 119 } 120 return tip; 121 } 122 123 function lintAsync(cm, getAnnotations, passOptions) { 124 var state = cm.state.lint 125 var id = ++state.waitingFor 126 function abort() { 127 id = -1 128 cm.off("change", abort) 129 } 130 cm.on("change", abort) 131 getAnnotations(cm.getValue(), function(annotations, arg2) { 132 cm.off("change", abort) 133 if (state.waitingFor != id) return 134 if (arg2 && annotations instanceof CodeMirror) annotations = arg2 135 updateLinting(cm, annotations) 136 }, passOptions, cm); 137 } 138 139 function startLinting(cm) { 140 var state = cm.state.lint, options = state.options; 141 /* 142 * Passing rules in `options` property prevents JSHint (and other linters) from complaining 143 * about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc. 144 */ 145 var passOptions = options.options || options; 146 var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint"); 147 if (!getAnnotations) return; 148 if (options.async || getAnnotations.async) { 149 lintAsync(cm, getAnnotations, passOptions) 150 } else { 151 var annotations = getAnnotations(cm.getValue(), passOptions, cm); 152 if (!annotations) return; 153 if (annotations.then) annotations.then(function(issues) { 154 updateLinting(cm, issues); 155 }); 156 else updateLinting(cm, annotations); 157 } 158 } 159 160 function updateLinting(cm, annotationsNotSorted) { 161 clearMarks(cm); 162 var state = cm.state.lint, options = state.options; 163 164 var annotations = groupByLine(annotationsNotSorted); 165 166 for (var line = 0; line < annotations.length; ++line) { 167 var anns = annotations[line]; 168 if (!anns) continue; 169 170 var maxSeverity = null; 171 var tipLabel = state.hasGutter && document.createDocumentFragment(); 172 173 for (var i = 0; i < anns.length; ++i) { 174 var ann = anns[i]; 175 var severity = ann.severity; 176 if (!severity) severity = "error"; 177 maxSeverity = getMaxSeverity(maxSeverity, severity); 178 179 if (options.formatAnnotation) ann = options.formatAnnotation(ann); 180 if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann)); 181 182 if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, { 183 className: "CodeMirror-lint-mark-" + severity, 184 __annotation: ann 185 })); 186 } 187 188 if (state.hasGutter) 189 cm.setGutterMarker(line, GUTTER_ID, makeMarker(tipLabel, maxSeverity, anns.length > 1, 190 state.options.tooltips)); 191 } 192 if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm); 193 } 194 195 function onChange(cm) { 196 var state = cm.state.lint; 197 if (!state) return; 198 clearTimeout(state.timeout); 199 state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay || 500); 200 } 201 202 function popupTooltips(annotations, e) { 203 var target = e.target || e.srcElement; 204 var tooltip = document.createDocumentFragment(); 205 for (var i = 0; i < annotations.length; i++) { 206 var ann = annotations[i]; 207 tooltip.appendChild(annotationTooltip(ann)); 208 } 209 showTooltipFor(e, tooltip, target); 210 } 211 212 function onMouseOver(cm, e) { 213 var target = e.target || e.srcElement; 214 if (!/\bCodeMirror-lint-mark-/.test(target.className)) return; 215 var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2; 216 var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client")); 217 218 var annotations = []; 219 for (var i = 0; i < spans.length; ++i) { 220 var ann = spans[i].__annotation; 221 if (ann) annotations.push(ann); 222 } 223 if (annotations.length) popupTooltips(annotations, e); 224 } 225 226 CodeMirror.defineOption("lint", false, function(cm, val, old) { 227 if (old && old != CodeMirror.Init) { 228 clearMarks(cm); 229 if (cm.state.lint.options.lintOnChange !== false) 230 cm.off("change", onChange); 231 CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver); 232 clearTimeout(cm.state.lint.timeout); 233 delete cm.state.lint; 234 } 235 236 if (val) { 237 var gutters = cm.getOption("gutters"), hasLintGutter = false; 238 for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true; 239 var state = cm.state.lint = new LintState(cm, parseOptions(cm, val), hasLintGutter); 240 if (state.options.lintOnChange !== false) 241 cm.on("change", onChange); 242 if (state.options.tooltips != false && state.options.tooltips != "gutter") 243 CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver); 244 245 startLinting(cm); 246 } 247 }); 248 249 CodeMirror.defineExtension("performLint", function() { 250 if (this.state.lint) startLinting(this); 251 }); 252 });