##// END OF EJS Templates
Added server side file editing with commit
marcink -
r1305:166317d4 beta
parent child Browse files
Show More
@@ -0,0 +1,54 b''
1 .CodeMirror {
2 overflow: auto;
3 height: 450px;
4 line-height: 1em;
5 font-family: monospace;
6 _position: relative; /* IE6 hack */
7 }
8
9 .CodeMirror-gutter {
10 position: absolute; left: 0; top: 0;
11 background-color: #f7f7f7;
12 border-right: 1px solid #eee;
13 min-width: 2em;
14 height: 100%;
15 }
16 .CodeMirror-gutter-text {
17 color: #aaa;
18 text-align: right;
19 padding: .4em .2em .4em .4em;
20 }
21 .CodeMirror-lines {
22 padding: .4em;
23 }
24
25 .CodeMirror pre {
26 -moz-border-radius: 0;
27 -webkit-border-radius: 0;
28 -o-border-radius: 0;
29 border-radius: 0;
30 border-width: 0; margin: 0; padding: 0; background: transparent;
31 font-family: inherit;
32 }
33
34 .CodeMirror-cursor {
35 z-index: 10;
36 position: absolute;
37 visibility: hidden;
38 border-left: 1px solid black !important;
39 }
40 .CodeMirror-focused .CodeMirror-cursor {
41 visibility: visible;
42 }
43
44 span.CodeMirror-selected {
45 background: #ccc !important;
46 color: HighlightText !important;
47 }
48 .CodeMirror-focused span.CodeMirror-selected {
49 background: Highlight !important;
50 }
51
52 .CodeMirror-matchingbracket {color: #0f0 !important;}
53 .CodeMirror-nonmatchingbracket {color: #f22 !important;}
54 .CodeMirror-gutter-text{color: #003367 !important;} No newline at end of file
This diff has been collapsed as it changes many lines, (1915 lines changed) Show them Hide them
@@ -0,0 +1,1915 b''
1 // All functions that need access to the editor's state live inside
2 // the CodeMirror function. Below that, at the bottom of the file,
3 // some utilities are defined.
4
5 // CodeMirror is the only global var we claim
6 var CodeMirror = (function() {
7 // This is the function that produces an editor instance. It's
8 // closure is used to store the editor state.
9 function CodeMirror(place, givenOptions) {
10 // Determine effective options based on given values and defaults.
11 var options = {}, defaults = CodeMirror.defaults;
12 for (var opt in defaults)
13 if (defaults.hasOwnProperty(opt))
14 options[opt] = (givenOptions && givenOptions.hasOwnProperty(opt) ? givenOptions : defaults)[opt];
15
16 // The element in which the editor lives. Takes care of scrolling
17 // (if enabled).
18 var wrapper = document.createElement("div");
19 wrapper.className = "CodeMirror";
20 // This mess creates the base DOM structure for the editor.
21 wrapper.innerHTML =
22 '<div style="position: relative">' + // Set to the height of the text, causes scrolling
23 '<pre style="position: relative; height: 0; visibility: hidden; overflow: hidden;">' + // To measure line/char size
24 '<span>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</span></pre>' +
25 '<div style="position: relative">' + // Moved around its parent to cover visible view
26 '<div class="CodeMirror-gutter"><div class="CodeMirror-gutter-text"></div></div>' +
27 '<div style="overflow: hidden; position: absolute; width: 0; left: 0">' + // Wraps and hides input textarea
28 '<textarea style="height: 1px; position: absolute; width: 1px;" wrap="off"></textarea></div>' +
29 // Provides positioning relative to (visible) text origin
30 '<div class="CodeMirror-lines"><div style="position: relative">' +
31 '<pre class="CodeMirror-cursor">&#160;</pre>' + // Absolutely positioned blinky cursor
32 '<div></div></div></div></div></div>'; // This DIV contains the actual code
33 if (place.appendChild) place.appendChild(wrapper); else place(wrapper);
34 // I've never seen more elegant code in my life.
35 var code = wrapper.firstChild, measure = code.firstChild, mover = measure.nextSibling,
36 gutter = mover.firstChild, gutterText = gutter.firstChild,
37 inputDiv = gutter.nextSibling, input = inputDiv.firstChild,
38 lineSpace = inputDiv.nextSibling.firstChild, cursor = lineSpace.firstChild, lineDiv = cursor.nextSibling;
39 if (options.tabindex != null) input.tabindex = options.tabindex;
40 if (!options.gutter && !options.lineNumbers) gutter.style.display = "none";
41
42 // Delayed object wrap timeouts, making sure only one is active. blinker holds an interval.
43 var poll = new Delayed(), highlight = new Delayed(), blinker;
44
45 // mode holds a mode API object. lines an array of Line objects
46 // (see Line constructor), work an array of lines that should be
47 // parsed, and history the undo history (instance of History
48 // constructor).
49 var mode, lines = [new Line("")], work, history = new History(), focused;
50 loadMode();
51 // The selection. These are always maintained to point at valid
52 // positions. Inverted is used to remember that the user is
53 // selecting bottom-to-top.
54 var sel = {from: {line: 0, ch: 0}, to: {line: 0, ch: 0}, inverted: false};
55 // Selection-related flags. shiftSelecting obviously tracks
56 // whether the user is holding shift. reducedSelection is a hack
57 // to get around the fact that we can't create inverted
58 // selections. See below.
59 var shiftSelecting, reducedSelection;
60 // Variables used by startOperation/endOperation to track what
61 // happened during the operation.
62 var updateInput, changes, textChanged, selectionChanged, leaveInputAlone;
63 // Current visible range (may be bigger than the view window).
64 var showingFrom = 0, showingTo = 0, lastHeight = 0, curKeyId = null;
65 // editing will hold an object describing the things we put in the
66 // textarea, to help figure out whether something changed.
67 // bracketHighlighted is used to remember that a backet has been
68 // marked.
69 var editing, bracketHighlighted;
70
71 // Initialize the content. Somewhat hacky (delayed prepareInput)
72 // to work around browser issues.
73 operation(function(){setValue(options.value || ""); updateInput = false;})();
74 setTimeout(prepareInput, 20);
75
76 // Register our event handlers.
77 connect(wrapper, "mousedown", operation(onMouseDown));
78 // Gecko browsers fire contextmenu *after* opening the menu, at
79 // which point we can't mess with it anymore. Context menu is
80 // handled in onMouseDown for Gecko.
81 if (!gecko) connect(wrapper, "contextmenu", operation(onContextMenu));
82 connect(code, "dblclick", operation(onDblClick));
83 connect(wrapper, "scroll", function() {updateDisplay([]); if (options.onScroll) options.onScroll(instance);});
84 connect(window, "resize", function() {updateDisplay(true);});
85 connect(input, "keyup", operation(onKeyUp));
86 connect(input, "keydown", operation(onKeyDown));
87 connect(input, "keypress", operation(onKeyPress));
88 connect(input, "focus", onFocus);
89 connect(input, "blur", onBlur);
90
91 connect(wrapper, "dragenter", function(e){e.stop();});
92 connect(wrapper, "dragover", function(e){e.stop();});
93 connect(wrapper, "drop", operation(onDrop));
94 connect(wrapper, "paste", function(){input.focus(); fastPoll();});
95 connect(input, "paste", function(){fastPoll();});
96 connect(input, "cut", function(){fastPoll();});
97
98 if (document.activeElement == input) onFocus();
99 else onBlur();
100
101 function isLine(l) {return l >= 0 && l < lines.length;}
102 // The instance object that we'll return. Mostly calls out to
103 // local functions in the CodeMirror function. Some do some extra
104 // range checking and/or clipping. operation is used to wrap the
105 // call so that changes it makes are tracked, and the display is
106 // updated afterwards.
107 var instance = {
108 getValue: getValue,
109 setValue: operation(setValue),
110 getSelection: getSelection,
111 replaceSelection: operation(replaceSelection),
112 focus: function(){input.focus(); onFocus(); fastPoll();},
113 setOption: function(option, value) {
114 options[option] = value;
115 if (option == "lineNumbers" || option == "gutter") gutterChanged();
116 else if (option == "mode" || option == "indentUnit") loadMode();
117 },
118 getOption: function(option) {return options[option];},
119 undo: operation(undo),
120 redo: operation(redo),
121 indentLine: operation(function(n) {if (isLine(n)) indentLine(n, "smart");}),
122 historySize: function() {return {undo: history.done.length, redo: history.undone.length};},
123 matchBrackets: operation(function(){matchBrackets(true);}),
124 getTokenAt: function(pos) {
125 pos = clipPos(pos);
126 return lines[pos.line].getTokenAt(mode, getStateBefore(pos.line), pos.ch);
127 },
128 cursorCoords: function(start){
129 if (start == null) start = sel.inverted;
130 return pageCoords(start ? sel.from : sel.to);
131 },
132 charCoords: function(pos){return pageCoords(clipPos(pos));},
133 coordsChar: function(coords) {
134 var off = eltOffset(lineSpace);
135 var line = Math.min(showingTo - 1, showingFrom + Math.floor(coords.y / lineHeight()));
136 return clipPos({line: line, ch: charFromX(clipLine(line), coords.x)});
137 },
138 getSearchCursor: function(query, pos, caseFold) {return new SearchCursor(query, pos, caseFold);},
139 markText: operation(function(a, b, c){return operation(markText(a, b, c));}),
140 setMarker: addGutterMarker,
141 clearMarker: removeGutterMarker,
142 setLineClass: operation(setLineClass),
143 lineInfo: lineInfo,
144 addWidget: function(pos, node, scroll) {
145 var pos = localCoords(clipPos(pos), true);
146 node.style.top = (showingFrom * lineHeight() + pos.yBot + paddingTop()) + "px";
147 node.style.left = (pos.x + paddingLeft()) + "px";
148 code.appendChild(node);
149 if (scroll)
150 scrollIntoView(pos.x, pos.yBot, pos.x + node.offsetWidth, pos.yBot + node.offsetHeight);
151 },
152
153 lineCount: function() {return lines.length;},
154 getCursor: function(start) {
155 if (start == null) start = sel.inverted;
156 return copyPos(start ? sel.from : sel.to);
157 },
158 somethingSelected: function() {return !posEq(sel.from, sel.to);},
159 setCursor: operation(function(line, ch) {
160 if (ch == null && typeof line.line == "number") setCursor(line.line, line.ch);
161 else setCursor(line, ch);
162 }),
163 setSelection: operation(function(from, to) {setSelection(clipPos(from), clipPos(to || from));}),
164 getLine: function(line) {if (isLine(line)) return lines[line].text;},
165 setLine: operation(function(line, text) {
166 if (isLine(line)) replaceRange(text, {line: line, ch: 0}, {line: line, ch: lines[line].text.length});
167 }),
168 removeLine: operation(function(line) {
169 if (isLine(line)) replaceRange("", {line: line, ch: 0}, clipPos({line: line+1, ch: 0}));
170 }),
171 replaceRange: operation(replaceRange),
172 getRange: function(from, to) {return getRange(clipPos(from), clipPos(to));},
173
174 operation: function(f){return operation(f)();},
175 refresh: function(){updateDisplay(true);},
176 getInputField: function(){return input;},
177 getWrapperElement: function(){return wrapper;}
178 };
179
180 function setValue(code) {
181 history = null;
182 var top = {line: 0, ch: 0};
183 updateLines(top, {line: lines.length - 1, ch: lines[lines.length-1].text.length},
184 splitLines(code), top, top);
185 history = new History();
186 }
187 function getValue(code) {
188 var text = [];
189 for (var i = 0, l = lines.length; i < l; ++i)
190 text.push(lines[i].text);
191 return text.join("\n");
192 }
193
194 function onMouseDown(e) {
195 // First, see if this is a click in the gutter
196 for (var n = e.target(); n != wrapper; n = n.parentNode)
197 if (n.parentNode == gutterText) {
198 if (options.onGutterClick)
199 options.onGutterClick(instance, indexOf(gutterText.childNodes, n) + showingFrom);
200 return e.stop();
201 }
202
203 if (gecko && e.button() == 3) onContextMenu(e);
204 if (e.button() != 1) return;
205 // For button 1, if it was clicked inside the editor
206 // (posFromMouse returning non-null), we have to adjust the
207 // selection.
208 var start = posFromMouse(e), last = start, going;
209 if (!start) {if (e.target() == wrapper) e.stop(); return;}
210 setCursor(start.line, start.ch, false);
211
212 if (!focused) onFocus();
213 e.stop();
214 // And then we have to see if it's a drag event, in which case
215 // the dragged-over text must be selected.
216 function end() {
217 input.focus();
218 updateInput = true;
219 move(); up();
220 }
221 function extend(e) {
222 var cur = posFromMouse(e, true);
223 if (cur && !posEq(cur, last)) {
224 if (!focused) onFocus();
225 last = cur;
226 setSelection(start, cur);
227 updateInput = false;
228 var visible = visibleLines();
229 if (cur.line >= visible.to || cur.line < visible.from)
230 going = setTimeout(operation(function(){extend(e);}), 150);
231 }
232 }
233
234 var move = connect(document, "mousemove", operation(function(e) {
235 clearTimeout(going);
236 e.stop();
237 extend(e);
238 }), true);
239 var up = connect(document, "mouseup", operation(function(e) {
240 clearTimeout(going);
241 var cur = posFromMouse(e);
242 if (cur) setSelection(start, cur);
243 e.stop();
244 end();
245 }), true);
246 }
247 function onDblClick(e) {
248 var pos = posFromMouse(e);
249 if (!pos) return;
250 selectWordAt(pos);
251 e.stop();
252 }
253 function onDrop(e) {
254 var pos = posFromMouse(e, true), files = e.e.dataTransfer.files;
255 if (!pos || options.readOnly) return;
256 if (files && files.length && window.FileReader && window.File) {
257 var n = files.length, text = Array(n), read = 0;
258 for (var i = 0; i < n; ++i) loadFile(files[i], i);
259 function loadFile(file, i) {
260 var reader = new FileReader;
261 reader.onload = function() {
262 text[i] = reader.result;
263 if (++read == n) replaceRange(text.join(""), clipPos(pos), clipPos(pos));
264 };
265 reader.readAsText(file);
266 }
267 }
268 else {
269 try {
270 var text = e.e.dataTransfer.getData("Text");
271 if (text) replaceRange(text, pos, pos);
272 }
273 catch(e){}
274 }
275 }
276 function onKeyDown(e) {
277 if (!focused) onFocus();
278
279 var code = e.e.keyCode;
280 // Tries to detect ctrl on non-mac, cmd on mac.
281 var mod = (mac ? e.e.metaKey : e.e.ctrlKey) && !e.e.altKey, anyMod = e.e.ctrlKey || e.e.altKey || e.e.metaKey;
282 if (code == 16 || e.e.shiftKey) shiftSelecting = shiftSelecting || (sel.inverted ? sel.to : sel.from);
283 else shiftSelecting = null;
284 // First give onKeyEvent option a chance to handle this.
285 if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e.e))) return;
286
287 if (code == 33 || code == 34) {scrollPage(code == 34); return e.stop();} // page up/down
288 if (mod && (code == 36 || code == 35)) {scrollEnd(code == 36); return e.stop();} // ctrl-home/end
289 if (mod && code == 65) {selectAll(); return e.stop();} // ctrl-a
290 if (!options.readOnly) {
291 if (!anyMod && code == 13) {return;} // enter
292 if (!anyMod && code == 9 && handleTab(e.e.shiftKey)) return e.stop(); // tab
293 if (mod && code == 90) {undo(); return e.stop();} // ctrl-z
294 if (mod && ((e.e.shiftKey && code == 90) || code == 89)) {redo(); return e.stop();} // ctrl-shift-z, ctrl-y
295 }
296
297 // Key id to use in the movementKeys map. We also pass it to
298 // fastPoll in order to 'self learn'. We need this because
299 // reducedSelection, the hack where we collapse the selection to
300 // its start when it is inverted and a movement key is pressed
301 // (and later restore it again), shouldn't be used for
302 // non-movement keys.
303 curKeyId = (mod ? "c" : "") + code;
304 if (sel.inverted && movementKeys.hasOwnProperty(curKeyId)) {
305 var range = selRange(input);
306 if (range) {
307 reducedSelection = {anchor: range.start};
308 setSelRange(input, range.start, range.start);
309 }
310 }
311 fastPoll(curKeyId);
312 }
313 function onKeyUp(e) {
314 if (reducedSelection) {
315 reducedSelection = null;
316 updateInput = true;
317 }
318 if (e.e.keyCode == 16) shiftSelecting = null;
319 }
320 function onKeyPress(e) {
321 if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e.e))) return;
322 if (options.electricChars && mode.electricChars) {
323 var ch = String.fromCharCode(e.e.charCode == null ? e.e.keyCode : e.e.charCode);
324 if (mode.electricChars.indexOf(ch) > -1)
325 setTimeout(operation(function() {indentLine(sel.to.line, "smart");}), 50);
326 }
327 var code = e.e.keyCode;
328 // Re-stop tab and enter. Necessary on some browsers.
329 if (code == 13) {handleEnter(); e.stop();}
330 else if (code == 9 && options.tabMode != "default") e.stop();
331 else fastPoll(curKeyId);
332 }
333
334 function onFocus() {
335 if (!focused && options.onFocus) options.onFocus(instance);
336 focused = true;
337 slowPoll();
338 if (wrapper.className.search(/\bCodeMirror-focused\b/) == -1)
339 wrapper.className += " CodeMirror-focused";
340 restartBlink();
341 }
342 function onBlur() {
343 if (focused && options.onBlur) options.onBlur(instance);
344 clearInterval(blinker);
345 shiftSelecting = null;
346 focused = false;
347 wrapper.className = wrapper.className.replace(" CodeMirror-focused", "");
348 }
349
350 // Replace the range from from to to by the strings in newText.
351 // Afterwards, set the selection to selFrom, selTo.
352 function updateLines(from, to, newText, selFrom, selTo) {
353 if (history) {
354 var old = [];
355 for (var i = from.line, e = to.line + 1; i < e; ++i) old.push(lines[i].text);
356 history.addChange(from.line, newText.length, old);
357 while (history.done.length > options.undoDepth) history.done.shift();
358 }
359 updateLinesNoUndo(from, to, newText, selFrom, selTo);
360 }
361 function unredoHelper(from, to) {
362 var change = from.pop();
363 if (change) {
364 var replaced = [], end = change.start + change.added;
365 for (var i = change.start; i < end; ++i) replaced.push(lines[i].text);
366 to.push({start: change.start, added: change.old.length, old: replaced});
367 var pos = clipPos({line: change.start + change.old.length - 1,
368 ch: editEnd(replaced[replaced.length-1], change.old[change.old.length-1])});
369 updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: lines[end-1].text.length}, change.old, pos, pos);
370 }
371 }
372 function undo() {unredoHelper(history.done, history.undone);}
373 function redo() {unredoHelper(history.undone, history.done);}
374
375 function updateLinesNoUndo(from, to, newText, selFrom, selTo) {
376 var nlines = to.line - from.line, firstLine = lines[from.line], lastLine = lines[to.line];
377 // First adjust the line structure, taking some care to leave highlighting intact.
378 if (firstLine == lastLine) {
379 if (newText.length == 1)
380 firstLine.replace(from.ch, to.ch, newText[0]);
381 else {
382 lastLine = firstLine.split(to.ch, newText[newText.length-1]);
383 var spliceargs = [from.line + 1, nlines];
384 firstLine.replace(from.ch, firstLine.text.length, newText[0]);
385 for (var i = 1, e = newText.length - 1; i < e; ++i) spliceargs.push(new Line(newText[i]));
386 spliceargs.push(lastLine);
387 lines.splice.apply(lines, spliceargs);
388 }
389 }
390 else if (newText.length == 1) {
391 firstLine.replace(from.ch, firstLine.text.length, newText[0] + lastLine.text.slice(to.ch));
392 lines.splice(from.line + 1, nlines);
393 }
394 else {
395 var spliceargs = [from.line + 1, nlines - 1];
396 firstLine.replace(from.ch, firstLine.text.length, newText[0]);
397 lastLine.replace(0, to.ch, newText[newText.length-1]);
398 for (var i = 1, e = newText.length - 1; i < e; ++i) spliceargs.push(new Line(newText[i]));
399 lines.splice.apply(lines, spliceargs);
400 }
401
402 // Add these lines to the work array, so that they will be
403 // highlighted. Adjust work lines if lines were added/removed.
404 var newWork = [], lendiff = newText.length - nlines - 1;
405 for (var i = 0, l = work.length; i < l; ++i) {
406 var task = work[i];
407 if (task < from.line) newWork.push(task);
408 else if (task > to.line) newWork.push(task + lendiff);
409 }
410 if (newText.length) newWork.push(from.line);
411 work = newWork;
412 startWorker(100);
413 // Remember that these lines changed, for updating the display
414 changes.push({from: from.line, to: to.line + 1, diff: lendiff});
415 textChanged = true;
416
417 // Update the selection
418 function updateLine(n) {return n <= Math.min(to.line, to.line + lendiff) ? n : n + lendiff;}
419 setSelection(selFrom, selTo, updateLine(sel.from.line), updateLine(sel.to.line));
420
421 // Make sure the scroll-size div has the correct height.
422 code.style.height = (lines.length * lineHeight() + 2 * paddingTop()) + "px";
423 }
424
425 function replaceRange(code, from, to) {
426 from = clipPos(from);
427 if (!to) to = from; else to = clipPos(to);
428 code = splitLines(code);
429 function adjustPos(pos) {
430 if (posLess(pos, from)) return pos;
431 if (!posLess(to, pos)) return end;
432 var line = pos.line + code.length - (to.line - from.line) - 1;
433 var ch = pos.ch;
434 if (pos.line == to.line)
435 ch += code[code.length-1].length - (to.ch - (to.line == from.line ? from.ch : 0));
436 return {line: line, ch: ch};
437 }
438 var end;
439 replaceRange1(code, from, to, function(end1) {
440 end = end1;
441 return {from: adjustPos(sel.from), to: adjustPos(sel.to)};
442 });
443 return end;
444 }
445 function replaceSelection(code, collapse) {
446 replaceRange1(splitLines(code), sel.from, sel.to, function(end) {
447 if (collapse == "end") return {from: end, to: end};
448 else if (collapse == "start") return {from: sel.from, to: sel.from};
449 else return {from: sel.from, to: end};
450 });
451 }
452 function replaceRange1(code, from, to, computeSel) {
453 var endch = code.length == 1 ? code[0].length + from.ch : code[code.length-1].length;
454 var newSel = computeSel({line: from.line + code.length - 1, ch: endch});
455 updateLines(from, to, code, newSel.from, newSel.to);
456 }
457
458 function getRange(from, to) {
459 var l1 = from.line, l2 = to.line;
460 if (l1 == l2) return lines[l1].text.slice(from.ch, to.ch);
461 var code = [lines[l1].text.slice(from.ch)];
462 for (var i = l1 + 1; i < l2; ++i) code.push(lines[i].text);
463 code.push(lines[l2].text.slice(0, to.ch));
464 return code.join("\n");
465 }
466 function getSelection() {
467 return getRange(sel.from, sel.to);
468 }
469
470 var pollingFast = false; // Ensures slowPoll doesn't cancel fastPoll
471 function slowPoll() {
472 if (pollingFast) return;
473 poll.set(2000, function() {
474 startOperation();
475 readInput();
476 if (focused) slowPoll();
477 endOperation();
478 });
479 }
480 function fastPoll(keyId) {
481 var missed = false;
482 pollingFast = true;
483 function p() {
484 startOperation();
485 var changed = readInput();
486 if (changed == "moved" && keyId) movementKeys[keyId] = true;
487 if (!changed && !missed) {missed = true; poll.set(80, p);}
488 else {pollingFast = false; slowPoll();}
489 endOperation();
490 }
491 poll.set(20, p);
492 }
493
494 // Inspects the textarea, compares its state (content, selection)
495 // to the data in the editing variable, and updates the editor
496 // content or cursor if something changed.
497 function readInput() {
498 var changed = false, text = input.value, sr = selRange(input);
499 if (!sr) return false;
500 var changed = editing.text != text, rs = reducedSelection;
501 var moved = changed || sr.start != editing.start || sr.end != (rs ? editing.start : editing.end);
502 if (reducedSelection && !moved && sel.from.line == 0 && sel.from.ch == 0)
503 reducedSelection = null;
504 else if (!moved) return false;
505 if (changed) {
506 shiftSelecting = reducedSelection = null;
507 if (options.readOnly) {updateInput = true; return "changed";}
508 }
509
510 // Compute selection start and end based on start/end offsets in textarea
511 function computeOffset(n, startLine) {
512 var pos = 0;
513 for (;;) {
514 var found = text.indexOf("\n", pos);
515 if (found == -1 || (text.charAt(found-1) == "\r" ? found - 1 : found) >= n)
516 return {line: startLine, ch: n - pos};
517 ++startLine;
518 pos = found + 1;
519 }
520 }
521 var from = computeOffset(sr.start, editing.from),
522 to = computeOffset(sr.end, editing.from);
523 // Here we have to take the reducedSelection hack into account,
524 // so that you can, for example, press shift-up at the start of
525 // your selection and have the right thing happen.
526 if (rs) {
527 from = sr.start == rs.anchor ? to : from;
528 to = shiftSelecting ? sel.to : sr.start == rs.anchor ? from : to;
529 if (!posLess(from, to)) {
530 reducedSelection = null;
531 sel.inverted = false;
532 var tmp = from; from = to; to = tmp;
533 }
534 }
535
536 // In some cases (cursor on same line as before), we don't have
537 // to update the textarea content at all.
538 if (from.line == to.line && from.line == sel.from.line && from.line == sel.to.line && !shiftSelecting)
539 updateInput = false;
540
541 // Magic mess to extract precise edited range from the changed
542 // string.
543 if (changed) {
544 var start = 0, end = text.length, len = Math.min(end, editing.text.length);
545 var c, line = editing.from, nl = -1;
546 while (start < len && (c = text.charAt(start)) == editing.text.charAt(start)) {
547 ++start;
548 if (c == "\n") {line++; nl = start;}
549 }
550 var ch = nl > -1 ? start - nl : start, endline = editing.to - 1, edend = editing.text.length;
551 for (;;) {
552 c = editing.text.charAt(edend);
553 if (c == "\n") endline--;
554 if (text.charAt(end) != c) {++end; ++edend; break;}
555 if (edend <= start || end <= start) break;
556 --end; --edend;
557 }
558 var nl = editing.text.lastIndexOf("\n", edend - 1), endch = nl == -1 ? edend : edend - nl - 1;
559 updateLines({line: line, ch: ch}, {line: endline, ch: endch}, splitLines(text.slice(start, end)), from, to);
560 if (line != endline || from.line != line) updateInput = true;
561 }
562 else setSelection(from, to);
563
564 editing.text = text; editing.start = sr.start; editing.end = sr.end;
565 return changed ? "changed" : moved ? "moved" : false;
566 }
567
568 // Set the textarea content and selection range to match the
569 // editor state.
570 function prepareInput() {
571 var text = [];
572 var from = Math.max(0, sel.from.line - 1), to = Math.min(lines.length, sel.to.line + 2);
573 for (var i = from; i < to; ++i) text.push(lines[i].text);
574 text = input.value = text.join(lineSep);
575 var startch = sel.from.ch, endch = sel.to.ch;
576 for (var i = from; i < sel.from.line; ++i)
577 startch += lineSep.length + lines[i].text.length;
578 for (var i = from; i < sel.to.line; ++i)
579 endch += lineSep.length + lines[i].text.length;
580 editing = {text: text, from: from, to: to, start: startch, end: endch};
581 setSelRange(input, startch, reducedSelection ? startch : endch);
582 }
583
584 function scrollCursorIntoView() {
585 var cursor = localCoords(sel.inverted ? sel.from : sel.to);
586 return scrollIntoView(cursor.x, cursor.y, cursor.x, cursor.yBot);
587 }
588 function scrollIntoView(x1, y1, x2, y2) {
589 var pl = paddingLeft(), pt = paddingTop();
590 y1 += pt; y2 += pt; x1 += pl; x2 += pl;
591 var screen = wrapper.clientHeight, screentop = wrapper.scrollTop, scrolled = false, result = true;
592 if (y1 < screentop) {wrapper.scrollTop = Math.max(0, y1 - 10); scrolled = true;}
593 else if (y2 > screentop + screen) {wrapper.scrollTop = y2 + 10 - screen; scrolled = true;}
594
595 var screenw = wrapper.clientWidth, screenleft = wrapper.scrollLeft;
596 if (x1 < screenleft) {wrapper.scrollLeft = Math.max(0, x1 - 10); scrolled = true;}
597 else if (x2 > screenw + screenleft) {
598 wrapper.scrollLeft = x2 + 10 - screenw;
599 scrolled = true;
600 if (x2 > code.clientWidth) result = false;
601 }
602 if (scrolled && options.onScroll) options.onScroll(instance);
603 return result;
604 }
605
606 function visibleLines() {
607 var lh = lineHeight(), top = wrapper.scrollTop - paddingTop();
608 return {from: Math.min(lines.length, Math.max(0, Math.floor(top / lh))),
609 to: Math.min(lines.length, Math.ceil((top + wrapper.clientHeight) / lh))};
610 }
611 // Uses a set of changes plus the current scroll position to
612 // determine which DOM updates have to be made, and makes the
613 // updates.
614 function updateDisplay(changes) {
615 if (!wrapper.clientWidth) {
616 showingFrom = showingTo = 0;
617 return;
618 }
619 // First create a range of theoretically intact lines, and punch
620 // holes in that using the change info.
621 var intact = changes === true ? [] : [{from: showingFrom, to: showingTo, domStart: 0}];
622 for (var i = 0, l = changes.length || 0; i < l; ++i) {
623 var change = changes[i], intact2 = [], diff = change.diff || 0;
624 for (var j = 0, l2 = intact.length; j < l2; ++j) {
625 var range = intact[j];
626 if (change.to <= range.from)
627 intact2.push({from: range.from + diff, to: range.to + diff, domStart: range.domStart});
628 else if (range.to <= change.from)
629 intact2.push(range);
630 else {
631 if (change.from > range.from)
632 intact2.push({from: range.from, to: change.from, domStart: range.domStart})
633 if (change.to < range.to)
634 intact2.push({from: change.to + diff, to: range.to + diff,
635 domStart: range.domStart + (change.to - range.from)});
636 }
637 }
638 intact = intact2;
639 }
640
641 // Then, determine which lines we'd want to see, and which
642 // updates have to be made to get there.
643 var visible = visibleLines();
644 var from = Math.min(showingFrom, Math.max(visible.from - 3, 0)),
645 to = Math.min(lines.length, Math.max(showingTo, visible.to + 3)),
646 updates = [], domPos = 0, domEnd = showingTo - showingFrom, pos = from, changedLines = 0;
647
648 for (var i = 0, l = intact.length; i < l; ++i) {
649 var range = intact[i];
650 if (range.to <= from) continue;
651 if (range.from >= to) break;
652 if (range.domStart > domPos || range.from > pos) {
653 updates.push({from: pos, to: range.from, domSize: range.domStart - domPos, domStart: domPos});
654 changedLines += range.from - pos;
655 }
656 pos = range.to;
657 domPos = range.domStart + (range.to - range.from);
658 }
659 if (domPos != domEnd || pos != to) {
660 changedLines += Math.abs(to - pos);
661 updates.push({from: pos, to: to, domSize: domEnd - domPos, domStart: domPos});
662 }
663
664 if (!updates.length) return;
665 lineDiv.style.display = "none";
666 // If more than 30% of the screen needs update, just do a full
667 // redraw (which is quicker than patching)
668 if (changedLines > (visible.to - visible.from) * .3)
669 refreshDisplay(from = Math.max(visible.from - 10, 0), to = Math.min(visible.to + 7, lines.length));
670 // Otherwise, only update the stuff that needs updating.
671 else
672 patchDisplay(updates);
673 lineDiv.style.display = "";
674
675 // Position the mover div to align with the lines it's supposed
676 // to be showing (which will cover the visible display)
677 var different = from != showingFrom || to != showingTo || lastHeight != wrapper.clientHeight;
678 showingFrom = from; showingTo = to;
679 mover.style.top = (from * lineHeight()) + "px";
680 if (different) {
681 lastHeight = wrapper.clientHeight;
682 code.style.height = (lines.length * lineHeight() + 2 * paddingTop()) + "px";
683 updateGutter();
684 }
685
686 // Since this is all rather error prone, it is honoured with the
687 // only assertion in the whole file.
688 if (lineDiv.childNodes.length != showingTo - showingFrom)
689 throw new Error("BAD PATCH! " + JSON.stringify(updates) + " size=" + (showingTo - showingFrom) +
690 " nodes=" + lineDiv.childNodes.length);
691 updateCursor();
692 }
693
694 function refreshDisplay(from, to) {
695 var html = [], start = {line: from, ch: 0}, inSel = posLess(sel.from, start) && !posLess(sel.to, start);
696 for (var i = from; i < to; ++i) {
697 var ch1 = null, ch2 = null;
698 if (inSel) {
699 ch1 = 0;
700 if (sel.to.line == i) {inSel = false; ch2 = sel.to.ch;}
701 }
702 else if (sel.from.line == i) {
703 if (sel.to.line == i) {ch1 = sel.from.ch; ch2 = sel.to.ch;}
704 else {inSel = true; ch1 = sel.from.ch;}
705 }
706 html.push(lines[i].getHTML(ch1, ch2, true));
707 }
708 lineDiv.innerHTML = html.join("");
709 }
710 function patchDisplay(updates) {
711 // Slightly different algorithm for IE (badInnerHTML), since
712 // there .innerHTML on PRE nodes is dumb, and discards
713 // whitespace.
714 var sfrom = sel.from.line, sto = sel.to.line, off = 0,
715 scratch = badInnerHTML && document.createElement("div");
716 for (var i = 0, e = updates.length; i < e; ++i) {
717 var rec = updates[i];
718 var extra = (rec.to - rec.from) - rec.domSize;
719 var nodeAfter = lineDiv.childNodes[rec.domStart + rec.domSize + off] || null;
720 if (badInnerHTML)
721 for (var j = Math.max(-extra, rec.domSize); j > 0; --j)
722 lineDiv.removeChild(nodeAfter ? nodeAfter.previousSibling : lineDiv.lastChild);
723 else if (extra) {
724 for (var j = Math.max(0, extra); j > 0; --j)
725 lineDiv.insertBefore(document.createElement("pre"), nodeAfter);
726 for (var j = Math.max(0, -extra); j > 0; --j)
727 lineDiv.removeChild(nodeAfter ? nodeAfter.previousSibling : lineDiv.lastChild);
728 }
729 var node = lineDiv.childNodes[rec.domStart + off], inSel = sfrom < rec.from && sto >= rec.from;
730 for (var j = rec.from; j < rec.to; ++j) {
731 var ch1 = null, ch2 = null;
732 if (inSel) {
733 ch1 = 0;
734 if (sto == j) {inSel = false; ch2 = sel.to.ch;}
735 }
736 else if (sfrom == j) {
737 if (sto == j) {ch1 = sel.from.ch; ch2 = sel.to.ch;}
738 else {inSel = true; ch1 = sel.from.ch;}
739 }
740 if (badInnerHTML) {
741 scratch.innerHTML = lines[j].getHTML(ch1, ch2, true);
742 lineDiv.insertBefore(scratch.firstChild, nodeAfter);
743 }
744 else {
745 node.innerHTML = lines[j].getHTML(ch1, ch2, false);
746 node.className = lines[j].className || "";
747 node = node.nextSibling;
748 }
749 }
750 off += extra;
751 }
752 }
753
754 function updateGutter() {
755 if (!options.gutter && !options.lineNumbers) return;
756 var hText = mover.offsetHeight, hEditor = wrapper.clientHeight;
757 gutter.style.height = (hText - hEditor < 2 ? hEditor : hText) + "px";
758 var html = [];
759 for (var i = showingFrom; i < showingTo; ++i) {
760 var marker = lines[i].gutterMarker;
761 var text = options.lineNumbers ? i + options.firstLineNumber : null;
762 if (marker && marker.text)
763 text = marker.text.replace("%N%", text != null ? text : "");
764 else if (text == null)
765 text = "\u00a0";
766 html.push((marker && marker.style ? '<pre class="' + marker.style + '">' : "<pre>"), text, "</pre>");
767 }
768 gutter.style.display = "none";
769 gutterText.innerHTML = html.join("");
770 var minwidth = String(lines.length).length, firstNode = gutterText.firstChild, val = eltText(firstNode), pad = "";
771 while (val.length + pad.length < minwidth) pad += "\u00a0";
772 if (pad) firstNode.insertBefore(document.createTextNode(pad), firstNode.firstChild);
773 gutter.style.display = "";
774 lineSpace.style.marginLeft = gutter.offsetWidth + "px";
775 }
776 function updateCursor() {
777 var head = sel.inverted ? sel.from : sel.to;
778 var x = charX(head.line, head.ch) + "px", y = (head.line - showingFrom) * lineHeight() + "px";
779 inputDiv.style.top = y; inputDiv.style.left = x;
780 if (posEq(sel.from, sel.to)) {
781 cursor.style.top = y; cursor.style.left = x;
782 cursor.style.display = "";
783 }
784 else cursor.style.display = "none";
785 }
786
787 // Update the selection. Last two args are only used by
788 // updateLines, since they have to be expressed in the line
789 // numbers before the update.
790 function setSelection(from, to, oldFrom, oldTo) {
791 if (posEq(sel.from, from) && posEq(sel.to, to)) return;
792 var sh = shiftSelecting && clipPos(shiftSelecting);
793 if (posLess(to, from)) {var tmp = to; to = from; from = tmp;}
794 if (sh) {
795 if (posLess(sh, from)) from = sh;
796 else if (posLess(to, sh)) to = sh;
797 }
798
799 var startEq = posEq(sel.to, to), endEq = posEq(sel.from, from);
800 if (posEq(from, to)) sel.inverted = false;
801 else if (startEq && !endEq) sel.inverted = true;
802 else if (endEq && !startEq) sel.inverted = false;
803
804 // Some ugly logic used to only mark the lines that actually did
805 // see a change in selection as changed, rather than the whole
806 // selected range.
807 if (oldFrom == null) {oldFrom = sel.from.line; oldTo = sel.to.line;}
808 if (posEq(from, to)) {
809 if (!posEq(sel.from, sel.to))
810 changes.push({from: oldFrom, to: oldTo + 1});
811 }
812 else if (posEq(sel.from, sel.to)) {
813 changes.push({from: from.line, to: to.line + 1});
814 }
815 else {
816 if (!posEq(from, sel.from)) {
817 if (from.line < oldFrom)
818 changes.push({from: from.line, to: Math.min(to.line, oldFrom) + 1});
819 else
820 changes.push({from: oldFrom, to: Math.min(oldTo, from.line) + 1});
821 }
822 if (!posEq(to, sel.to)) {
823 if (to.line < oldTo)
824 changes.push({from: Math.max(oldFrom, from.line), to: oldTo + 1});
825 else
826 changes.push({from: Math.max(from.line, oldTo), to: to.line + 1});
827 }
828 }
829 sel.from = from; sel.to = to;
830 selectionChanged = true;
831 }
832 function setCursor(line, ch) {
833 var pos = clipPos({line: line, ch: ch || 0});
834 setSelection(pos, pos);
835 }
836
837 function clipLine(n) {return Math.max(0, Math.min(n, lines.length-1));}
838 function clipPos(pos) {
839 if (pos.line < 0) return {line: 0, ch: 0};
840 if (pos.line >= lines.length) return {line: lines.length-1, ch: lines[lines.length-1].text.length};
841 var ch = pos.ch, linelen = lines[pos.line].text.length;
842 if (ch == null || ch > linelen) return {line: pos.line, ch: linelen};
843 else if (ch < 0) return {line: pos.line, ch: 0};
844 else return pos;
845 }
846
847 function scrollPage(down) {
848 var linesPerPage = Math.floor(wrapper.clientHeight / lineHeight()), head = sel.inverted ? sel.from : sel.to;
849 setCursor(head.line + (Math.max(linesPerPage - 1, 1) * (down ? 1 : -1)), head.ch);
850 }
851 function scrollEnd(top) {
852 setCursor(top ? 0 : lines.length - 1);
853 }
854 function selectAll() {
855 var endLine = lines.length - 1;
856 setSelection({line: 0, ch: 0}, {line: endLine, ch: lines[endLine].text.length});
857 }
858 function selectWordAt(pos) {
859 var line = lines[pos.line].text;
860 var start = pos.ch, end = pos.ch;
861 while (start > 0 && /\w/.test(line.charAt(start - 1))) --start;
862 while (end < line.length - 1 && /\w/.test(line.charAt(end))) ++end;
863 setSelection({line: pos.line, ch: start}, {line: pos.line, ch: end});
864 }
865 function handleEnter() {
866 replaceSelection("\n", "end");
867 if (options.enterMode != "flat")
868 indentLine(sel.from.line, options.enterMode == "keep" ? "prev" : "smart");
869 }
870 function handleTab(shift) {
871 shiftSelecting = null;
872 switch (options.tabMode) {
873 case "default":
874 return false;
875 case "indent":
876 for (var i = sel.from.line, e = sel.to.line; i <= e; ++i) indentLine(i, "smart");
877 break;
878 case "classic":
879 if (posEq(sel.from, sel.to)) {
880 if (shift) indentLine(sel.from.line, "smart");
881 else replaceSelection("\t", "end");
882 break;
883 }
884 case "shift":
885 for (var i = sel.from.line, e = sel.to.line; i <= e; ++i) indentLine(i, shift ? "subtract" : "add");
886 break;
887 }
888 return true;
889 }
890
891 function indentLine(n, how) {
892 if (how == "smart") {
893 if (!mode.indent) how = "prev";
894 else var state = getStateBefore(n);
895 }
896
897 var line = lines[n], curSpace = line.indentation(), curSpaceString = line.text.match(/^\s*/)[0], indentation;
898 if (how == "prev") {
899 if (n) indentation = lines[n-1].indentation();
900 else indentation = 0;
901 }
902 else if (how == "smart") indentation = mode.indent(state, line.text.slice(curSpaceString.length));
903 else if (how == "add") indentation = curSpace + options.indentUnit;
904 else if (how == "subtract") indentation = curSpace - options.indentUnit;
905 indentation = Math.max(0, indentation);
906 var diff = indentation - curSpace;
907
908 if (!diff) {
909 if (sel.from.line != n && sel.to.line != n) return;
910 var indentString = curSpaceString;
911 }
912 else {
913 var indentString = "", pos = 0;
914 if (options.indentWithTabs)
915 for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";}
916 while (pos < indentation) {++pos; indentString += " ";}
917 }
918
919 replaceRange(indentString, {line: n, ch: 0}, {line: n, ch: curSpaceString.length});
920 }
921
922 function loadMode() {
923 mode = CodeMirror.getMode(options, options.mode);
924 for (var i = 0, l = lines.length; i < l; ++i)
925 lines[i].stateAfter = null;
926 work = [0];
927 }
928 function gutterChanged() {
929 var visible = options.gutter || options.lineNumbers;
930 gutter.style.display = visible ? "" : "none";
931 if (visible) updateGutter();
932 else lineDiv.parentNode.style.marginLeft = 0;
933 }
934
935 function markText(from, to, className) {
936 from = clipPos(from); to = clipPos(to);
937 var accum = [];
938 function add(line, from, to, className) {
939 var line = lines[line], mark = line.addMark(from, to, className);
940 mark.line = line;
941 accum.push(mark);
942 }
943 if (from.line == to.line) add(from.line, from.ch, to.ch, className);
944 else {
945 add(from.line, from.ch, null, className);
946 for (var i = from.line + 1, e = to.line; i < e; ++i)
947 add(i, 0, null, className);
948 add(to.line, 0, to.ch, className);
949 }
950 changes.push({from: from.line, to: to.line + 1});
951 return function() {
952 var start, end;
953 for (var i = 0; i < accum.length; ++i) {
954 var mark = accum[i], found = indexOf(lines, mark.line);
955 mark.line.removeMark(mark);
956 if (found > -1) {
957 if (start == null) start = found;
958 end = found;
959 }
960 }
961 if (start != null) changes.push({from: start, to: end + 1});
962 };
963 }
964
965 function addGutterMarker(line, text, className) {
966 if (typeof line == "number") line = lines[clipLine(line)];
967 line.gutterMarker = {text: text, style: className};
968 updateGutter();
969 return line;
970 }
971 function removeGutterMarker(line) {
972 if (typeof line == "number") line = lines[clipLine(line)];
973 line.gutterMarker = null;
974 updateGutter();
975 }
976 function setLineClass(line, className) {
977 if (typeof line == "number") {
978 var no = line;
979 line = lines[clipLine(line)];
980 }
981 else {
982 var no = indexOf(lines, line);
983 if (no == -1) return null;
984 }
985 line.className = className;
986 changes.push({from: no, to: no + 1});
987 return line;
988 }
989
990 function lineInfo(line) {
991 if (typeof line == "number") {
992 var n = line;
993 line = lines[line];
994 if (!line) return null;
995 }
996 else {
997 var n = indexOf(lines, line);
998 if (n == -1) return null;
999 }
1000 var marker = line.gutterMarker;
1001 return {line: n, text: line.text, markerText: marker && marker.text, markerClass: marker && marker.style};
1002 }
1003
1004 // These are used to go from pixel positions to character
1005 // positions, taking tabs into account.
1006 function charX(line, pos) {
1007 var text = lines[line].text, span = measure.firstChild;
1008 if (text.lastIndexOf("\t", pos) == -1) return pos * charWidth();
1009 var old = span.firstChild.nodeValue;
1010 try {
1011 span.firstChild.nodeValue = text.slice(0, pos);
1012 return span.offsetWidth;
1013 } finally {span.firstChild.nodeValue = old;}
1014 }
1015 function charFromX(line, x) {
1016 var text = lines[line].text, cw = charWidth();
1017 if (x <= 0) return 0;
1018 if (text.indexOf("\t") == -1) return Math.min(text.length, Math.round(x / cw));
1019 var mspan = measure.firstChild, mtext = mspan.firstChild, old = mtext.nodeValue;
1020 try {
1021 mtext.nodeValue = text;
1022 var from = 0, fromX = 0, to = text.length, toX = mspan.offsetWidth;
1023 if (x > toX) return to;
1024 for (;;) {
1025 if (to - from <= 1) return (toX - x > x - fromX) ? from : to;
1026 var middle = Math.ceil((from + to) / 2);
1027 mtext.nodeValue = text.slice(0, middle);
1028 var curX = mspan.offsetWidth;
1029 if (curX > x) {to = middle; toX = curX;}
1030 else {from = middle; fromX = curX;}
1031 }
1032 } finally {mtext.nodeValue = old;}
1033 }
1034
1035 function localCoords(pos, inLineWrap) {
1036 var lh = lineHeight(), line = pos.line - (inLineWrap ? showingFrom : 0);
1037 return {x: charX(pos.line, pos.ch), y: line * lh, yBot: (line + 1) * lh};
1038 }
1039 function pageCoords(pos) {
1040 var local = localCoords(pos, true), off = eltOffset(lineSpace);
1041 return {x: off.left + local.x, y: off.top + local.y, yBot: off.top + local.yBot};
1042 }
1043
1044 function lineHeight() {
1045 var nlines = lineDiv.childNodes.length;
1046 if (nlines) return lineDiv.offsetHeight / nlines;
1047 else return measure.firstChild.offsetHeight || 1;
1048 }
1049 function charWidth() {return (measure.firstChild.offsetWidth || 320) / 40;}
1050 function paddingTop() {return lineSpace.offsetTop;}
1051 function paddingLeft() {return lineSpace.offsetLeft;}
1052
1053 function posFromMouse(e, liberal) {
1054 var off = eltOffset(lineSpace),
1055 x = e.pageX() - off.left,
1056 y = e.pageY() - off.top;
1057 if (!liberal && e.target() != lineSpace.parentNode && !(e.target() == wrapper && y > (lines.length * lineHeight())))
1058 for (var n = e.target(); n != lineDiv && n != cursor; n = n.parentNode)
1059 if (!n || n == wrapper) return null;
1060 var line = showingFrom + Math.floor(y / lineHeight());
1061 return clipPos({line: line, ch: charFromX(clipLine(line), x)});
1062 }
1063 function onContextMenu(e) {
1064 var pos = posFromMouse(e);
1065 if (!pos || window.opera) return; // Opera is difficult.
1066 if (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to))
1067 setCursor(pos.line, pos.ch);
1068
1069 var oldCSS = input.style.cssText;
1070 input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.pageY() - 1) +
1071 "px; left: " + (e.pageX() - 1) + "px; z-index: 1000; background: white; " +
1072 "border-width: 0; outline: none; overflow: hidden;";
1073 var val = input.value = getSelection();
1074 input.focus();
1075 setSelRange(input, 0, val.length);
1076 if (gecko) e.stop();
1077 leaveInputAlone = true;
1078 setTimeout(function() {
1079 if (input.value != val) operation(replaceSelection)(input.value, "end");
1080 input.style.cssText = oldCSS;
1081 leaveInputAlone = false;
1082 prepareInput();
1083 slowPoll();
1084 }, 50);
1085 }
1086
1087 // Cursor-blinking
1088 function restartBlink() {
1089 clearInterval(blinker);
1090 var on = true;
1091 cursor.style.visibility = "";
1092 blinker = setInterval(function() {
1093 cursor.style.visibility = (on = !on) ? "" : "hidden";
1094 }, 650);
1095 }
1096
1097 var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"};
1098 function matchBrackets(autoclear) {
1099 var head = sel.inverted ? sel.from : sel.to, line = lines[head.line], pos = head.ch - 1;
1100 var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)];
1101 if (!match) return;
1102 var ch = match.charAt(0), forward = match.charAt(1) == ">", d = forward ? 1 : -1, st = line.styles;
1103 for (var off = pos + 1, i = 0, e = st.length; i < e; i+=2)
1104 if ((off -= st[i].length) <= 0) {var style = st[i+1]; break;}
1105
1106 var stack = [line.text.charAt(pos)], re = /[(){}[\]]/;
1107 function scan(line, from, to) {
1108 if (!line.text) return;
1109 var st = line.styles, pos = forward ? 0 : line.text.length - 1, cur;
1110 for (var i = forward ? 0 : st.length - 2, e = forward ? st.length : -2; i != e; i += 2*d) {
1111 var text = st[i];
1112 if (st[i+1] != null && st[i+1] != style) {pos += d * text.length; continue;}
1113 for (var j = forward ? 0 : text.length - 1, te = forward ? text.length : -1; j != te; j += d, pos+=d) {
1114 if (pos >= from && pos < to && re.test(cur = text.charAt(j))) {
1115 var match = matching[cur];
1116 if (match.charAt(1) == ">" == forward) stack.push(cur);
1117 else if (stack.pop() != match.charAt(0)) return {pos: pos, match: false};
1118 else if (!stack.length) return {pos: pos, match: true};
1119 }
1120 }
1121 }
1122 }
1123 for (var i = head.line, e = forward ? Math.min(i + 50, lines.length) : Math.max(0, i - 50); i != e; i+=d) {
1124 var line = lines[i], first = i == head.line;
1125 var found = scan(line, first && forward ? pos + 1 : 0, first && !forward ? pos : line.text.length);
1126 if (found) {
1127 var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
1128 var one = markText({line: head.line, ch: pos}, {line: head.line, ch: pos+1}, style),
1129 two = markText({line: i, ch: found.pos}, {line: i, ch: found.pos + 1}, style);
1130 var clear = operation(function(){one(); two();});
1131 if (autoclear) setTimeout(clear, 800);
1132 else bracketHighlighted = clear;
1133 break;
1134 }
1135 }
1136 }
1137
1138 // Finds the line to start with when starting a parse. Tries to
1139 // find a line with a stateAfter, so that it can start with a
1140 // valid state. If that fails, it returns the line with the
1141 // smallest indentation, which tends to need the least context to
1142 // parse correctly.
1143 function findStartLine(n) {
1144 var minindent, minline;
1145 for (var search = n, lim = n - 40; search > lim; --search) {
1146 if (search == 0) return 0;
1147 var line = lines[search-1];
1148 if (line.stateAfter) return search;
1149 var indented = line.indentation();
1150 if (minline == null || minindent > indented) {
1151 minline = search;
1152 minindent = indented;
1153 }
1154 }
1155 return minline;
1156 }
1157 function getStateBefore(n) {
1158 var start = findStartLine(n), state = start && lines[start-1].stateAfter;
1159 if (!state) state = startState(mode);
1160 else state = copyState(mode, state);
1161 for (var i = start; i < n; ++i) {
1162 var line = lines[i];
1163 line.highlight(mode, state);
1164 line.stateAfter = copyState(mode, state);
1165 }
1166 if (!lines[n].stateAfter) work.push(n);
1167 return state;
1168 }
1169 function highlightWorker() {
1170 var end = +new Date + options.workTime;
1171 while (work.length) {
1172 if (!lines[showingFrom].stateAfter) var task = showingFrom;
1173 else var task = work.pop();
1174 if (task >= lines.length) continue;
1175 var start = findStartLine(task), state = start && lines[start-1].stateAfter;
1176 if (state) state = copyState(mode, state);
1177 else state = startState(mode);
1178
1179 for (var i = start, l = lines.length; i < l; ++i) {
1180 var line = lines[i], hadState = line.stateAfter;
1181 if (+new Date > end) {
1182 work.push(i);
1183 startWorker(options.workDelay);
1184 changes.push({from: task, to: i});
1185 return;
1186 }
1187 var changed = line.highlight(mode, state);
1188 line.stateAfter = copyState(mode, state);
1189 if (hadState && !changed && line.text) break;
1190 }
1191 changes.push({from: task, to: i});
1192 }
1193 }
1194 function startWorker(time) {
1195 if (!work.length) return;
1196 highlight.set(time, operation(highlightWorker));
1197 }
1198
1199 // Operations are used to wrap changes in such a way that each
1200 // change won't have to update the cursor and display (which would
1201 // be awkward, slow, and error-prone), but instead updates are
1202 // batched and then all combined and executed at once.
1203 function startOperation() {
1204 updateInput = null; changes = []; textChanged = selectionChanged = false;
1205 }
1206 function endOperation() {
1207 var reScroll = false;
1208 if (selectionChanged) reScroll = !scrollCursorIntoView();
1209 if (changes.length) updateDisplay(changes);
1210 else if (selectionChanged) updateCursor();
1211 if (reScroll) scrollCursorIntoView();
1212 if (selectionChanged) restartBlink();
1213
1214 // updateInput can be set to a boolean value to force/prevent an
1215 // update.
1216 if (!leaveInputAlone && (updateInput === true || (updateInput !== false && selectionChanged)))
1217 prepareInput();
1218
1219 if (selectionChanged && options.onCursorActivity)
1220 options.onCursorActivity(instance);
1221 if (textChanged && options.onChange)
1222 options.onChange(instance);
1223 if (selectionChanged && options.matchBrackets)
1224 setTimeout(operation(function() {
1225 if (bracketHighlighted) {bracketHighlighted(); bracketHighlighted = null;}
1226 matchBrackets(false);
1227 }), 20);
1228 }
1229 var nestedOperation = 0;
1230 function operation(f) {
1231 return function() {
1232 if (!nestedOperation++) startOperation();
1233 try {var result = f.apply(this, arguments);}
1234 finally {if (!--nestedOperation) endOperation();}
1235 return result;
1236 };
1237 }
1238
1239 function SearchCursor(query, pos, caseFold) {
1240 this.atOccurrence = false;
1241 if (caseFold == null) caseFold = typeof query == "string" && query == query.toLowerCase();
1242
1243 if (pos && typeof pos == "object") pos = clipPos(pos);
1244 else pos = {line: 0, ch: 0};
1245 this.pos = {from: pos, to: pos};
1246
1247 // The matches method is filled in based on the type of query.
1248 // It takes a position and a direction, and returns an object
1249 // describing the next occurrence of the query, or null if no
1250 // more matches were found.
1251 if (typeof query != "string") // Regexp match
1252 this.matches = function(reverse, pos) {
1253 if (reverse) {
1254 var line = lines[pos.line].text.slice(0, pos.ch), match = line.match(query), start = 0;
1255 while (match) {
1256 var ind = line.indexOf(match[0]);
1257 start += ind;
1258 line = line.slice(ind + 1);
1259 var newmatch = line.match(query);
1260 if (newmatch) match = newmatch;
1261 else break;
1262 }
1263 }
1264 else {
1265 var line = lines[pos.line].text.slice(pos.ch), match = line.match(query),
1266 start = match && pos.ch + line.indexOf(match[0]);
1267 }
1268 if (match)
1269 return {from: {line: pos.line, ch: start},
1270 to: {line: pos.line, ch: start + match[0].length},
1271 match: match};
1272 };
1273 else { // String query
1274 if (caseFold) query = query.toLowerCase();
1275 var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;};
1276 var target = query.split("\n");
1277 // Different methods for single-line and multi-line queries
1278 if (target.length == 1)
1279 this.matches = function(reverse, pos) {
1280 var line = fold(lines[pos.line].text), len = query.length, match;
1281 if (reverse ? (pos.ch >= len && (match = line.lastIndexOf(query, pos.ch - len)) != -1)
1282 : (match = line.indexOf(query, pos.ch)) != -1)
1283 return {from: {line: pos.line, ch: match},
1284 to: {line: pos.line, ch: match + len}};
1285 };
1286 else
1287 this.matches = function(reverse, pos) {
1288 var ln = pos.line, idx = (reverse ? target.length - 1 : 0), match = target[idx], line = fold(lines[ln].text);
1289 var offsetA = (reverse ? line.indexOf(match) + match.length : line.lastIndexOf(match));
1290 if (reverse ? offsetA >= pos.ch || offsetA != match.length
1291 : offsetA <= pos.ch || offsetA != line.length - match.length)
1292 return;
1293 for (;;) {
1294 if (reverse ? !ln : ln == lines.length - 1) return;
1295 line = fold(lines[ln += reverse ? -1 : 1].text);
1296 match = target[reverse ? --idx : ++idx];
1297 if (idx > 0 && idx < target.length - 1) {
1298 if (line != match) return;
1299 else continue;
1300 }
1301 var offsetB = (reverse ? line.lastIndexOf(match) : line.indexOf(match) + match.length);
1302 if (reverse ? offsetB != line.length - match.length : offsetB != match.length)
1303 return;
1304 var start = {line: pos.line, ch: offsetA}, end = {line: ln, ch: offsetB};
1305 return {from: reverse ? end : start, to: reverse ? start : end};
1306 }
1307 };
1308 }
1309 }
1310
1311 SearchCursor.prototype = {
1312 findNext: function() {return this.find(false);},
1313 findPrevious: function() {return this.find(true);},
1314
1315 find: function(reverse) {
1316 var self = this, pos = clipPos(reverse ? this.pos.from : this.pos.to);
1317 function savePosAndFail(line) {
1318 var pos = {line: line, ch: 0};
1319 self.pos = {from: pos, to: pos};
1320 self.atOccurrence = false;
1321 return false;
1322 }
1323
1324 for (;;) {
1325 if (this.pos = this.matches(reverse, pos)) {
1326 this.atOccurrence = true;
1327 return this.pos.match || true;
1328 }
1329 if (reverse) {
1330 if (!pos.line) return savePosAndFail(0);
1331 pos = {line: pos.line-1, ch: lines[pos.line-1].text.length};
1332 }
1333 else {
1334 if (pos.line == lines.length - 1) return savePosAndFail(lines.length);
1335 pos = {line: pos.line+1, ch: 0};
1336 }
1337 }
1338 },
1339
1340 from: function() {if (this.atOccurrence) return copyPos(this.pos.from);},
1341 to: function() {if (this.atOccurrence) return copyPos(this.pos.to);}
1342 };
1343
1344 return instance;
1345 } // (end of function CodeMirror)
1346
1347 // The default configuration options.
1348 CodeMirror.defaults = {
1349 value: "",
1350 mode: null,
1351 indentUnit: 2,
1352 indentWithTabs: false,
1353 tabMode: "classic",
1354 enterMode: "indent",
1355 electricChars: true,
1356 onKeyEvent: null,
1357 lineNumbers: false,
1358 gutter: false,
1359 firstLineNumber: 1,
1360 readOnly: false,
1361 onChange: null,
1362 onCursorActivity: null,
1363 onGutterClick: null,
1364 onFocus: null, onBlur: null, onScroll: null,
1365 matchBrackets: false,
1366 workTime: 100,
1367 workDelay: 200,
1368 undoDepth: 40,
1369 tabindex: null
1370 };
1371
1372 // Known modes, by name and by MIME
1373 var modes = {}, mimeModes = {};
1374 CodeMirror.defineMode = function(name, mode) {
1375 if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name;
1376 modes[name] = mode;
1377 };
1378 CodeMirror.defineMIME = function(mime, spec) {
1379 mimeModes[mime] = spec;
1380 };
1381 CodeMirror.getMode = function(options, spec) {
1382 if (typeof spec == "string" && mimeModes.hasOwnProperty(spec))
1383 spec = mimeModes[spec];
1384 if (typeof spec == "string")
1385 var mname = spec, config = {};
1386 else
1387 var mname = spec.name, config = spec;
1388 var mfactory = modes[mname];
1389 if (!mfactory) {
1390 if (window.console) console.warn("No mode " + mname + " found, falling back to plain text.");
1391 return CodeMirror.getMode(options, "text/plain");
1392 }
1393 return mfactory(options, config);
1394 }
1395 CodeMirror.listModes = function() {
1396 var list = [];
1397 for (var m in modes)
1398 if (modes.propertyIsEnumerable(m)) list.push(m);
1399 return list;
1400 };
1401 CodeMirror.listMIMEs = function() {
1402 var list = [];
1403 for (var m in mimeModes)
1404 if (mimeModes.propertyIsEnumerable(m)) list.push(m);
1405 return list;
1406 };
1407
1408 CodeMirror.fromTextArea = function(textarea, options) {
1409 if (!options) options = {};
1410 options.value = textarea.value;
1411 if (!options.tabindex && textarea.tabindex)
1412 options.tabindex = textarea.tabindex;
1413
1414 function save() {textarea.value = instance.getValue();}
1415 if (textarea.form) {
1416 // Deplorable hack to make the submit method do the right thing.
1417 var rmSubmit = connect(textarea.form, "submit", save, true);
1418 if (typeof textarea.form.submit == "function") {
1419 var realSubmit = textarea.form.submit;
1420 function wrappedSubmit() {
1421 save();
1422 textarea.form.submit = realSubmit;
1423 textarea.form.submit();
1424 textarea.form.submit = wrappedSubmit;
1425 }
1426 textarea.form.submit = wrappedSubmit;
1427 }
1428 }
1429
1430 textarea.style.display = "none";
1431 var instance = CodeMirror(function(node) {
1432 textarea.parentNode.insertBefore(node, textarea.nextSibling);
1433 }, options);
1434 instance.save = save;
1435 instance.toTextArea = function() {
1436 save();
1437 textarea.parentNode.removeChild(instance.getWrapperElement());
1438 textarea.style.display = "";
1439 if (textarea.form) {
1440 rmSubmit();
1441 if (typeof textarea.form.submit == "function")
1442 textarea.form.submit = realSubmit;
1443 }
1444 };
1445 return instance;
1446 };
1447
1448 // Utility functions for working with state. Exported because modes
1449 // sometimes need to do this.
1450 function copyState(mode, state) {
1451 if (state === true) return state;
1452 if (mode.copyState) return mode.copyState(state);
1453 var nstate = {};
1454 for (var n in state) {
1455 var val = state[n];
1456 if (val instanceof Array) val = val.concat([]);
1457 nstate[n] = val;
1458 }
1459 return nstate;
1460 }
1461 CodeMirror.startState = startState;
1462 function startState(mode, a1, a2) {
1463 return mode.startState ? mode.startState(a1, a2) : true;
1464 }
1465 CodeMirror.copyState = copyState;
1466
1467 // The character stream used by a mode's parser.
1468 function StringStream(string) {
1469 this.pos = this.start = 0;
1470 this.string = string;
1471 }
1472 StringStream.prototype = {
1473 eol: function() {return this.pos >= this.string.length;},
1474 sol: function() {return this.pos == 0;},
1475 peek: function() {return this.string.charAt(this.pos);},
1476 next: function() {
1477 if (this.pos < this.string.length)
1478 return this.string.charAt(this.pos++);
1479 },
1480 eat: function(match) {
1481 var ch = this.string.charAt(this.pos);
1482 if (typeof match == "string") var ok = ch == match;
1483 else var ok = ch && (match.test ? match.test(ch) : match(ch));
1484 if (ok) {++this.pos; return ch;}
1485 },
1486 eatWhile: function(match) {
1487 var start = this.start;
1488 while (this.eat(match)){}
1489 return this.pos > start;
1490 },
1491 eatSpace: function() {
1492 var start = this.pos;
1493 while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos;
1494 return this.pos > start;
1495 },
1496 skipToEnd: function() {this.pos = this.string.length;},
1497 skipTo: function(ch) {
1498 var found = this.string.indexOf(ch, this.pos);
1499 if (found > -1) {this.pos = found; return true;}
1500 },
1501 backUp: function(n) {this.pos -= n;},
1502 column: function() {return countColumn(this.string, this.start);},
1503 indentation: function() {return countColumn(this.string);},
1504 match: function(pattern, consume, caseInsensitive) {
1505 if (typeof pattern == "string") {
1506 function cased(str) {return caseInsensitive ? str.toLowerCase() : str;}
1507 if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) {
1508 if (consume !== false) this.pos += pattern.length;
1509 return true;
1510 }
1511 }
1512 else {
1513 var match = this.string.slice(this.pos).match(pattern);
1514 if (match && consume !== false) this.pos += match[0].length;
1515 return match;
1516 }
1517 },
1518 current: function(){return this.string.slice(this.start, this.pos);}
1519 };
1520
1521 // Line objects. These hold state related to a line, including
1522 // highlighting info (the styles array).
1523 function Line(text, styles) {
1524 this.styles = styles || [text, null];
1525 this.stateAfter = null;
1526 this.text = text;
1527 this.marked = this.gutterMarker = this.className = null;
1528 }
1529 Line.prototype = {
1530 // Replace a piece of a line, keeping the styles around it intact.
1531 replace: function(from, to, text) {
1532 var st = [], mk = this.marked;
1533 copyStyles(0, from, this.styles, st);
1534 if (text) st.push(text, null);
1535 copyStyles(to, this.text.length, this.styles, st);
1536 this.styles = st;
1537 this.text = this.text.slice(0, from) + text + this.text.slice(to);
1538 this.stateAfter = null;
1539 if (mk) {
1540 var diff = text.length - (to - from), end = this.text.length;
1541 function fix(n) {return n <= Math.min(to, to + diff) ? n : n + diff;}
1542 for (var i = 0; i < mk.length; ++i) {
1543 var mark = mk[i], del = false;
1544 if (mark.from >= end) del = true;
1545 else {mark.from = fix(mark.from); if (mark.to != null) mark.to = fix(mark.to);}
1546 if (del || mark.from >= mark.to) {mk.splice(i, 1); i--;}
1547 }
1548 }
1549 },
1550 // Split a line in two, again keeping styles intact.
1551 split: function(pos, textBefore) {
1552 var st = [textBefore, null];
1553 copyStyles(pos, this.text.length, this.styles, st);
1554 return new Line(textBefore + this.text.slice(pos), st);
1555 },
1556 addMark: function(from, to, style) {
1557 var mk = this.marked, mark = {from: from, to: to, style: style};
1558 if (this.marked == null) this.marked = [];
1559 this.marked.push(mark);
1560 this.marked.sort(function(a, b){return a.from - b.from;});
1561 return mark;
1562 },
1563 removeMark: function(mark) {
1564 var mk = this.marked;
1565 if (!mk) return;
1566 for (var i = 0; i < mk.length; ++i)
1567 if (mk[i] == mark) {mk.splice(i, 1); break;}
1568 },
1569 // Run the given mode's parser over a line, update the styles
1570 // array, which contains alternating fragments of text and CSS
1571 // classes.
1572 highlight: function(mode, state) {
1573 var stream = new StringStream(this.text), st = this.styles, pos = 0, changed = false;
1574 while (!stream.eol()) {
1575 var style = mode.token(stream, state);
1576 var substr = this.text.slice(stream.start, stream.pos);
1577 stream.start = stream.pos;
1578 if (pos && st[pos-1] == style)
1579 st[pos-2] += substr;
1580 else if (substr) {
1581 if (!changed && st[pos] != substr || st[pos+1] != style) changed = true;
1582 st[pos++] = substr; st[pos++] = style;
1583 }
1584 // Give up when line is ridiculously long
1585 if (stream.pos > 5000) {
1586 st[pos++] = this.text.slice(stream.pos); st[pos++] = null;
1587 break;
1588 }
1589 }
1590 if (st.length != pos) {st.length = pos; changed = true;}
1591 return changed;
1592 },
1593 // Fetch the parser token for a given character. Useful for hacks
1594 // that want to inspect the mode state (say, for completion).
1595 getTokenAt: function(mode, state, ch) {
1596 var txt = this.text, stream = new StringStream(txt);
1597 while (stream.pos < ch && !stream.eol()) {
1598 stream.start = stream.pos;
1599 var style = mode.token(stream, state);
1600 }
1601 return {start: stream.start,
1602 end: stream.pos,
1603 string: stream.current(),
1604 className: style || null,
1605 state: state};
1606 },
1607 indentation: function() {return countColumn(this.text);},
1608 // Produces an HTML fragment for the line, taking selection,
1609 // marking, and highlighting into account.
1610 getHTML: function(sfrom, sto, includePre) {
1611 var html = [];
1612 if (includePre)
1613 html.push(this.className ? '<pre class="' + this.className + '">': "<pre>");
1614 function span(text, style) {
1615 if (!text) return;
1616 if (style) html.push('<span class="', style, '">', htmlEscape(text), "</span>");
1617 else html.push(htmlEscape(text));
1618 }
1619 var st = this.styles, allText = this.text, marked = this.marked;
1620 if (sfrom == sto) sfrom = null;
1621
1622 if (!allText)
1623 span(" ", sfrom != null && sto == null ? "CodeMirror-selected" : null);
1624 else if (!marked && sfrom == null)
1625 for (var i = 0, e = st.length; i < e; i+=2) span(st[i], st[i+1]);
1626 else {
1627 var pos = 0, i = 0, text = "", style, sg = 0;
1628 var markpos = -1, mark = null;
1629 function nextMark() {
1630 if (marked) {
1631 markpos += 1;
1632 mark = (markpos < marked.length) ? marked[markpos] : null;
1633 }
1634 }
1635 nextMark();
1636 while (pos < allText.length) {
1637 var upto = allText.length;
1638 var extraStyle = "";
1639 if (sfrom != null) {
1640 if (sfrom > pos) upto = sfrom;
1641 else if (sto == null || sto > pos) {
1642 extraStyle = " CodeMirror-selected";
1643 if (sto != null) upto = Math.min(upto, sto);
1644 }
1645 }
1646 while (mark && mark.to != null && mark.to <= pos) nextMark();
1647 if (mark) {
1648 if (mark.from > pos) upto = Math.min(upto, mark.from);
1649 else {
1650 extraStyle += " " + mark.style;
1651 if (mark.to != null) upto = Math.min(upto, mark.to);
1652 }
1653 }
1654 for (;;) {
1655 var end = pos + text.length;
1656 var apliedStyle = style;
1657 if (extraStyle) apliedStyle = style ? style + extraStyle : extraStyle;
1658 span(end > upto ? text.slice(0, upto - pos) : text, apliedStyle);
1659 if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;}
1660 pos = end;
1661 text = st[i++]; style = st[i++];
1662 }
1663 }
1664 if (sfrom != null && sto == null) span(" ", "CodeMirror-selected");
1665 }
1666 if (includePre) html.push("</pre>");
1667 return html.join("");
1668 }
1669 };
1670 // Utility used by replace and split above
1671 function copyStyles(from, to, source, dest) {
1672 for (var i = 0, pos = 0, state = 0; pos < to; i+=2) {
1673 var part = source[i], end = pos + part.length;
1674 if (state == 0) {
1675 if (end > from) dest.push(part.slice(from - pos, Math.min(part.length, to - pos)), source[i+1]);
1676 if (end >= from) state = 1;
1677 }
1678 else if (state == 1) {
1679 if (end > to) dest.push(part.slice(0, to - pos), source[i+1]);
1680 else dest.push(part, source[i+1]);
1681 }
1682 pos = end;
1683 }
1684 }
1685
1686 // The history object 'chunks' changes that are made close together
1687 // and at almost the same time into bigger undoable units.
1688 function History() {
1689 this.time = 0;
1690 this.done = []; this.undone = [];
1691 }
1692 History.prototype = {
1693 addChange: function(start, added, old) {
1694 this.undone.length = 0;
1695 var time = +new Date, last = this.done[this.done.length - 1];
1696 if (time - this.time > 400 || !last ||
1697 last.start > start + added || last.start + last.added < start - last.added + last.old.length)
1698 this.done.push({start: start, added: added, old: old});
1699 else {
1700 var oldoff = 0;
1701 if (start < last.start) {
1702 for (var i = last.start - start - 1; i >= 0; --i)
1703 last.old.unshift(old[i]);
1704 last.added += last.start - start;
1705 last.start = start;
1706 }
1707 else if (last.start < start) {
1708 oldoff = start - last.start;
1709 added += oldoff;
1710 }
1711 for (var i = last.added - oldoff, e = old.length; i < e; ++i)
1712 last.old.push(old[i]);
1713 if (last.added < added) last.added = added;
1714 }
1715 this.time = time;
1716 }
1717 };
1718
1719 // Event stopping compatibility wrapper.
1720 function stopEvent() {
1721 if (this.preventDefault) {this.preventDefault(); this.stopPropagation();}
1722 else {this.returnValue = false; this.cancelBubble = true;}
1723 }
1724 // Ensure an event has a stop method.
1725 function addStop(event) {
1726 if (!event.stop) event.stop = stopEvent;
1727 return event;
1728 }
1729
1730 // Event wrapper, exposing the few operations we need.
1731 function Event(orig) {this.e = orig;}
1732 Event.prototype = {
1733 stop: function() {stopEvent.call(this.e);},
1734 target: function() {return this.e.target || this.e.srcElement;},
1735 button: function() {
1736 if (this.e.which) return this.e.which;
1737 else if (this.e.button & 1) return 1;
1738 else if (this.e.button & 2) return 3;
1739 else if (this.e.button & 4) return 2;
1740 },
1741 pageX: function() {
1742 if (this.e.pageX != null) return this.e.pageX;
1743 else return this.e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
1744 },
1745 pageY: function() {
1746 if (this.e.pageY != null) return this.e.pageY;
1747 else return this.e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
1748 }
1749 };
1750
1751 // Event handler registration. If disconnect is true, it'll return a
1752 // function that unregisters the handler.
1753 function connect(node, type, handler, disconnect) {
1754 function wrapHandler(event) {handler(new Event(event || window.event));}
1755 if (typeof node.addEventListener == "function") {
1756 node.addEventListener(type, wrapHandler, false);
1757 if (disconnect) return function() {node.removeEventListener(type, wrapHandler, false);};
1758 }
1759 else {
1760 node.attachEvent("on" + type, wrapHandler);
1761 if (disconnect) return function() {node.detachEvent("on" + type, wrapHandler);};
1762 }
1763 }
1764
1765 function Delayed() {this.id = null;}
1766 Delayed.prototype = {set: function(ms, f) {clearTimeout(this.id); this.id = setTimeout(f, ms);}};
1767
1768 // Some IE versions don't preserve whitespace when setting the
1769 // innerHTML of a PRE tag.
1770 var badInnerHTML = (function() {
1771 var pre = document.createElement("pre");
1772 pre.innerHTML = " "; return !pre.innerHTML;
1773 })();
1774
1775 var gecko = /gecko\/\d{7}/i.test(navigator.userAgent);
1776
1777 var lineSep = "\n";
1778 // Feature-detect whether newlines in textareas are converted to \r\n
1779 (function () {
1780 var te = document.createElement("textarea");
1781 te.value = "foo\nbar";
1782 if (te.value.indexOf("\r") > -1) lineSep = "\r\n";
1783 }());
1784
1785 var tabSize = 8;
1786 var mac = /Mac/.test(navigator.platform);
1787 var movementKeys = {};
1788 for (var i = 35; i <= 40; ++i)
1789 movementKeys[i] = movementKeys["c" + i] = true;
1790
1791 // Counts the column offset in a string, taking tabs into account.
1792 // Used mostly to find indentation.
1793 function countColumn(string, end) {
1794 if (end == null) {
1795 end = string.search(/[^\s\u00a0]/);
1796 if (end == -1) end = string.length;
1797 }
1798 for (var i = 0, n = 0; i < end; ++i) {
1799 if (string.charAt(i) == "\t") n += tabSize - (n % tabSize);
1800 else ++n;
1801 }
1802 return n;
1803 }
1804
1805 // Find the position of an element by following the offsetParent chain.
1806 function eltOffset(node) {
1807 var x = 0, y = 0, n2 = node;
1808 for (var n = node; n; n = n.offsetParent) {x += n.offsetLeft; y += n.offsetTop;}
1809 for (var n = node; n != document.body; n = n.parentNode) {x -= n.scrollLeft; y -= n.scrollTop;}
1810 return {left: x, top: y};
1811 }
1812 // Get a node's text content.
1813 function eltText(node) {
1814 return node.textContent || node.innerText || node.nodeValue || "";
1815 }
1816
1817 // Operations on {line, ch} objects.
1818 function posEq(a, b) {return a.line == b.line && a.ch == b.ch;}
1819 function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);}
1820 function copyPos(x) {return {line: x.line, ch: x.ch};}
1821
1822 function htmlEscape(str) {
1823 return str.replace(/[<&]/g, function(str) {return str == "&" ? "&amp;" : "&lt;";});
1824 }
1825
1826 // Used to position the cursor after an undo/redo by finding the
1827 // last edited character.
1828 function editEnd(from, to) {
1829 if (!to) return from ? from.length : 0;
1830 if (!from) return to.length;
1831 for (var i = from.length, j = to.length; i >= 0 && j >= 0; --i, --j)
1832 if (from.charAt(i) != to.charAt(j)) break;
1833 return j + 1;
1834 }
1835
1836 function indexOf(collection, elt) {
1837 if (collection.indexOf) return collection.indexOf(elt);
1838 for (var i = 0, e = collection.length; i < e; ++i)
1839 if (collection[i] == elt) return i;
1840 return -1;
1841 }
1842
1843 // See if "".split is the broken IE version, if so, provide an
1844 // alternative way to split lines.
1845 if ("\n\nb".split(/\n/).length != 3)
1846 var splitLines = function(string) {
1847 var pos = 0, nl, result = [];
1848 while ((nl = string.indexOf("\n", pos)) > -1) {
1849 result.push(string.slice(pos, string.charAt(nl-1) == "\r" ? nl - 1 : nl));
1850 pos = nl + 1;
1851 }
1852 result.push(string.slice(pos));
1853 return result;
1854 };
1855 else
1856 var splitLines = function(string){return string.split(/\r?\n/);};
1857
1858 // Sane model of finding and setting the selection in a textarea
1859 if (window.getSelection) {
1860 var selRange = function(te) {
1861 try {return {start: te.selectionStart, end: te.selectionEnd};}
1862 catch(e) {return null;}
1863 };
1864 var setSelRange = function(te, start, end) {
1865 try {te.setSelectionRange(start, end);}
1866 catch(e) {} // Fails on Firefox when textarea isn't part of the document
1867 };
1868 }
1869 // IE model. Don't ask.
1870 else {
1871 var selRange = function(te) {
1872 try {var range = document.selection.createRange();}
1873 catch(e) {return null;}
1874 if (!range || range.parentElement() != te) return null;
1875 var val = te.value, len = val.length, localRange = te.createTextRange();
1876 localRange.moveToBookmark(range.getBookmark());
1877 var endRange = te.createTextRange();
1878 endRange.collapse(false);
1879
1880 if (localRange.compareEndPoints("StartToEnd", endRange) > -1)
1881 return {start: len, end: len};
1882
1883 var start = -localRange.moveStart("character", -len);
1884 for (var i = val.indexOf("\r"); i > -1 && i < start; i = val.indexOf("\r", i+1), start++) {}
1885
1886 if (localRange.compareEndPoints("EndToEnd", endRange) > -1)
1887 return {start: start, end: len};
1888
1889 var end = -localRange.moveEnd("character", -len);
1890 for (var i = val.indexOf("\r"); i > -1 && i < end; i = val.indexOf("\r", i+1), end++) {}
1891 return {start: start, end: end};
1892 };
1893 var setSelRange = function(te, start, end) {
1894 var range = te.createTextRange();
1895 range.collapse(true);
1896 var endrange = range.duplicate();
1897 var newlines = 0, txt = te.value;
1898 for (var pos = txt.indexOf("\n"); pos > -1 && pos < start; pos = txt.indexOf("\n", pos + 1))
1899 ++newlines;
1900 range.move("character", start - newlines);
1901 for (; pos > -1 && pos < end; pos = txt.indexOf("\n", pos + 1))
1902 ++newlines;
1903 endrange.move("character", end - newlines);
1904 range.setEndPoint("EndToEnd", endrange);
1905 range.select();
1906 };
1907 }
1908
1909 CodeMirror.defineMode("null", function() {
1910 return {token: function(stream) {stream.skipToEnd();}};
1911 });
1912 CodeMirror.defineMIME("text/plain", "null");
1913
1914 return CodeMirror;
1915 })();
@@ -0,0 +1,66 b''
1 <%inherit file="/base/base.html"/>
2
3 <%def name="title()">
4 ${c.repo_name} ${_('Edit file')} - ${c.rhodecode_name}
5 </%def>
6
7 <%def name="js_extra()">
8 <script type="text/javascript" src="${h.url('/js/codemirror.js')}"></script>
9 </%def>
10 <%def name="css_extra()">
11 <link rel="stylesheet" type="text/css" href="${h.url('/css/codemirror.css')}"/>
12 </%def>
13
14 <%def name="breadcrumbs_links()">
15 ${h.link_to(u'Home',h.url('/'))}
16 &raquo;
17 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
18 &raquo;
19 ${_('edit file')} @ R${c.cs.revision}:${h.short_id(c.cs.raw_id)}
20 </%def>
21
22 <%def name="page_nav()">
23 ${self.menu('files')}
24 </%def>
25 <%def name="main()">
26 <div class="box">
27 <!-- box / title -->
28 <div class="title">
29 ${self.breadcrumbs()}
30 <ul class="links">
31 <li>
32 <span style="text-transform: uppercase;">
33 <a href="#">${_('branch')}: ${c.cs.branch}</a></span>
34 </li>
35 </ul>
36 </div>
37 <div class="table">
38 <div id="files_data">
39 <h3 class="files_location">${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.cs.revision,c.file.path)}</h3>
40 ${h.form(h.url.current(),method='post',id='eform')}
41 <div id="body" class="codeblock">
42 <pre id="editor_pre"></pre>
43 <textarea id="editor" name="content" style="display:none">${c.file.content|n}</textarea>
44
45 <div style="padding-top: 10px;">${_('commit message')}</div>
46 <textarea id="commit" name="message" style="height: 100px;width: 99%"></textarea>
47
48 </div>
49 <div style="text-align: right;padding-top: 5px">
50 <input id="reset" type="button" value="${_('Reset')}" class="ui-button-small" />
51 ${h.submit('commit',_('Commit changes'),class_="ui-button-small-blue")}
52 </div>
53 ${h.end_form()}
54 <script type="text/javascript">
55 var myCodeMirror = CodeMirror.fromTextArea(YUD.get('editor'),{
56 mode: "null",
57 lineNumbers:true
58 });
59 YUE.on('reset','click',function(){
60 window.location="${h.url('files_home',repo_name=c.repo_name,revision=c.cs.revision,f_path=c.file.path)}";
61 })
62 </script>
63 </div>
64 </div>
65 </div>
66 </%def> No newline at end of file
@@ -314,6 +314,11 b' def make_map(config):'
314 controller='files', action='annotate', revision='tip',
314 controller='files', action='annotate', revision='tip',
315 f_path='', conditions=dict(function=check_repo))
315 f_path='', conditions=dict(function=check_repo))
316
316
317 rmap.connect('files_edit_home',
318 '/{repo_name:.*}/edit/{revision}/{f_path:.*}',
319 controller='files', action='edit', revision='tip',
320 f_path='', conditions=dict(function=check_repo))
321
317 rmap.connect('files_archive_home', '/{repo_name:.*}/archive/{fname}',
322 rmap.connect('files_archive_home', '/{repo_name:.*}/archive/{fname}',
318 controller='files', action='archivefile',
323 controller='files', action='archivefile',
319 conditions=dict(function=check_repo))
324 conditions=dict(function=check_repo))
@@ -26,31 +26,31 b''
26 import os
26 import os
27 import logging
27 import logging
28 import mimetypes
28 import mimetypes
29 import rhodecode.lib.helpers as h
29 import traceback
30
30
31 from pylons import request, response, session, tmpl_context as c, url
31 from pylons import request, response, session, tmpl_context as c, url
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34
34
35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
36 from rhodecode.lib.base import BaseRepoController, render
37 from rhodecode.lib.utils import EmptyChangeset
38 from rhodecode.model.repo import RepoModel
39
40 from vcs.backends import ARCHIVE_SPECS
35 from vcs.backends import ARCHIVE_SPECS
41 from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
36 from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
42 EmptyRepositoryError, ImproperArchiveTypeError, VCSError
37 EmptyRepositoryError, ImproperArchiveTypeError, VCSError
43 from vcs.nodes import FileNode, NodeKind
38 from vcs.nodes import FileNode, NodeKind
44 from vcs.utils import diffs as differ
39 from vcs.utils import diffs as differ
45
40
41 from rhodecode.lib import convert_line_endings, detect_mode
42 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
43 from rhodecode.lib.base import BaseRepoController, render
44 from rhodecode.lib.utils import EmptyChangeset
45 import rhodecode.lib.helpers as h
46 from rhodecode.model.repo import RepoModel
47
46 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
47
49
48
50
49 class FilesController(BaseRepoController):
51 class FilesController(BaseRepoController):
50
52
51 @LoginRequired()
53 @LoginRequired()
52 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
53 'repository.admin')
54 def __before__(self):
54 def __before__(self):
55 super(FilesController, self).__before__()
55 super(FilesController, self).__before__()
56 c.cut_off_limit = self.cut_off_limit
56 c.cut_off_limit = self.cut_off_limit
@@ -95,6 +95,8 b' class FilesController(BaseRepoController'
95
95
96 return file_node
96 return file_node
97
97
98 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
99 'repository.admin')
98 def index(self, repo_name, revision, f_path):
100 def index(self, repo_name, revision, f_path):
99 #reditect to given revision from form if given
101 #reditect to given revision from form if given
100 post_revision = request.POST.get('at_rev', None)
102 post_revision = request.POST.get('at_rev', None)
@@ -144,6 +146,8 b' class FilesController(BaseRepoController'
144
146
145 return render('files/files.html')
147 return render('files/files.html')
146
148
149 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
150 'repository.admin')
147 def rawfile(self, repo_name, revision, f_path):
151 def rawfile(self, repo_name, revision, f_path):
148 cs = self.__get_cs_or_redirect(revision, repo_name)
152 cs = self.__get_cs_or_redirect(revision, repo_name)
149 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
153 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
@@ -154,6 +158,8 b' class FilesController(BaseRepoController'
154 response.content_type = file_node.mimetype
158 response.content_type = file_node.mimetype
155 return file_node.content
159 return file_node.content
156
160
161 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
162 'repository.admin')
157 def raw(self, repo_name, revision, f_path):
163 def raw(self, repo_name, revision, f_path):
158 cs = self.__get_cs_or_redirect(revision, repo_name)
164 cs = self.__get_cs_or_redirect(revision, repo_name)
159 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
165 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
@@ -198,6 +204,8 b' class FilesController(BaseRepoController'
198 response.content_type = mimetype
204 response.content_type = mimetype
199 return file_node.content
205 return file_node.content
200
206
207 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
208 'repository.admin')
201 def annotate(self, repo_name, revision, f_path):
209 def annotate(self, repo_name, revision, f_path):
202 c.cs = self.__get_cs_or_redirect(revision, repo_name)
210 c.cs = self.__get_cs_or_redirect(revision, repo_name)
203 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
211 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
@@ -206,6 +214,54 b' class FilesController(BaseRepoController'
206 c.f_path = f_path
214 c.f_path = f_path
207 return render('files/files_annotate.html')
215 return render('files/files_annotate.html')
208
216
217 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
218 def edit(self, repo_name, revision, f_path):
219 r_post = request.POST
220
221 if c.rhodecode_repo.alias == 'hg':
222 from vcs.backends.hg import MercurialInMemoryChangeset as IMC
223 elif c.rhodecode_repo.alias == 'git':
224 from vcs.backends.git import GitInMemoryChangeset as IMC
225
226 c.cs = self.__get_cs_or_redirect(revision, repo_name)
227 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
228
229 c.file_history = self._get_node_history(c.cs, f_path)
230 c.f_path = f_path
231
232 if r_post:
233
234 old_content = c.file.content
235 # modes: 0 - Unix, 1 - Mac, 2 - DOS
236 mode = detect_mode(old_content.splitlines(1)[0], 0)
237 content = convert_line_endings(r_post.get('content'), mode)
238 message = r_post.get('message') or (_('Edited %s via RhodeCode')
239 % (f_path))
240
241 if content == old_content:
242 h.flash(_('No changes'),
243 category='warning')
244 return redirect(url('changeset_home',
245 repo_name=c.repo_name, revision='tip'))
246 try:
247 new_node = FileNode(f_path, content)
248 m = IMC(c.rhodecode_repo)
249 m.change(new_node)
250 m.commit(message=message,
251 author=self.rhodecode_user.full_contact,
252 parents=[c.cs], branch=c.cs.branch)
253 h.flash(_('Successfully committed to %s' % f_path),
254 category='success')
255 except Exception, e:
256 log.error(traceback.format_exc())
257 h.flash(_('Error occurred during commit'), category='error')
258 return redirect(url('changeset_home',
259 repo_name=c.repo_name, revision='tip'))
260
261 return render('files/files_edit.html')
262
263 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
264 'repository.admin')
209 def archivefile(self, repo_name, fname):
265 def archivefile(self, repo_name, fname):
210
266
211 fileformat = None
267 fileformat = None
@@ -239,6 +295,8 b' class FilesController(BaseRepoController'
239
295
240 return cs.get_chunked_archive(stream=None, kind=fileformat)
296 return cs.get_chunked_archive(stream=None, kind=fileformat)
241
297
298 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
299 'repository.admin')
242 def diff(self, repo_name, f_path):
300 def diff(self, repo_name, f_path):
243 diff1 = request.GET.get('diff1')
301 diff1 = request.GET.get('diff1')
244 diff2 = request.GET.get('diff2')
302 diff2 = request.GET.get('diff2')
@@ -282,7 +340,6 b' class FilesController(BaseRepoController'
282 return diff.raw_diff()
340 return diff.raw_diff()
283
341
284 elif c.action == 'diff':
342 elif c.action == 'diff':
285
286 if node1.is_binary or node2.is_binary:
343 if node1.is_binary or node2.is_binary:
287 c.cur_diff = _('Binary file')
344 c.cur_diff = _('Binary file')
288 elif node1.size > self.cut_off_limit or \
345 elif node1.size > self.cut_off_limit or \
@@ -82,6 +82,30 b' def str2bool(_str):'
82 _str = str(_str).strip().lower()
82 _str = str(_str).strip().lower()
83 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
83 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
84
84
85 def convert_line_endings(temp, mode):
86 from string import replace
87 #modes: 0 - Unix, 1 - Mac, 2 - DOS
88 if mode == 0:
89 temp = replace(temp, '\r\n', '\n')
90 temp = replace(temp, '\r', '\n')
91 elif mode == 1:
92 temp = replace(temp, '\r\n', '\r')
93 temp = replace(temp, '\n', '\r')
94 elif mode == 2:
95 import re
96 temp = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", temp)
97 return temp
98
99
100 def detect_mode(line, default):
101 if line.endswith('\r\n'):
102 return 2
103 elif line.endswith('\n'):
104 return 0
105 elif line.endswith('\r'):
106 return 1
107 else:
108 return default
85
109
86 def generate_api_key(username, salt=None):
110 def generate_api_key(username, salt=None):
87 """
111 """
@@ -278,6 +278,10 b' class AuthUser(object):'
278 def is_admin(self):
278 def is_admin(self):
279 return self.admin
279 return self.admin
280
280
281 @property
282 def full_contact(self):
283 return '%s %s <%s>' % (self.name, self.lastname, self.email)
284
281 def __repr__(self):
285 def __repr__(self):
282 return "<AuthUser('id:%s:%s|%s')>" % (self.user_id, self.username,
286 return "<AuthUser('id:%s:%s|%s')>" % (self.user_id, self.username,
283 self.is_authenticated)
287 self.is_authenticated)
@@ -2204,6 +2204,15 b' outline:none;'
2204 margin:0;
2204 margin:0;
2205 }
2205 }
2206
2206
2207 #content div.box input.ui-button-small-blue {
2208 background:#4e85bb url("../images/button_highlight.png") repeat-x;
2209 border-top:1px solid #5c91a4;
2210 border-left:1px solid #2a6f89;
2211 border-right:1px solid #2b7089;
2212 border-bottom:1px solid #1a6480;
2213 color:#fff;
2214 }
2215
2207 #content div.box input.ui-button-small submit,button{
2216 #content div.box input.ui-button-small submit,button{
2208 cursor: pointer;
2217 cursor: pointer;
2209 }
2218 }
@@ -267,8 +267,8 b''
267 <li>${h.link_to(_('fork'),h.url('repo_fork_home',repo_name=c.repo_name),class_='fork')}</li>
267 <li>${h.link_to(_('fork'),h.url('repo_fork_home',repo_name=c.repo_name),class_='fork')}</li>
268 <li>${h.link_to(_('search'),h.url('search_repo',search_repo=c.repo_name),class_='search')}</li>
268 <li>${h.link_to(_('search'),h.url('search_repo',search_repo=c.repo_name),class_='search')}</li>
269
269
270 %if h.HasPermissionAll('hg.admin')('access admin main page'):
270 % if h.HasPermissionAll('hg.admin')('access admin main page'):
271 <li>
271 <li>
272 ${h.link_to(_('admin'),h.url('admin_home'),class_='admin')}
272 ${h.link_to(_('admin'),h.url('admin_home'),class_='admin')}
273 <%def name="admin_menu()">
273 <%def name="admin_menu()">
274 <ul>
274 <ul>
@@ -283,9 +283,8 b''
283 </%def>
283 </%def>
284
284
285 ${admin_menu()}
285 ${admin_menu()}
286 </li>
286 </li>
287 %endif
287 % endif
288
289 </ul>
288 </ul>
290 </li>
289 </li>
291
290
@@ -306,8 +305,6 b''
306 </a>
305 </a>
307 </li>
306 </li>
308
307
309
310
311 </ul>
308 </ul>
312 %else:
309 %else:
313 ##ROOT MENU
310 ##ROOT MENU
@@ -320,16 +317,16 b''
320 <span>${_('Home')}</span>
317 <span>${_('Home')}</span>
321 </a>
318 </a>
322 </li>
319 </li>
323 %if c.rhodecode_user.username != 'default':
320 % if c.rhodecode_user.username != 'default':
324 <li>
321 <li>
325 <a title="${_('Journal')}" href="${h.url('journal')}">
322 <a title="${_('Journal')}" href="${h.url('journal')}">
326 <span class="icon">
323 <span class="icon">
327 <img src="${h.url("/images/icons/book.png")}" alt="${_('Journal')}" />
324 <img src="${h.url("/images/icons/book.png")}" alt="${_('Journal')}" />
328 </span>
325 </span>
329 <span>${_('Journal')}</span>
326 <span>${_('Journal')}</span>
330 </a>
327 </a>
331 </li>
328 </li>
332 %endif
329 % endif
333 <li>
330 <li>
334 <a title="${_('Search')}" href="${h.url('search')}">
331 <a title="${_('Search')}" href="${h.url('search')}">
335 <span class="icon">
332 <span class="icon">
@@ -37,11 +37,13 b''
37 </html>
37 </html>
38
38
39 <%def name="css()">
39 <%def name="css()">
40 <link rel="stylesheet" type="text/css" href="${h.url('/css/style.css')}" media="screen" />
40 <link rel="stylesheet" type="text/css" href="${h.url('/css/style.css')}" media="screen"/>
41 <link rel="stylesheet" type="text/css" href="${h.url('/css/pygments.css')}" />
41 <link rel="stylesheet" type="text/css" href="${h.url('/css/pygments.css')}"/>
42 <link rel="stylesheet" type="text/css" href="${h.url('/css/diff.css')}" />
42 <link rel="stylesheet" type="text/css" href="${h.url('/css/diff.css')}"/>
43 ${self.css_extra()}
43 </%def>
44 </%def>
44
45 <%def name="css_extra()">
46 </%def>
45 <%def name="js()">
47 <%def name="js()">
46 <script type="text/javascript">
48 <script type="text/javascript">
47 if (typeof console == "undefined" || typeof console.log == "undefined")
49 if (typeof console == "undefined" || typeof console.log == "undefined")
@@ -61,7 +63,7 b''
61 <![endif]-->
63 <![endif]-->
62 <script type="text/javascript" src="${h.url('/js/yui.flot.js')}"></script>
64 <script type="text/javascript" src="${h.url('/js/yui.flot.js')}"></script>
63 % endif
65 % endif
64
66 ${self.js_extra()}
65 <script type="text/javascript">
67 <script type="text/javascript">
66 var YUC = YAHOO.util.Connect;
68 var YUC = YAHOO.util.Connect;
67 var YUD = YAHOO.util.Dom;
69 var YUD = YAHOO.util.Dom;
@@ -123,4 +125,6 b''
123 }
125 }
124 </script>
126 </script>
125
127
128 </%def>
129 <%def name="js_extra()">
126 </%def> No newline at end of file
130 </%def>
@@ -44,6 +44,10 b''
44 h.url('files_raw_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path))}
44 h.url('files_raw_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path))}
45 / ${h.link_to(_('download as raw'),
45 / ${h.link_to(_('download as raw'),
46 h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path))}
46 h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path))}
47 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
48 / ${h.link_to(_('edit'),
49 h.url('files_edit_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path))}
50 % endif
47 </dd>
51 </dd>
48 <dt>${_('History')}</dt>
52 <dt>${_('History')}</dt>
49 <dd>
53 <dd>
@@ -15,6 +15,10 b''
15 h.url('files_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path))}
15 h.url('files_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path))}
16 / ${h.link_to(_('download as raw'),
16 / ${h.link_to(_('download as raw'),
17 h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path))}
17 h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path))}
18 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
19 / ${h.link_to(_('edit'),
20 h.url('files_edit_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path))}
21 % endif
18 </dd>
22 </dd>
19 <dt>${_('History')}</dt>
23 <dt>${_('History')}</dt>
20 <dd>
24 <dd>
General Comments 0
You need to be logged in to leave comments. Login now