Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'master' into live-cache

  • Loading branch information...
commit 8bb04d06b7d90c4d09211af1f66c3bb377f8c669 2 parents 5b0004c + e55f441
@miccolis miccolis authored
Showing with 1,269 additions and 533 deletions.
  1. +7 −0 README.md
  2. +16 −10 _layouts/default.html
  3. +14 −11 _posts/0100-01-01-CHANGELOG.md
  4. +10 −2 _posts/docs/0201-01-01-upgrade.md
  5. +1 −1  _posts/docs/manual/0202-02-01-usage.md
  6. +3 −3 _posts/docs/tutorials/0202-01-01-virtualbox.md
  7. +2 −2 _posts/feature/0200-01-05-download.md
  8. +65 −17 assets/js/codemirror.carto.complete.js
  9. +3 −0  assets/js/codemirror.carto.js
  10. +1,047 −419 assets/js/codemirror.js
  11. +15 −19 lib/fsutil.js
  12. +2 −0  models/Library.server.bones
  13. +1 −1  models/Project.bones
  14. +8 −2 models/Project.server.bones
  15. +3 −4 package.json
  16. +1 −1  platforms/osx/MBTiles Spotlight Importer/Info.plist
  17. +2 −2 platforms/osx/MBTiles Spotlight Importer/MBTiles Spotlight Importer.xcodeproj/project.pbxproj
  18. +4 −6 platforms/osx/MBTilesQuickLook/MBTilesQuickLook.xcodeproj/project.pbxproj
  19. +1 −1  platforms/osx/MBTilesQuickLook/MBTilesQuickLook/MBTilesQuickLook-Info.plist
  20. +3 −2 platforms/osx/Makefile
  21. +2 −8 platforms/osx/PACKAGE.md
  22. +4 −0 platforms/osx/TileMill.xcodeproj/project.pbxproj
  23. +22 −21 platforms/virtualbox/README.md
  24. +9 −0 templates/DatasourceRows._
  25. +24 −1 views/Project.bones
View
7 README.md
@@ -5,6 +5,13 @@ on the [TileMill website](http://mapbox.com/tilemill).
- [Download and install](http://mapbox.com/tilemill/download/)
- [Build from source](http://mapbox.com/tilemill/docs/source/)
+# Running tests
+
+Install expresso and run the tests
+
+ npm install expresso
+ npm test
+
# Viewing docs locally
## Install jekyll
View
26 _layouts/default.html
@@ -2,6 +2,9 @@
# MapBox project pages template
# -----------------------------
navigation:
+ - title: Maps
+ path: http://tiles.mapbox.com
+ category: maps
- title: Design
path: /tilemill
category: design
@@ -11,9 +14,6 @@
- title: Support
path: /support
category: support
- - title: Showcase
- path: /showcase
- category: showcase
- title: Documentation
path: /documentation
category: documentation
@@ -35,11 +35,11 @@
<meta charset='UTF-8'/>
{% if page.description %}<meta name='description' content='{{page.description}}'/>{% endif %}
<title>{{page.title}} | MapBox</title>
- <link rel='stylesheet' href='{{site.url}}/site.css' type='text/css'/>
- <!--[if IE]><link rel='stylesheet' href='{{site.url}}/site-ie.css' type='text/css'/><![endif]-->
+ <link rel='stylesheet' href='{{site.url}}/css/site.css' type='text/css'/>
+ <!--[if IE]><link rel='stylesheet' href='{{site.url}}/css/site-ie.css' type='text/css'/><![endif]-->
<link rel='stylesheet' href='{{site.url}}/tilemill/assets/pages/site.css' type='text/css'/>
<link rel='shortcut icon' href='{{site.url}}/images/favicon.ico' type='image/x-icon' />
- <script src='{{site.url}}/site.js' type='text/javascript'></script>
+ <script src='{{site.url}}/js/site.js' type='text/javascript'></script>
<script src='{{site.url}}/tilemill/assets/pages/site.js' type='text/javascript'></script>
</head>
<body {% if page.class %}class='{{page.class}}'{% endif %}><div class='wrapper'>
@@ -49,14 +49,20 @@
<ul class='menu'>
{% for item in page.navigation %}
<li class='{{item.category}} {% if page.category contains item.category or item.category == page.section %}active{% endif %}'>
+ {% if item.path contains '://' %}
+ <a href='{{item.path}}'><span class='icon'></span>{{item.title}}</a>
+ {% else %}
<a href='{{site.url}}{{item.path}}'><span class='icon'></span>{{item.title}}</a>
+ {% endif %}
</li>
{% endfor %}
</ul>
- <a class='goto' href='http://tiles.mapbox.com'>
- <span class='icon'></span>
- Login <em>to MapBox</em>
- </a>
+ <small class='auth'>
+ <a class='login' href='http://tiles.mapbox.com/login'>
+ <span class='icon'></span>
+ My account
+ </a>
+ </small>
</div></div>
{{content}}
View
25 _posts/0100-01-01-CHANGELOG.md
@@ -14,17 +14,20 @@ title: Changelog
permalink: /docs/changelog
releases:
-# - version x.x
-#
-# notes:
-# - Fixed a bug in the Mac app where saving files such as exports would not obey custom filenames entered.
-# - Ensured that saved files automatically contain the original file's extension if a custom filename is entered.
-# - Added autocomplete in Carto editor for properties, variables and values. Press 'tab' to activate.
-# - Upload MBTiles to MapBox Hosting directly from export menu.
-# - Includes SQLite fixes from Mapnik which ensure features are not missing and all join types are supported.
-# - Significant changes to how interactivity is authored and exported.
-# [Mustache](http://mustache.github.com/) templates are now used instead of pure JavaScript.
-# - Add ability to browse entire local filesystem when adding or editing a layer.
+- version: 0.7.0
+ date: 2011-11-20
+ size: 58399816
+ sign: MC0CFCDhkHU1BTJ32n/AWGv+vDSREAFKAhUAjhIqAUqtqlfhhBltAxJFBa0f/gg=
+
+ notes:
+ - Fixed a bug in the Mac app where saving files such as exports would not obey custom filenames entered.
+ - Ensured that saved files automatically contain the original file's extension if a custom filename is entered.
+ - Added autocomplete in Carto editor for properties, variables and values. Press 'tab' to activate.
+ - Upload MBTiles to MapBox Hosting directly from export menu.
+ - Includes SQLite fixes from Mapnik which ensure features are not missing and all join types are supported.
+ - Significant changes to how interactivity is authored and exported.
+ [Mustache](http://mustache.github.com/) templates are now used instead of pure JavaScript.
+ - Add ability to browse entire local filesystem when adding or editing a layer.
- version: 0.6.2
date: 2011-11-01
View
12 _posts/docs/0201-01-01-upgrade.md
@@ -13,9 +13,17 @@ In TileMill 0.6 connections are now only accepted from the loopback interface by
### Upgrading on Ubuntu
-To upgrade run:
+The MapBox PPA now includes its own packages for nodejs. You must remove the chris-lea nodejs PPA from your sources list and uninstall nodejs before upgrading TileMill. Refer to [this issue](https://github.com/mapbox/tilemill/issues/910#issuecomment-2687449) for more information.
- apt-get install tilemill libmapnik2
+To remove the previous nodejs packages run:
+
+ sudo apt-get remove nodejs
+ sudo rm /etc/apt/sources.list.d/chris-lea-node_js-*.list
+
+To upgrade TileMill run:
+
+ sudo apt-get update
+ sudo apt-get install tilemill libmapnik2
#### Application data directory on Ubuntu
View
2  _posts/docs/manual/0202-02-01-usage.md
@@ -31,7 +31,7 @@ TileMill configuration should be provided in JSON format. The configuration belo
{
"port": 3001,
- "listenHost": '0.0.0.0'
+ "listenHost": "0.0.0.0"
}
The most commonly used options include:
View
6 _posts/docs/tutorials/0202-01-01-virtualbox.md
@@ -8,11 +8,11 @@ permalink: /docs/tutorials/virtualbox
[VirtualBox](http://www.virtualbox.org) is software that runs on all operating systems and allows you to launch Linux as an application. It is then easy to start and stop Linux when you need and safe to cleanly remove at any time.
-We provide an Ubuntu "Natty" Linux (32 bit) machine with the latest TileMill release pre-installed and running when you boot up. This provides a great way for Windows users to learn and experiment with TileMill without having to upgrade their operating system.
+We provide an Ubuntu Linux (32 bit) machine with the latest TileMill release pre-installed and running when you boot up. This provides a great way for Windows users to learn and experiment with TileMill without having to upgrade their operating system.
## Getting Started
-First start [downloading the TileMill 0.6.0 VM](http://tilemill-vm.s3.amazonaws.com/TileMill-0.6.0-32bit.ova) (1.4 GB).
+First start [downloading the TileMill 0.7.0 VM](http://tilemill-vm.s3.amazonaws.com/TileMill-0.7.0-32bit.ova) (1.3 GB).
Then make sure you have installed [VirtualBox 4.0.x](http://www.virtualbox.org/wiki/Downloads) or greater.
@@ -27,7 +27,7 @@ The new machine should now appear in your VirtualBox menu of machines. Boot the
The main account is an administrative user with the username "ubuntu" and password "ubuntu".
-After logging in you will find TileMill running at http://localhost:8889/ and to use it just open Firefox and view that url (there is a browser toolbar bookmark for you too).
+After logging in you will find a TileMill icon on the desktop. Double click it to start TileMill and it will then be available in a browser at http://localhost:8889/. To start making maps just open Firefox and view that url (there is a browser toolbar bookmark for you too).
## Next Steps
View
4 _posts/feature/0200-01-05-download.md
@@ -10,7 +10,7 @@ hero: |
<h1 class='title centered'>Mac OS X</h1>
<img class='centered' src='/tilemill/assets/pages/tilemill-osx.jpg' />
<div class='centered'>
- <a class='button' href='https://github.com/downloads/mapbox/tilemill/TileMill-0.6.2.zip'>TileMill-0.6.2.zip<small>58.4 MB</small></a>
+ <a class='button' href='https://github.com/downloads/mapbox/tilemill/TileMill-0.7.0.zip'>TileMill-0.7.0.zip<small>55.7 MB</small></a>
<small>Extract this archive and drag TileMill to your Applications folder.</small>
</div>
</div>
@@ -41,7 +41,7 @@ Extract this archive and double-click the TileMill icon to start. Learn how to u
## Ubuntu Linux
### Requirements
<ul class='checklist'>
- <li class='check'>Ubuntu 10.10/11.04</li>
+ <li class='check'>Ubuntu 10.10, 11.04 or 11.10</li>
<li class='check'>2 GB memory</li>
<li class='check'>A modern browser (Chrome, Firefox)</li>
<li class='check'>Internet connection for remote datasources</li>
View
82 assets/js/codemirror.carto.complete.js
@@ -6,11 +6,17 @@
function cartoCompletion(editor, reference) {
var widget = document.createElement('div'),
sel = widget.appendChild(document.createElement('select')),
+ ids = [],
+ classes = [],
$widget = $(widget),
$sel = $(sel);
function cancelEvent(e) {
if (!e) return;
+ e.cancelBubble = true;
+ e.cancel = true;
+ e.returnValue = false;
+ if (e.stop) e.stop();
if (e.stopPropagation) { e.stopPropagation(); }
if (e.preventDefault) { e.preventDefault(); }
}
@@ -25,31 +31,50 @@
return _.uniq(ids);
})(reference);
- var valid_keywords = (function(reference) {
- var ids = [];
- for (var i in reference.symbolizers) {
- for (var j in reference.symbolizers[i]) {
- if (typeof reference.symbolizers[i][j].type == 'object') {
- for (var k in reference.symbolizers[i][j].type) {
- ids.push(reference.symbolizers[i][j].type[k]);
- }
+ var valid_keywords = [];
+ var kw_by_property = {};
+ var kw_reference = {};
+ for (var i in reference.symbolizers) {
+ for (var j in reference.symbolizers[i]) {
+ kw_reference[reference.symbolizers[i][j].css] = reference.symbolizers[i][j].doc || '';
+ if (typeof reference.symbolizers[i][j].type == 'object') {
+ var css = reference.symbolizers[i][j].css;
+ for (var k in reference.symbolizers[i][j].type) {
+ valid_keywords.push(reference.symbolizers[i][j].type[k]);
+ if (!kw_by_property[css]) kw_by_property[css] = [];
+ kw_by_property[css].push(reference.symbolizers[i][j].type[k]);
}
}
}
- return _.uniq(ids);
- })(reference);
+ }
+ valid_keywords = _.uniq(valid_keywords);
function getVariables() {
var v = editor.getValue();
return _.uniq(v.match(/@[\w\d\-]+/));
}
- function getCompletions(token, context) {
+ function getCompletions(token, cur) {
var against;
if (token.className === 'carto-value') {
- against = valid_keywords;
+ var l = editor.getLine(cur.line);
+ var start = l.match(/\w/);
+ var p = editor.getTokenAt({
+ line: cur.line,
+ ch: start.index + 1
+ });
+ if (p && p.className === 'carto-valid-identifier' &&
+ kw_by_property[p.string]) {
+ against = kw_by_property[p.string];
+ }
} else if (token.className === 'carto-variable') {
against = getVariables();
+ } else if (token.className === 'carto-selector') {
+ if (token.string[0] == '.') {
+ against = classes;
+ } else {
+ against = ids;
+ }
} else {
against = valid_identifiers;
}
@@ -61,6 +86,8 @@
return i + ';';
} else if (token.className === 'carto-variable') {
return i;
+ } else if (token.className === 'carto-selector') {
+ return i + ' {';
} else {
return i + ':';
}
@@ -78,9 +105,11 @@
token = editor.getTokenAt(cur),
done = false;
- if (!/@?[\w-$_]+$/.test(token.string)) {
+ // If this is not on a token that's autocompletable,
+ // insert a tab.
+ if (!/(@|#|\.)?[\w-$_]+$/.test(token.string)) {
editor.focus();
- return false;
+ return !/^\s*$/.test(token.string);
}
function insert(str) {
@@ -117,9 +146,9 @@
}, 50);
}
- var completions = getCompletions(token, context);
+ var completions = getCompletions(token, cur);
if (!completions.length) {
- return false;
+ return true;
} else if (completions.length == 1) {
insert(completions[0]); return true;
}
@@ -134,6 +163,7 @@
opt.appendChild(document.createTextNode(completions[i]));
}
sel.firstChild.selected = true;
+ sel.selectedIndex = 0;
sel.size = Math.min(10, completions.length);
sel.style.height = '100px';
@@ -189,13 +219,31 @@
return true;
}
+ function setTitles() {
+ var wrap = editor.getWrapperElement();
+ var ids = $('.cm-carto-valid-identifier', wrap).each(function() {
+ if (kw_reference[this.innerHTML]) {
+ this.title = kw_reference[this.innerHTML];
+ }
+ });
+ }
+
return {
onKeyEvent: function(i, e) {
// Hook into tab
if (e.which == 9 && !(e.ctrlKey || e.metaKey) && !e.altKey) {
- cancelEvent(e);
+ e.stop();
return complete(e);
}
+ },
+ setTitles: function() {
+ setTitles();
+ },
+ ids: function(x) {
+ ids = x;
+ },
+ classes: function(x) {
+ classes = x;
}
};
}
View
3  assets/js/codemirror.carto.js
@@ -57,6 +57,9 @@ CodeMirror.defineMode('carto', function(config, parserConfig) {
} else if (ch == '#') {
stream.eatWhile(/[\w\-]/);
return ret('carto-selector', 'hash');
+ } else if (ch == '.') {
+ stream.eatWhile(/[\w\-]/);
+ return ret('carto-selector', 'hash');
} else if (/\-|\d/.test(ch)) {
stream.eatWhile(/[\w.%]/);
return ret('carto-unit', 'unit');
View
1,466 assets/js/codemirror.js
@@ -1,3 +1,5 @@
+// CodeMirror v2.18
+
// All functions that need access to the editor's state live inside
// the CodeMirror function. Below that, at the bottom of the file,
// some utilities are defined.
@@ -16,18 +18,19 @@ var CodeMirror = (function() {
var targetDocument = options["document"];
// The element in which the editor lives.
var wrapper = targetDocument.createElement("div");
- wrapper.className = "CodeMirror";
+ wrapper.className = "CodeMirror" + (options.lineWrapping ? " CodeMirror-wrap" : "");
// This mess creates the base DOM structure for the editor.
wrapper.innerHTML =
'<div style="overflow: hidden; position: relative; width: 1px; height: 0px;">' + // Wraps and hides input textarea
- '<textarea style="position: absolute; width: 2px;" wrap="off"></textarea></div>' +
+ '<textarea style="position: absolute; width: 10000px;" wrap="off" ' +
+ 'autocorrect="off" autocapitalize="off"></textarea></div>' +
'<div class="CodeMirror-scroll cm-s-' + options.theme + '">' +
'<div style="position: relative">' + // Set to the height of the text, causes scrolling
- '<div style="position: absolute; height: 0; width: 0; overflow: hidden;"></div>' +
'<div style="position: relative">' + // Moved around its parent to cover visible view
'<div class="CodeMirror-gutter"><div class="CodeMirror-gutter-text"></div></div>' +
// Provides positioning relative to (visible) text origin
'<div class="CodeMirror-lines"><div style="position: relative">' +
+ '<div style="position: absolute; width: 100%; height: 0; overflow: hidden; visibility: hidden"></div>' +
'<pre class="CodeMirror-cursor">&#160;</pre>' + // Absolutely positioned blinky cursor
'<div></div>' + // This DIV contains the actual code
'</div></div></div></div></div>';
@@ -35,21 +38,29 @@ var CodeMirror = (function() {
// I've never seen more elegant code in my life.
var inputDiv = wrapper.firstChild, input = inputDiv.firstChild,
scroller = wrapper.lastChild, code = scroller.firstChild,
- measure = code.firstChild, mover = measure.nextSibling,
- gutter = mover.firstChild, gutterText = gutter.firstChild,
- lineSpace = gutter.nextSibling.firstChild,
- cursor = lineSpace.firstChild, lineDiv = cursor.nextSibling;
+ mover = code.firstChild, gutter = mover.firstChild, gutterText = gutter.firstChild,
+ lineSpace = gutter.nextSibling.firstChild, measure = lineSpace.firstChild,
+ cursor = measure.nextSibling, lineDiv = cursor.nextSibling;
+ if (!webkit) lineSpace.draggable = true;
if (options.tabindex != null) input.tabindex = options.tabindex;
if (!options.gutter && !options.lineNumbers) gutter.style.display = "none";
+ // Check for problem with IE innerHTML not working when we have a
+ // P (or similar) parent node.
+ try { stringWidth("x"); }
+ catch (e) {
+ if (e.message.match(/unknown runtime/i))
+ e = new Error("A CodeMirror inside a P-style element does not work in Internet Explorer. (innerHTML bug)");
+ throw e;
+ }
+
// Delayed object wrap timeouts, making sure only one is active. blinker holds an interval.
var poll = new Delayed(), highlight = new Delayed(), blinker;
- // mode holds a mode API object. lines an array of Line objects
- // (see Line constructor), work an array of lines that should be
- // parsed, and history the undo history (instance of History
- // constructor).
- var mode, lines = [new Line("")], work, history = new History(), focused;
+ // mode holds a mode API object. doc is the tree of Line objects,
+ // work an array of lines that should be parsed, and history the
+ // undo history (instance of History constructor).
+ var mode, doc = new BranchChunk([new LeafChunk([new Line("")])]), work, focused;
loadMode();
// The selection. These are always maintained to point at valid
// positions. Inverted is used to remember that the user is
@@ -59,12 +70,12 @@ var CodeMirror = (function() {
// whether the user is holding shift. reducedSelection is a hack
// to get around the fact that we can't create inverted
// selections. See below.
- var shiftSelecting, reducedSelection, lastDoubleClick;
+ var shiftSelecting, reducedSelection, lastClick, lastDoubleClick, draggingText;
// Variables used by startOperation/endOperation to track what
// happened during the operation.
- var updateInput, changes, textChanged, selectionChanged, leaveInputAlone;
+ var updateInput, changes, textChanged, selectionChanged, leaveInputAlone, gutterDirty;
// Current visible range (may be bigger than the view window).
- var showingFrom = 0, showingTo = 0, lastHeight = 0, curKeyId = null;
+ var displayOffset = 0, showingFrom = 0, showingTo = 0, lastHeight = 0, curKeyId = null;
// editing will hold an object describing the things we put in the
// textarea, to help figure out whether something changed.
// bracketHighlighted is used to remember that a backet has been
@@ -76,17 +87,33 @@ var CodeMirror = (function() {
// Initialize the content.
operation(function(){setValue(options.value || ""); updateInput = false;})();
+ var history = new History();
+
+ var slowPollInterval = 2000;
+ // Gecko and Opera Linux do not reliably fire any event when starting an IME compose
+ var alwaysPollForIME = (!win && !mac) && (gecko || window.opera);
+ if (options.pollForIME && alwaysPollForIME) slowPollInterval = 50;
+ function keyMightStartIME(keyCode) {
+ return (win && ((gecko && keyCode == 229) || (window.opera && keyCode == 197))) || (mac && gecko);
+ }
// Register our event handlers.
connect(scroller, "mousedown", operation(onMouseDown));
+ connect(scroller, "dblclick", operation(onDoubleClick));
+ connect(lineSpace, "dragstart", onDragStart);
+ connect(lineSpace, "selectstart", e_preventDefault);
// Gecko browsers fire contextmenu *after* opening the menu, at
// which point we can't mess with it anymore. Context menu is
// handled in onMouseDown for Gecko.
if (!gecko) connect(scroller, "contextmenu", onContextMenu);
- connect(code, "dblclick", operation(onDblClick));
- connect(scroller, "scroll", function() {updateDisplay([]); if (options.onScroll) options.onScroll(instance);});
+ connect(scroller, "scroll", function() {
+ updateDisplay([]);
+ if (options.fixedGutter) gutter.style.left = scroller.scrollLeft + "px";
+ if (options.onScroll) options.onScroll(instance);
+ });
connect(window, "resize", function() {updateDisplay(true);});
connect(input, "keyup", operation(onKeyUp));
+ connect(input, "input", function() {fastPoll(curKeyId);});
connect(input, "keydown", operation(onKeyDown));
connect(input, "keypress", operation(onKeyPress));
connect(input, "focus", onFocus);
@@ -98,31 +125,35 @@ var CodeMirror = (function() {
connect(scroller, "paste", function(){focusInput(); fastPoll();});
connect(input, "paste", function(){fastPoll();});
connect(input, "cut", function(){fastPoll();});
-
- // IE throws unspecified error in certain cases, when
+
+ // IE throws unspecified error in certain cases, when
// trying to access activeElement before onload
var hasFocus; try { hasFocus = (targetDocument.activeElement == input); } catch(e) { }
if (hasFocus) setTimeout(onFocus, 20);
else onBlur();
- function isLine(l) {return l >= 0 && l < lines.length;}
+ function isLine(l) {return l >= 0 && l < doc.size;}
// The instance object that we'll return. Mostly calls out to
// local functions in the CodeMirror function. Some do some extra
// range checking and/or clipping. operation is used to wrap the
// call so that changes it makes are tracked, and the display is
// updated afterwards.
- var instance = {
+ var instance = wrapper.CodeMirror = {
getValue: getValue,
setValue: operation(setValue),
getSelection: getSelection,
replaceSelection: operation(replaceSelection),
focus: function(){focusInput(); onFocus(); fastPoll();},
setOption: function(option, value) {
+ var oldVal = options[option];
options[option] = value;
- if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber") gutterChanged();
- else if (option == "mode" || option == "indentUnit") loadMode();
+ if (option == "mode" || option == "indentUnit") loadMode();
else if (option == "readOnly" && value == "nocursor") input.blur();
else if (option == "theme") scroller.className = scroller.className.replace(/cm-s-\w+/, "cm-s-" + value);
+ else if (option == "lineWrapping" && oldVal != value) operation(wrappingChanged)();
+ else if (option == "pollForIME" && alwaysPollForIME) slowPollInterval = value ? 50 : 2000;
+ if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber" || option == "theme")
+ operation(gutterChanged)();
},
getOption: function(option) {return options[option];},
undo: operation(undo),
@@ -131,13 +162,14 @@ var CodeMirror = (function() {
if (isLine(n)) indentLine(n, dir == null ? "smart" : dir ? "add" : "subtract");
}),
historySize: function() {return {undo: history.done.length, redo: history.undone.length};},
+ clearHistory: function() {history = new History();},
matchBrackets: operation(function(){matchBrackets(true);}),
- getTokenAt: function(pos) {
+ getTokenAt: operation(function(pos) {
pos = clipPos(pos);
- return lines[pos.line].getTokenAt(mode, getStateBefore(pos.line), pos.ch);
- },
+ return getLine(pos.line).getTokenAt(mode, getStateBefore(pos.line), pos.ch);
+ }),
getStateAfter: function(line) {
- line = clipLine(line == null ? lines.length - 1: line);
+ line = clipLine(line == null ? doc.size - 1: line);
return getStateBefore(line + 1);
},
cursorCoords: function(start){
@@ -147,24 +179,25 @@ var CodeMirror = (function() {
charCoords: function(pos){return pageCoords(clipPos(pos));},
coordsChar: function(coords) {
var off = eltOffset(lineSpace);
- var line = clipLine(Math.min(lines.length - 1, showingFrom + Math.floor((coords.y - off.top) / lineHeight())));
- return clipPos({line: line, ch: charFromX(clipLine(line), coords.x - off.left)});
+ return coordsChar(coords.x - off.left, coords.y - off.top);
},
getSearchCursor: function(query, pos, caseFold) {return new SearchCursor(query, pos, caseFold);},
- markText: operation(function(a, b, c){return operation(markText(a, b, c));}),
- setMarker: addGutterMarker,
- clearMarker: removeGutterMarker,
+ markText: operation(markText),
+ setBookmark: setBookmark,
+ setMarker: operation(addGutterMarker),
+ clearMarker: operation(removeGutterMarker),
setLineClass: operation(setLineClass),
+ hideLine: operation(function(h) {return setLineHidden(h, true);}),
+ showLine: operation(function(h) {return setLineHidden(h, false);}),
lineInfo: lineInfo,
- addWidget: function(pos, node, scroll, where) {
+ addWidget: function(pos, node, scroll, vert, horiz) {
pos = localCoords(clipPos(pos));
var top = pos.yBot, left = pos.x;
node.style.position = "absolute";
code.appendChild(node);
- node.style.left = left + "px";
- if (where == "over") top = pos.y;
- else if (where == "near") {
- var vspace = Math.max(scroller.offsetHeight, lines.length * lineHeight()),
+ if (vert == "over") top = pos.y;
+ else if (vert == "near") {
+ var vspace = Math.max(scroller.offsetHeight, doc.height * textHeight()),
hspace = Math.max(code.clientWidth, lineSpace.clientWidth) - paddingLeft();
if (pos.yBot + node.offsetHeight > vspace && pos.y > node.offsetHeight)
top = pos.y - node.offsetHeight;
@@ -172,12 +205,20 @@ var CodeMirror = (function() {
left = hspace - node.offsetWidth;
}
node.style.top = (top + paddingTop()) + "px";
- node.style.left = (left + paddingLeft()) + "px";
+ node.style.left = node.style.right = "";
+ if (horiz == "right") {
+ left = code.clientWidth - node.offsetWidth;
+ node.style.right = "0px";
+ } else {
+ if (horiz == "left") left = 0;
+ else if (horiz == "middle") left = (code.clientWidth - node.offsetWidth) / 2;
+ node.style.left = (left + paddingLeft()) + "px";
+ }
if (scroll)
scrollIntoView(left, top, left + node.offsetWidth, top + node.offsetHeight);
},
- lineCount: function() {return lines.length;},
+ lineCount: function() {return doc.size;},
getCursor: function(start) {
if (start == null) start = sel.inverted;
return copyPos(start ? sel.from : sel.to);
@@ -188,9 +229,9 @@ var CodeMirror = (function() {
else setCursor(line, ch);
}),
setSelection: operation(function(from, to) {setSelection(clipPos(from), clipPos(to || from));}),
- getLine: function(line) {if (isLine(line)) return lines[line].text;},
+ getLine: function(line) {if (isLine(line)) return getLine(line).text;},
setLine: operation(function(line, text) {
- if (isLine(line)) replaceRange(text, {line: line, ch: 0}, {line: line, ch: lines[line].text.length});
+ if (isLine(line)) replaceRange(text, {line: line, ch: 0}, {line: line, ch: getLine(line).text.length});
}),
removeLine: operation(function(line) {
if (isLine(line)) replaceRange("", {line: line, ch: 0}, clipPos({line: line+1, ch: 0}));
@@ -198,6 +239,17 @@ var CodeMirror = (function() {
replaceRange: operation(replaceRange),
getRange: function(from, to) {return getRange(clipPos(from), clipPos(to));},
+ coordsFromIndex: function(off) {
+ var lineNo = 0, ch;
+ doc.iter(0, doc.size, function(line) {
+ var sz = line.text.length + 1;
+ if (sz > off) { ch = off; return true; }
+ off -= sz;
+ ++lineNo;
+ });
+ return clipPos({line: lineNo, ch: ch});
+ },
+
operation: function(f){return operation(f)();},
refresh: function(){updateDisplay(true);},
getInputField: function(){return input;},
@@ -206,17 +258,22 @@ var CodeMirror = (function() {
getGutterElement: function(){return gutter;}
};
+ function getLine(n) { return getLineAt(doc, n); }
+ function updateLineHeight(line, height) {
+ gutterDirty = true;
+ var diff = height - line.height;
+ for (var n = line; n; n = n.parent) n.height += diff;
+ }
+
function setValue(code) {
- history = null;
var top = {line: 0, ch: 0};
- updateLines(top, {line: lines.length - 1, ch: lines[lines.length-1].text.length},
+ updateLines(top, {line: doc.size - 1, ch: getLine(doc.size-1).text.length},
splitLines(code), top, top);
- history = new History();
+ updateInput = true;
}
function getValue(code) {
var text = [];
- for (var i = 0, l = lines.length; i < l; ++i)
- text.push(lines[i].text);
+ doc.iter(0, doc.size, function(line) { text.push(line.text); });
return text.join("\n");
}
@@ -224,17 +281,17 @@ var CodeMirror = (function() {
// Check whether this is a click in a widget
for (var n = e_target(e); n != wrapper; n = n.parentNode)
if (n.parentNode == code && n != mover) return;
- var ld = lastDoubleClick; lastDoubleClick = null;
- // First, see if this is a click in the gutter
+
+ // See if this is a click in the gutter
for (var n = e_target(e); n != wrapper; n = n.parentNode)
if (n.parentNode == gutterText) {
if (options.onGutterClick)
- options.onGutterClick(instance, indexOf(gutterText.childNodes, n) + showingFrom);
+ options.onGutterClick(instance, indexOf(gutterText.childNodes, n) + showingFrom, e);
return e_preventDefault(e);
}
var start = posFromMouse(e);
-
+
switch (e_button(e)) {
case 3:
if (gecko && !mac) onContextMenu(e);
@@ -249,18 +306,39 @@ var CodeMirror = (function() {
if (!start) {if (e_target(e) == scroller) e_preventDefault(e); return;}
if (!focused) onFocus();
- e_preventDefault(e);
- if (ld && +new Date - ld < 400) return selectLine(start.line);
- setCursor(start.line, start.ch, true);
+ var now = +new Date;
+ if (lastDoubleClick && lastDoubleClick.time > now - 400 && posEq(lastDoubleClick.pos, start)) {
+ e_preventDefault(e);
+ setTimeout(focusInput, 20);
+ return selectLine(start.line);
+ } else if (lastClick && lastClick.time > now - 400 && posEq(lastClick.pos, start)) {
+ lastDoubleClick = {time: now, pos: start};
+ e_preventDefault(e);
+ return selectWordAt(start);
+ } else { lastClick = {time: now, pos: start}; }
+
var last = start, going;
- // And then we have to see if it's a drag event, in which case
- // the dragged-over text must be selected.
- function end() {
- focusInput();
- updateInput = true;
- move(); up();
+ if (dragAndDrop && !posEq(sel.from, sel.to) &&
+ !posLess(start, sel.from) && !posLess(sel.to, start)) {
+ // Let the drag handler handle this.
+ if (webkit) lineSpace.draggable = true;
+ var up = connect(targetDocument, "mouseup", operation(function(e2) {
+ if (webkit) lineSpace.draggable = false;
+ draggingText = false;
+ up();
+ if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) {
+ e_preventDefault(e2);
+ setCursor(start.line, start.ch, true);
+ focusInput();
+ }
+ }), true);
+ draggingText = true;
+ return;
}
+ e_preventDefault(e);
+ setCursor(start.line, start.ch, true);
+
function extend(e) {
var cur = posFromMouse(e, true);
if (cur && !posEq(cur, last)) {
@@ -284,15 +362,19 @@ var CodeMirror = (function() {
var cur = posFromMouse(e);
if (cur) setSelectionUser(start, cur);
e_preventDefault(e);
- end();
+ focusInput();
+ updateInput = true;
+ move(); up();
}), true);
}
- function onDblClick(e) {
- var pos = posFromMouse(e);
- if (!pos) return;
- selectWordAt(pos);
+ function onDoubleClick(e) {
+ for (var n = e_target(e); n != wrapper; n = n.parentNode)
+ if (n.parentNode == gutterText) return e_preventDefault(e);
+ var start = posFromMouse(e);
+ if (!start) return;
+ lastDoubleClick = {time: +new Date, pos: start};
e_preventDefault(e);
- lastDoubleClick = +new Date;
+ selectWordAt(start);
}
function onDrop(e) {
e.preventDefault();
@@ -303,7 +385,13 @@ var CodeMirror = (function() {
var reader = new FileReader;
reader.onload = function() {
text[i] = reader.result;
- if (++read == n) replaceRange(text.join(""), clipPos(pos), clipPos(pos));
+ if (++read == n) {
+ pos = clipPos(pos);
+ operation(function() {
+ var end = replaceRange(text.join(""), pos, pos);
+ setSelectionUser(pos, end);
+ })();
+ }
};
reader.readAsText(file);
}
@@ -313,11 +401,24 @@ var CodeMirror = (function() {
else {
try {
var text = e.dataTransfer.getData("Text");
- if (text) replaceRange(text, pos, pos);
+ if (text) {
+ var end = replaceRange(text, pos, pos);
+ var curFrom = sel.from, curTo = sel.to;
+ setSelectionUser(pos, end);
+ if (draggingText) replaceRange("", curFrom, curTo);
+ focusInput();
+ }
}
catch(e){}
}
}
+ function onDragStart(e) {
+ var txt = getSelection();
+ // This will reset escapeElement
+ htmlEscape(txt);
+ e.dataTransfer.setDragImage(escapeElement, 0, 0);
+ e.dataTransfer.setData("Text", txt);
+ }
function onKeyDown(e) {
if (!focused) onFocus();
@@ -362,6 +463,8 @@ var CodeMirror = (function() {
// Don't save the key as a movementkey unless it had a modifier
if (!mod && !e.altKey) curKeyId = null;
fastPoll(curKeyId);
+
+ if (options.pollForIME && keyMightStartIME(code)) slowPollInterval = 50;
}
function onKeyUp(e) {
if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return;
@@ -370,6 +473,8 @@ var CodeMirror = (function() {
updateInput = true;
}
if (e.keyCode == 16) shiftSelecting = null;
+
+ if (slowPollInterval < 2000 && !alwaysPollForIME) slowPollInterval = 2000;
}
function onKeyPress(e) {
if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return;
@@ -412,7 +517,7 @@ var CodeMirror = (function() {
function updateLines(from, to, newText, selFrom, selTo) {
if (history) {
var old = [];
- for (var i = from.line, e = to.line + 1; i < e; ++i) old.push(lines[i].text);
+ doc.iter(from.line, to.line + 1, function(line) { old.push(line.text); });
history.addChange(from.line, newText.length, old);
while (history.done.length > options.undoDepth) history.done.shift();
}
@@ -422,11 +527,11 @@ var CodeMirror = (function() {
var change = from.pop();
if (change) {
var replaced = [], end = change.start + change.added;
- for (var i = change.start; i < end; ++i) replaced.push(lines[i].text);
+ doc.iter(change.start, end, function(line) { replaced.push(line.text); });
to.push({start: change.start, added: change.old.length, old: replaced});
var pos = clipPos({line: change.start + change.old.length - 1,
ch: editEnd(replaced[replaced.length-1], change.old[change.old.length-1])});
- updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: lines[end-1].text.length}, change.old, pos, pos);
+ updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: getLine(end-1).text.length}, change.old, pos, pos);
updateInput = true;
}
}
@@ -435,51 +540,66 @@ var CodeMirror = (function() {
function updateLinesNoUndo(from, to, newText, selFrom, selTo) {
var recomputeMaxLength = false, maxLineLength = maxLine.length;
- for (var i = from.line; i <= to.line; ++i) {
- if (lines[i].text.length == maxLineLength) {recomputeMaxLength = true; break;}
- }
+ if (!options.lineWrapping)
+ doc.iter(from.line, to.line, function(line) {
+ if (line.text.length == maxLineLength) {recomputeMaxLength = true; return true;}
+ });
- var nlines = to.line - from.line, firstLine = lines[from.line], lastLine = lines[to.line];
+ var nlines = to.line - from.line, firstLine = getLine(from.line), lastLine = getLine(to.line);
// First adjust the line structure, taking some care to leave highlighting intact.
if (firstLine == lastLine) {
if (newText.length == 1)
firstLine.replace(from.ch, to.ch, newText[0]);
else {
lastLine = firstLine.split(to.ch, newText[newText.length-1]);
- var spliceargs = [from.line + 1, nlines];
- firstLine.replace(from.ch, firstLine.text.length, newText[0]);
- for (var i = 1, e = newText.length - 1; i < e; ++i) spliceargs.push(new Line(newText[i]));
- spliceargs.push(lastLine);
- lines.splice.apply(lines, spliceargs);
+ firstLine.replace(from.ch, null, newText[0]);
+ firstLine.fixMarkEnds(lastLine);
+ var added = [];
+ for (var i = 1, e = newText.length - 1; i < e; ++i)
+ added.push(Line.inheritMarks(newText[i], firstLine));
+ added.push(lastLine);
+ doc.insert(from.line + 1, added);
}
}
else if (newText.length == 1) {
- firstLine.replace(from.ch, firstLine.text.length, newText[0] + lastLine.text.slice(to.ch));
- lines.splice(from.line + 1, nlines);
+ firstLine.replace(from.ch, null, newText[0]);
+ lastLine.replace(null, to.ch, "");
+ firstLine.append(lastLine);
+ doc.remove(from.line + 1, nlines);
}
else {
- var spliceargs = [from.line + 1, nlines - 1];
- firstLine.replace(from.ch, firstLine.text.length, newText[0]);
- lastLine.replace(0, to.ch, newText[newText.length-1]);
- for (var i = 1, e = newText.length - 1; i < e; ++i) spliceargs.push(new Line(newText[i]));
- lines.splice.apply(lines, spliceargs);
- }
-
-
- for (var i = from.line, e = i + newText.length; i < e; ++i) {
- var l = lines[i].text;
- if (l.length > maxLineLength) {
- maxLine = l; maxLineLength = l.length; maxWidth = null;
- recomputeMaxLength = false;
- }
- }
- if (recomputeMaxLength) {
- maxLineLength = 0; maxLine = ""; maxWidth = null;
- for (var i = 0, e = lines.length; i < e; ++i) {
- var l = lines[i].text;
+ var added = [];
+ firstLine.replace(from.ch, null, newText[0]);
+ lastLine.replace(null, to.ch, newText[newText.length-1]);
+ firstLine.fixMarkEnds(lastLine);
+ for (var i = 1, e = newText.length - 1; i < e; ++i)
+ added.push(Line.inheritMarks(newText[i], firstLine));
+ if (nlines > 1) doc.remove(from.line + 1, nlines - 1);
+ doc.insert(from.line + 1, added);
+ }
+ if (options.lineWrapping) {
+ var perLine = scroller.clientWidth / charWidth() - 3;
+ doc.iter(from.line, from.line + newText.length, function(line) {
+ if (line.hidden) return;
+ var guess = Math.ceil(line.text.length / perLine) || 1;
+ if (guess != line.height) updateLineHeight(line, guess);
+ });
+ } else {
+ doc.iter(from.line, i + newText.length, function(line) {
+ var l = line.text;
if (l.length > maxLineLength) {
- maxLineLength = l.length; maxLine = l;
+ maxLine = l; maxLineLength = l.length; maxWidth = null;
+ recomputeMaxLength = false;
}
+ });
+ if (recomputeMaxLength) {
+ maxLineLength = 0; maxLine = ""; maxWidth = null;
+ doc.iter(0, doc.size, function(line) {
+ var l = line.text;
+ if (l.length > maxLineLength) {
+ maxLineLength = l.length; maxLine = l;
+ }
+ });
}
}
@@ -491,12 +611,9 @@ var CodeMirror = (function() {
if (task < from.line) newWork.push(task);
else if (task > to.line) newWork.push(task + lendiff);
}
- if (newText.length < 5) {
- highlightLines(from.line, from.line + newText.length);
- newWork.push(from.line + newText.length);
- } else {
- newWork.push(from.line);
- }
+ var hlEnd = from.line + Math.min(newText.length, 500);
+ highlightLines(from.line, hlEnd);
+ newWork.push(hlEnd);
work = newWork;
startWorker(100);
// Remember that these lines changed, for updating the display
@@ -508,7 +625,7 @@ var CodeMirror = (function() {
setSelection(selFrom, selTo, updateLine(sel.from.line), updateLine(sel.to.line));
// Make sure the scroll-size div has the correct height.
- code.style.height = (lines.length * lineHeight() + 2 * paddingTop()) + "px";
+ code.style.height = (doc.height * textHeight() + 2 * paddingTop()) + "px";
}
function replaceRange(code, from, to) {
@@ -546,10 +663,10 @@ var CodeMirror = (function() {
function getRange(from, to) {
var l1 = from.line, l2 = to.line;
- if (l1 == l2) return lines[l1].text.slice(from.ch, to.ch);
- var code = [lines[l1].text.slice(from.ch)];
- for (var i = l1 + 1; i < l2; ++i) code.push(lines[i].text);
- code.push(lines[l2].text.slice(0, to.ch));
+ if (l1 == l2) return getLine(l1).text.slice(from.ch, to.ch);
+ var code = [getLine(l1).text.slice(from.ch)];
+ doc.iter(l1 + 1, l2, function(line) { code.push(line.text); });
+ code.push(getLine(l2).text.slice(0, to.ch));
return code.join("\n");
}
function getSelection() {
@@ -559,7 +676,7 @@ var CodeMirror = (function() {
var pollingFast = false; // Ensures slowPoll doesn't cancel fastPoll
function slowPoll() {
if (pollingFast) return;
- poll.set(2000, function() {
+ poll.set(slowPollInterval, function() {
startOperation();
readInput();
if (focused) slowPoll();
@@ -657,14 +774,16 @@ var CodeMirror = (function() {
// editor state.
function prepareInput() {
var text = [];
- var from = Math.max(0, sel.from.line - 1), to = Math.min(lines.length, sel.to.line + 2);
- for (var i = from; i < to; ++i) text.push(lines[i].text);
+ var from = Math.max(0, sel.from.line - 1), to = Math.min(doc.size, sel.to.line + 2);
+ doc.iter(from, to, function(line) { text.push(line.text); });
text = input.value = text.join(lineSep);
var startch = sel.from.ch, endch = sel.to.ch;
- for (var i = from; i < sel.from.line; ++i)
- startch += lineSep.length + lines[i].text.length;
- for (var i = from; i < sel.to.line; ++i)
- endch += lineSep.length + lines[i].text.length;
+ doc.iter(from, sel.from.line, function(line) {
+ startch += lineSep.length + line.text.length;
+ });
+ doc.iter(from, sel.to.line, function(line) {
+ endch += lineSep.length + line.text.length;
+ });
editing = {text: text, from: from, to: to, start: startch, end: endch};
setSelRange(input, startch, reducedSelection ? startch : endch);
}
@@ -675,24 +794,26 @@ var CodeMirror = (function() {
function scrollEditorIntoView() {
if (!cursor.getBoundingClientRect) return;
var rect = cursor.getBoundingClientRect();
- var winH = window.innerHeight || document.body.offsetHeight || document.documentElement.offsetHeight;
+ var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
if (rect.top < 0 || rect.bottom > winH) cursor.scrollIntoView();
}
function scrollCursorIntoView() {
var cursor = localCoords(sel.inverted ? sel.from : sel.to);
- return scrollIntoView(cursor.x, cursor.y, cursor.x, cursor.yBot);
+ var x = options.lineWrapping ? Math.min(cursor.x, lineSpace.offsetWidth) : cursor.x;
+ return scrollIntoView(x, cursor.y, x, cursor.yBot);
}
function scrollIntoView(x1, y1, x2, y2) {
- var pl = paddingLeft(), pt = paddingTop(), lh = lineHeight();
+ var pl = paddingLeft(), pt = paddingTop(), lh = textHeight();
y1 += pt; y2 += pt; x1 += pl; x2 += pl;
var screen = scroller.clientHeight, screentop = scroller.scrollTop, scrolled = false, result = true;
if (y1 < screentop) {scroller.scrollTop = Math.max(0, y1 - 2*lh); scrolled = true;}
else if (y2 > screentop + screen) {scroller.scrollTop = y2 + lh - screen; scrolled = true;}
var screenw = scroller.clientWidth, screenleft = scroller.scrollLeft;
- if (x1 < screenleft) {
+ var gutterw = options.fixedGutter ? gutter.clientWidth : 0;
+ if (x1 < screenleft + gutterw) {
if (x1 < 50) x1 = 0;
- scroller.scrollLeft = Math.max(0, x1 - 10);
+ scroller.scrollLeft = Math.max(0, x1 - 10 - gutterw);
scrolled = true;
}
else if (x2 > screenw + screenleft) {
@@ -705,32 +826,103 @@ var CodeMirror = (function() {
}
function visibleLines() {
- var lh = lineHeight(), top = scroller.scrollTop - paddingTop();
- return {from: Math.min(lines.length, Math.max(0, Math.floor(top / lh))),
- to: Math.min(lines.length, Math.ceil((top + scroller.clientHeight) / lh))};
+ var lh = textHeight(), top = scroller.scrollTop - paddingTop();
+ var from_height = Math.max(0, Math.floor(top / lh));
+ var to_height = Math.ceil((top + scroller.clientHeight) / lh);
+ return {from: lineAtHeight(doc, from_height),
+ to: lineAtHeight(doc, to_height)};
}
// Uses a set of changes plus the current scroll position to
// determine which DOM updates have to be made, and makes the
// updates.
function updateDisplay(changes) {
if (!scroller.clientWidth) {
- showingFrom = showingTo = 0;
+ showingFrom = showingTo = displayOffset = 0;
return;
}
- // First create a range of theoretically intact lines, and punch
- // holes in that using the change info.
- var intact = changes === true ? [] : [{from: showingFrom, to: showingTo, domStart: 0}];
+ // Compute the new visible window
+ var visible = visibleLines();
+ // Bail out if the visible area is already rendered and nothing changed.
+ if (changes !== true && changes.length == 0 && visible.from >= showingFrom && visible.to <= showingTo) return;
+ var from = Math.max(visible.from - 100, 0), to = Math.min(doc.size, visible.to + 100);
+ if (showingFrom < from && from - showingFrom < 20) from = showingFrom;
+ if (showingTo > to && showingTo - to < 20) to = Math.min(doc.size, showingTo);
+
+ // Create a range of theoretically intact lines, and punch holes
+ // in that using the change info.
+ var intact = changes === true ? [] :
+ computeIntact([{from: showingFrom, to: showingTo, domStart: 0}], changes);
+ // Clip off the parts that won't be visible
+ var intactLines = 0;
+ for (var i = 0; i < intact.length; ++i) {
+ var range = intact[i];
+ if (range.from < from) {range.domStart += (from - range.from); range.from = from;}
+ if (range.to > to) range.to = to;
+ if (range.from >= range.to) intact.splice(i--, 1);
+ else intactLines += range.to - range.from;
+ }
+ if (intactLines == to - from) return;
+ intact.sort(function(a, b) {return a.domStart - b.domStart;});
+
+ var th = textHeight(), gutterDisplay = gutter.style.display;
+ lineDiv.style.display = gutter.style.display = "none";
+ patchDisplay(from, to, intact);
+ lineDiv.style.display = "";
+
+ // Position the mover div to align with the lines it's supposed
+ // to be showing (which will cover the visible display)
+ var different = from != showingFrom || to != showingTo || lastHeight != scroller.clientHeight;
+ if (different) lastHeight = scroller.clientHeight;
+ showingFrom = from; showingTo = to;
+ displayOffset = heightAtLine(doc, from);
+ mover.style.top = (displayOffset * th) + "px";
+ code.style.height = (doc.height * th + 2 * paddingTop()) + "px";
+
+ // Since this is all rather error prone, it is honoured with the
+ // only assertion in the whole file.
+ if (lineDiv.childNodes.length != showingTo - showingFrom)
+ throw new Error("BAD PATCH! " + JSON.stringify(intact) + " size=" + (showingTo - showingFrom) +
+ " nodes=" + lineDiv.childNodes.length);
+
+ if (options.lineWrapping) {
+ maxWidth = scroller.clientWidth;
+ var curNode = lineDiv.firstChild;
+ doc.iter(showingFrom, showingTo, function(line) {
+ if (!line.hidden) {
+ var height = Math.round(curNode.offsetHeight / th) || 1;
+ if (line.height != height) {updateLineHeight(line, height); gutterDirty = true;}
+ }
+ curNode = curNode.nextSibling;
+ });
+ } else {
+ if (maxWidth == null) maxWidth = stringWidth(maxLine);
+ if (maxWidth > scroller.clientWidth) {
+ lineSpace.style.width = maxWidth + "px";
+ // Needed to prevent odd wrapping/hiding of widgets placed in here.
+ code.style.width = "";
+ code.style.width = scroller.scrollWidth + "px";
+ } else {
+ lineSpace.style.width = code.style.width = "";
+ }
+ }
+ gutter.style.display = gutterDisplay;
+ if (different || gutterDirty) updateGutter();
+ updateCursor();
+ }
+
+ function computeIntact(intact, changes) {
for (var i = 0, l = changes.length || 0; i < l; ++i) {
var change = changes[i], intact2 = [], diff = change.diff || 0;
for (var j = 0, l2 = intact.length; j < l2; ++j) {
var range = intact[j];
- if (change.to <= range.from)
- intact2.push({from: range.from + diff, to: range.to + diff, domStart: range.domStart});
- else if (range.to <= change.from)
+ if (change.to <= range.from && change.diff)
+ intact2.push({from: range.from + diff, to: range.to + diff,
+ domStart: range.domStart});
+ else if (change.to <= range.from || change.from >= range.to)
intact2.push(range);
else {
if (change.from > range.from)
- intact2.push({from: range.from, to: change.from, domStart: range.domStart})
+ intact2.push({from: range.from, to: change.from, domStart: range.domStart});
if (change.to < range.to)
intact2.push({from: change.to + diff, to: range.to + diff,
domStart: range.domStart + (change.to - range.from)});
@@ -738,160 +930,90 @@ var CodeMirror = (function() {
}
intact = intact2;
}
+ return intact;
+ }
- // Then, determine which lines we'd want to see, and which
- // updates have to be made to get there.
- var visible = visibleLines();
- var from = Math.min(showingFrom, Math.max(visible.from - 3, 0)),
- to = Math.min(lines.length, Math.max(showingTo, visible.to + 3)),
- updates = [], domPos = 0, domEnd = showingTo - showingFrom, pos = from, changedLines = 0;
-
- for (var i = 0, l = intact.length; i < l; ++i) {
- var range = intact[i];
- if (range.to <= from) continue;
- if (range.from >= to) break;
- if (range.domStart > domPos || range.from > pos) {
- updates.push({from: pos, to: range.from, domSize: range.domStart - domPos, domStart: domPos});
- changedLines += range.from - pos;
+ function patchDisplay(from, to, intact) {
+ // The first pass removes the DOM nodes that aren't intact.
+ if (!intact.length) lineDiv.innerHTML = "";
+ else {
+ function killNode(node) {
+ var tmp = node.nextSibling;
+ node.parentNode.removeChild(node);
+ return tmp;
}
- pos = range.to;
- domPos = range.domStart + (range.to - range.from);
- }
- if (domPos != domEnd || pos != to) {
- changedLines += Math.abs(to - pos);
- updates.push({from: pos, to: to, domSize: domEnd - domPos, domStart: domPos});
- }
-
- if (!updates.length) return;
- lineDiv.style.display = "none";
- // If more than 30% of the screen needs update, just do a full
- // redraw (which is quicker than patching)
- if (changedLines > (visible.to - visible.from) * .3)
- refreshDisplay(from = Math.max(visible.from - 10, 0), to = Math.min(visible.to + 7, lines.length));
- // Otherwise, only update the stuff that needs updating.
- else
- patchDisplay(updates);
- lineDiv.style.display = "";
-
- // Position the mover div to align with the lines it's supposed
- // to be showing (which will cover the visible display)
- var different = from != showingFrom || to != showingTo || lastHeight != scroller.clientHeight;
- showingFrom = from; showingTo = to;
- mover.style.top = (from * lineHeight()) + "px";
- if (different) {
- lastHeight = scroller.clientHeight;
- code.style.height = (lines.length * lineHeight() + 2 * paddingTop()) + "px";
- }
- if (different || updates.length) updateGutter();
-
- if (maxWidth == null) maxWidth = stringWidth(maxLine);
- if (maxWidth > scroller.clientWidth) {
- lineSpace.style.width = maxWidth + "px";
- // Needed to prevent odd wrapping/hiding of widgets placed in here.
- code.style.width = "";
- code.style.width = scroller.scrollWidth + "px";
- } else {
- lineSpace.style.width = code.style.width = "";
+ var domPos = 0, curNode = lineDiv.firstChild, n;
+ for (var i = 0; i < intact.length; ++i) {
+ var cur = intact[i];
+ while (cur.domStart > domPos) {curNode = killNode(curNode); domPos++;}
+ for (var j = 0, e = cur.to - cur.from; j < e; ++j) {curNode = curNode.nextSibling; domPos++;}
+ }
+ while (curNode) curNode = killNode(curNode);
}
-
- // Since this is all rather error prone, it is honoured with the
- // only assertion in the whole file.
- if (lineDiv.childNodes.length != showingTo - showingFrom)
- throw new Error("BAD PATCH! " + JSON.stringify(updates) + " size=" + (showingTo - showingFrom) +
- " nodes=" + lineDiv.childNodes.length);
- updateCursor();
- }
-
- function refreshDisplay(from, to) {
- var html = [], start = {line: from, ch: 0}, inSel = posLess(sel.from, start) && !posLess(sel.to, start);
- for (var i = from; i < to; ++i) {
+ // This pass fills in the lines that actually changed.
+ var nextIntact = intact.shift(), curNode = lineDiv.firstChild, j = from;
+ var sfrom = sel.from.line, sto = sel.to.line, inSel = sfrom < from && sto >= from;
+ var scratch = targetDocument.createElement("div"), newElt;
+ doc.iter(from, to, function(line) {
var ch1 = null, ch2 = null;
if (inSel) {
ch1 = 0;
- if (sel.to.line == i) {inSel = false; ch2 = sel.to.ch;}
- }
- else if (sel.from.line == i) {
- if (sel.to.line == i) {ch1 = sel.from.ch; ch2 = sel.to.ch;}
+ if (sto == j) {inSel = false; ch2 = sel.to.ch;}
+ } else if (sfrom == j) {
+ if (sto == j) {ch1 = sel.from.ch; ch2 = sel.to.ch;}
else {inSel = true; ch1 = sel.from.ch;}
}
- html.push(lines[i].getHTML(ch1, ch2, true));
- }
- lineDiv.innerHTML = html.join("");
- }
- function patchDisplay(updates) {
- // Slightly different algorithm for IE (badInnerHTML), since
- // there .innerHTML on PRE nodes is dumb, and discards
- // whitespace.
- var sfrom = sel.from.line, sto = sel.to.line, off = 0,
- scratch = badInnerHTML && targetDocument.createElement("div");
- for (var i = 0, e = updates.length; i < e; ++i) {
- var rec = updates[i];
- var extra = (rec.to - rec.from) - rec.domSize;
- var nodeAfter = lineDiv.childNodes[rec.domStart + rec.domSize + off] || null;
- if (badInnerHTML)
- for (var j = Math.max(-extra, rec.domSize); j > 0; --j)
- lineDiv.removeChild(nodeAfter ? nodeAfter.previousSibling : lineDiv.lastChild);
- else if (extra) {
- for (var j = Math.max(0, extra); j > 0; --j)
- lineDiv.insertBefore(targetDocument.createElement("pre"), nodeAfter);
- for (var j = Math.max(0, -extra); j > 0; --j)
- lineDiv.removeChild(nodeAfter ? nodeAfter.previousSibling : lineDiv.lastChild);
- }
- var node = lineDiv.childNodes[rec.domStart + off], inSel = sfrom < rec.from && sto >= rec.from;
- for (var j = rec.from; j < rec.to; ++j) {
- var ch1 = null, ch2 = null;
- if (inSel) {
- ch1 = 0;
- if (sto == j) {inSel = false; ch2 = sel.to.ch;}
- }
- else if (sfrom == j) {
- if (sto == j) {ch1 = sel.from.ch; ch2 = sel.to.ch;}
- else {inSel = true; ch1 = sel.from.ch;}
- }
- if (badInnerHTML) {
- scratch.innerHTML = lines[j].getHTML(ch1, ch2, true);
- lineDiv.insertBefore(scratch.firstChild, nodeAfter);
- }
- else {
- node.innerHTML = lines[j].getHTML(ch1, ch2, false);
- node.className = lines[j].className || "";
- node = node.nextSibling;
- }
+ if (nextIntact && nextIntact.to == j) nextIntact = intact.shift();
+ if (!nextIntact || nextIntact.from > j) {
+ if (line.hidden) scratch.innerHTML = "<pre></pre>";
+ else scratch.innerHTML = line.getHTML(ch1, ch2, true);
+ lineDiv.insertBefore(scratch.firstChild, curNode);
+ } else {
+ curNode = curNode.nextSibling;
}
- off += extra;
- }
+ ++j;
+ });
}
function updateGutter() {
if (!options.gutter && !options.lineNumbers) return;
var hText = mover.offsetHeight, hEditor = scroller.clientHeight;
gutter.style.height = (hText - hEditor < 2 ? hEditor : hText) + "px";
- var html = [];
- for (var i = showingFrom; i < Math.max(showingTo, showingFrom + 1); ++i) {
- var marker = lines[i].gutterMarker;
- var text = options.lineNumbers ? i + options.firstLineNumber : null;
- if (marker && marker.text)
- text = marker.text.replace("%N%", text != null ? text : "");
- else if (text == null)
- text = "\u00a0";
- html.push((marker && marker.style ? '<pre class="' + marker.style + '">' : "<pre>"), text, "</pre>");
- }
+ var html = [], i = showingFrom;
+ doc.iter(showingFrom, Math.max(showingTo, showingFrom + 1), function(line) {
+ if (line.hidden) {
+ html.push("<pre></pre>");
+ } else {
+ var marker = line.gutterMarker;
+ var text = options.lineNumbers ? i + options.firstLineNumber : null;
+ if (marker && marker.text)
+ text = marker.text.replace("%N%", text != null ? text : "");
+ else if (text == null)
+ text = "\u00a0";
+ html.push((marker && marker.style ? '<pre class="' + marker.style + '">' : "<pre>"), text);
+ for (var j = 1; j < line.height; ++j) html.push("<br>&nbsp;");
+ html.push("</pre>");
+ }
+ ++i;
+ });
gutter.style.display = "none";
gutterText.innerHTML = html.join("");
- var minwidth = String(lines.length).length, firstNode = gutterText.firstChild, val = eltText(firstNode), pad = "";
+ var minwidth = String(doc.size).length, firstNode = gutterText.firstChild, val = eltText(firstNode), pad = "";
while (val.length + pad.length < minwidth) pad += "\u00a0";
if (pad) firstNode.insertBefore(targetDocument.createTextNode(pad), firstNode.firstChild);
gutter.style.display = "";
lineSpace.style.marginLeft = gutter.offsetWidth + "px";
+ gutterDirty = false;
}
function updateCursor() {
- var head = sel.inverted ? sel.from : sel.to, lh = lineHeight();
- var x = charX(head.line, head.ch);
- inputDiv.style.top = (head.line * lh - scroller.scrollTop) + "px";
- inputDiv.style.left = (x - scroller.scrollLeft) + "px";
+ var head = sel.inverted ? sel.from : sel.to, lh = textHeight();
+ var pos = localCoords(head, true);
+ var globalY = pos.y + displayOffset * textHeight();
+ inputDiv.style.top = Math.max(Math.min(globalY, scroller.offsetHeight), 0) + "px";
+ inputDiv.style.left = (pos.x - scroller.scrollLeft) + "px";
if (posEq(sel.from, sel.to)) {
- cursor.style.top = (head.line - showingFrom) * lh + "px";
- cursor.style.left = x + "px";
+ cursor.style.top = pos.y + "px";
+ cursor.style.left = (options.lineWrapping ? Math.min(pos.x, lineSpace.offsetWidth) : pos.x) + "px";
cursor.style.display = "";
}
else cursor.style.display = "none";
@@ -909,9 +1031,14 @@ var CodeMirror = (function() {
// updateLines, since they have to be expressed in the line
// numbers before the update.
function setSelection(from, to, oldFrom, oldTo) {
+ if (oldFrom == null) {oldFrom = sel.from.line; oldTo = sel.to.line;}
if (posEq(sel.from, from) && posEq(sel.to, to)) return;
if (posLess(to, from)) {var tmp = to; to = from; from = tmp;}
+ // Skip over hidden lines.
+ if (from.line != oldFrom) from = skipHidden(from, oldFrom, sel.from.ch);
+ if (to.line != oldTo) to = skipHidden(to, oldTo, sel.to.ch);
+
if (posEq(from, to)) sel.inverted = false;
else if (posEq(from, sel.to)) sel.inverted = false;
else if (posEq(to, sel.from)) sel.inverted = true;
@@ -919,7 +1046,6 @@ var CodeMirror = (function() {
// Some ugly logic used to only mark the lines that actually did
// see a change in selection as changed, rather than the whole
// selected range.
- if (oldFrom == null) {oldFrom = sel.from.line; oldTo = sel.to.line;}
if (posEq(from, to)) {
if (!posEq(sel.from, sel.to))
changes.push({from: oldFrom, to: oldTo + 1});
@@ -944,42 +1070,61 @@ var CodeMirror = (function() {
sel.from = from; sel.to = to;
selectionChanged = true;
}
+ function skipHidden(pos, oldLine, oldCh) {
+ function getNonHidden(dir) {
+ var lNo = pos.line + dir, end = dir == 1 ? doc.size : -1;
+ while (lNo != end) {
+ var line = getLine(lNo);
+ if (!line.hidden) {
+ var ch = pos.ch;
+ if (ch > oldCh || ch > line.text.length) ch = line.text.length;
+ return {line: lNo, ch: ch};
+ }
+ lNo += dir;
+ }
+ }
+ var line = getLine(pos.line);
+ if (!line.hidden) return pos;
+ if (pos.line >= oldLine) return getNonHidden(1) || getNonHidden(-1);
+ else return getNonHidden(-1) || getNonHidden(1);
+ }
function setCursor(line, ch, user) {
var pos = clipPos({line: line, ch: ch || 0});
(user ? setSelectionUser : setSelection)(pos, pos);
}
- function clipLine(n) {return Math.max(0, Math.min(n, lines.length-1));}
+ function clipLine(n) {return Math.max(0, Math.min(n, doc.size-1));}
function clipPos(pos) {
if (pos.line < 0) return {line: 0, ch: 0};
- if (pos.line >= lines.length) return {line: lines.length-1, ch: lines[lines.length-1].text.length};
- var ch = pos.ch, linelen = lines[pos.line].text.length;
+ if (pos.line >= doc.size) return {line: doc.size-1, ch: getLine(doc.size-1).text.length};
+ var ch = pos.ch, linelen = getLine(pos.line).text.length;
if (ch == null || ch > linelen) return {line: pos.line, ch: linelen};
else if (ch < 0) return {line: pos.line, ch: 0};
else return pos;
}
function scrollPage(down) {
- var linesPerPage = Math.floor(scroller.clientHeight / lineHeight()), head = sel.inverted ? sel.from : sel.to;
- setCursor(head.line + (Math.max(linesPerPage - 1, 1) * (down ? 1 : -1)), head.ch, true);
+ var linesPerPage = Math.floor(scroller.clientHeight / textHeight()), head = sel.inverted ? sel.from : sel.to;
+ var target = heightAtLine(doc, head.line) + (Math.max(linesPerPage - 1, 1) * (down ? 1 : -1));
+ setCursor(lineAtHeight(doc, target), head.ch, true);
}
function scrollEnd(top) {
- var pos = top ? {line: 0, ch: 0} : {line: lines.length - 1, ch: lines[lines.length-1].text.length};
+ var pos = top ? {line: 0, ch: 0} : {line: doc.size - 1, ch: getLine(doc.size-1).text.length};
setSelectionUser(pos, pos);
}
function selectAll() {
- var endLine = lines.length - 1;
- setSelection({line: 0, ch: 0}, {line: endLine, ch: lines[endLine].text.length});
+ var endLine = doc.size - 1;
+ setSelection({line: 0, ch: 0}, {line: endLine, ch: getLine(endLine).text.length});
}
function selectWordAt(pos) {
- var line = lines[pos.line].text;
+ var line = getLine(pos.line).text;
var start = pos.ch, end = pos.ch;
while (start > 0 && /\w/.test(line.charAt(start - 1))) --start;
while (end < line.length && /\w/.test(line.charAt(end))) ++end;
setSelectionUser({line: pos.line, ch: start}, {line: pos.line, ch: end});
}
function selectLine(line) {
- setSelectionUser({line: line, ch: 0}, {line: line, ch: lines[line].text.length});
+ setSelectionUser({line: line, ch: 0}, {line: line, ch: getLine(line).text.length});
}
function handleEnter() {
replaceSelection("\n", "end");
@@ -1012,7 +1157,7 @@ var CodeMirror = (function() {
return true;
}
function smartHome() {
- var firstNonWS = Math.max(0, lines[sel.from.line].text.search(/\S/));
+ var firstNonWS = Math.max(0, getLine(sel.from.line).text.search(/\S/));
setCursor(sel.from.line, sel.from.ch <= firstNonWS && sel.from.ch ? 0 : firstNonWS, true);
}
@@ -1022,9 +1167,9 @@ var CodeMirror = (function() {
else var state = getStateBefore(n);
}
- var line = lines[n], curSpace = line.indentation(), curSpaceString = line.text.match(/^\s*/)[0], indentation;
+ var line = getLine(n), curSpace = line.indentation(), curSpaceString = line.text.match(/^\s*/)[0], indentation;
if (how == "prev") {
- if (n) indentation = lines[n-1].indentation();
+ if (n) indentation = getLine(n-1).indentation();
else indentation = 0;
}
else if (how == "smart") indentation = mode.indent(state, line.text.slice(curSpaceString.length));
@@ -1049,87 +1194,148 @@ var CodeMirror = (function() {
function loadMode() {
mode = CodeMirror.getMode(options, options.mode);
- for (var i = 0, l = lines.length; i < l; ++i)
- lines[i].stateAfter = null;
+ doc.iter(0, doc.size, function(line) { line.stateAfter = null; });
work = [0];
startWorker();
}
function gutterChanged() {
var visible = options.gutter || options.lineNumbers;
gutter.style.display = visible ? "" : "none";
- if (visible) updateGutter();
+ if (visible) gutterDirty = true;
else lineDiv.parentNode.style.marginLeft = 0;
}
+ function wrappingChanged(from, to) {
+ if (options.lineWrapping) {
+ wrapper.className += " CodeMirror-wrap";
+ var perLine = scroller.clientWidth / charWidth() - 3;
+ doc.iter(0, doc.size, function(line) {
+ if (line.hidden) return;
+ var guess = Math.ceil(line.text.length / perLine) || 1;
+ if (guess != 1) updateLineHeight(line, guess);
+ });
+ lineSpace.style.width = code.style.width = "";
+ } else {
+ wrapper.className = wrapper.className.replace(" CodeMirror-wrap", "");
+ maxWidth = null; maxLine = "";
+ doc.iter(0, doc.size, function(line) {
+ if (line.height != 1 && !line.hidden) updateLineHeight(line, 1);
+ if (line.text.length > maxLine.length) maxLine = line.text;
+ });
+ }
+ changes.push({from: 0, to: doc.size});
+ }
+
+ function TextMarker() { this.set = []; }
+ TextMarker.prototype.clear = operation(function() {
+ for (var i = 0, e = this.set.length; i < e; ++i) {
+ var mk = this.set[i].marked;
+ if (!mk) continue;
+ for (var j = 0; j < mk.length; ++j)
+ if (mk[j].set == this.set) mk.splice(j--, 1);
+ }
+ // We don't know the exact lines that changed. Refreshing is
+ // cheaper than finding them.
+ changes.push({from: 0, to: doc.size});
+ });
+ TextMarker.prototype.find = function() {
+ var from, to;
+ for (var i = 0, e = this.set.length; i < e; ++i) {
+ var line = this.set[i], mk = line.marked;
+ for (var j = 0; j < mk.length; ++j) {
+ var mark = mk[j];
+ if (mark.set == this.set) {
+ if (mark.from != null || mark.to != null) {
+ var found = lineNo(line);
+ if (found != null) {
+ if (mark.from != null) from = {line: found, ch: mark.from};
+ if (mark.to != null) to = {line: found, ch: mark.to};
+ }
+ }
+ }
+ }
+ }
+ return {from: from, to: to};
+ };
function markText(from, to, className) {
from = clipPos(from); to = clipPos(to);
- var accum = [];
+ var tm = new TextMarker();
function add(line, from, to, className) {
- var line = lines[line], mark = line.addMark(from, to, className);
- mark.line = line;
- accum.push(mark);
+ mark = getLine(line).addMark(new MarkedText(from, to, className, tm.set));
}
if (from.line == to.line) add(from.line, from.ch, to.ch, className);
else {
add(from.line, from.ch, null, className);
for (var i = from.line + 1, e = to.line; i < e; ++i)
- add(i, 0, null, className);
- add(to.line, 0, to.ch, className);
+ add(i, null, null, className);
+ add(to.line, null, to.ch, className);
}
changes.push({from: from.line, to: to.line + 1});
- return function() {
- var start, end;
- for (var i = 0; i < accum.length; ++i) {
- var mark = accum[i], found = indexOf(lines, mark.line);
- mark.line.removeMark(mark);
- if (found > -1) {
- if (start == null) start = found;
- end = found;
- }
- }
- if (start != null) changes.push({from: start, to: end + 1});
- };
+ return tm;
+ }
+
+ function setBookmark(pos) {
+ pos = clipPos(pos);
+ var bm = new Bookmark(pos.ch);
+ getLine(pos.line).addMark(bm);
+ return bm;
}
function addGutterMarker(line, text, className) {
- if (typeof line == "number") line = lines[clipLine(line)];
+ if (typeof line == "number") line = getLine(clipLine(line));
line.gutterMarker = {text: text, style: className};
- updateGutter();
+ gutterDirty = true;
return line;
}
function removeGutterMarker(line) {
- if (typeof line == "number") line = lines[clipLine(line)];
+ if (typeof line == "number") line = getLine(clipLine(line));
line.gutterMarker = null;
- updateGutter();
+ gutterDirty = true;
}
- function setLineClass(line, className) {
- if (typeof line == "number") {
- var no = line;
- line = lines[clipLine(line)];
- }
- else {
- var no = indexOf(lines, line);
- if (no == -1) return null;
- }
- if (line.className != className) {
- line.className = className;
- changes.push({from: no, to: no + 1});
- }
+
+ function changeLine(handle, op) {
+ var no = handle, line = handle;
+ if (typeof handle == "number") line = getLine(clipLine(handle));
+ else no = lineNo(handle);
+ if (no == null) return null;
+ if (op(line, no)) changes.push({from: no, to: no + 1});
return line;
}
+ function setLineClass(handle, className) {
+ return changeLine(handle, function(line) {
+ if (line.className != className) {
+ line.className = className;
+ return true;
+ }
+ });
+ }
+ function setLineHidden(handle, hidden) {
+ return changeLine(handle, function(line, no) {
+ if (line.hidden != hidden) {
+ line.hidden = hidden;
+ updateLineHeight(line, hidden ? 0 : 1);
+ if (hidden && (sel.from.line == no || sel.to.line == no))
+ setSelection(skipHidden(sel.from, sel.from.line, sel.from.ch),
+ skipHidden(sel.to, sel.to.line, sel.to.ch));
+ return (gutterDirty = true);
+ }
+ });
+ }
function lineInfo(line) {
if (typeof line == "number") {
+ if (!isLine(line)) return null;
var n = line;
- line = lines[line];
+ line = getLine(line);
if (!line) return null;
}
else {
- var n = indexOf(lines, line);
- if (n == -1) return null;
+ var n = lineNo(line);
+ if (n == null) return null;
}
var marker = line.gutterMarker;
- return {line: n, text: line.text, markerText: marker && marker.text, markerClass: marker && marker.style};
+ return {line: n, handle: line, text: line.text, markerText: marker && marker.text,
+ markerClass: marker && marker.style, lineClass: line.className};
}
function stringWidth(str) {
@@ -1139,21 +1345,16 @@ var CodeMirror = (function() {
}
// These are used to go from pixel positions to character
// positions, taking varying character widths into account.
- function charX(line, pos) {
- if (pos == 0) return 0;
- measure.innerHTML = "<pre><span>" + lines[line].getHTML(null, null, false, pos) + "</span></pre>";
- return measure.firstChild.firstChild.offsetWidth;
- }
function charFromX(line, x) {
if (x <= 0) return 0;
- var lineObj = lines[line], text = lineObj.text;
+ var lineObj = getLine(line), text = lineObj.text;
function getX(len) {
measure.innerHTML = "<pre><span>" + lineObj.getHTML(null, null, false, len) + "</span></pre>";
return measure.firstChild.firstChild.offsetWidth;
}
var from = 0, fromX = 0, to = text.length, toX;
// Guess a suitable upper bound for our search.
- var estimated = Math.min(to, Math.ceil(x / stringWidth("x")));
+ var estimated = Math.min(to, Math.ceil(x / charWidth()));
for (;;) {
var estX = getX(estimated);
if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2));
@@ -1172,20 +1373,93 @@ var CodeMirror = (function() {
}
}
+ var tempId = Math.floor(Math.random() * 0xffffff).toString(16);
+ function measureLine(line, ch) {
+ var extra = "";
+ // Include extra text at the end to make sure the measured line is wrapped in the right way.
+ if (options.lineWrapping) {
+ var end = line.text.indexOf(" ", ch + 2);
+ extra = line.text.slice(ch + 1, end < 0 ? line.text.length : end + (ie ? 5 : 0));
+ }
+ measure.innerHTML = "<pre>" + line.getHTML(null, null, false, ch) +
+ '<span id="CodeMirror-temp-' + tempId + '">' + (line.text.charAt(ch) || " ") + "</span>" +
+ extra + "</pre>";
+ var elt = document.getElementById("CodeMirror-temp-" + tempId);
+ var top = elt.offsetTop, left = elt.offsetLeft;
+ // Older IEs report zero offsets for spans directly after a wrap
+ if (ie && ch && top == 0 && left == 0) {
+ var backup = document.createElement("span");
+ backup.innerHTML = "x";
+ elt.parentNode.insertBefore(backup, elt.nextSibling);
+ top = backup.offsetTop;
+ }
+ return {top: top, left: left};
+ }
function localCoords(pos, inLineWrap) {
- var lh = lineHeight(), line = pos.line - (inLineWrap ? showingFrom : 0);
- return {x: charX(pos.line, pos.ch), y: line * lh, yBot: (line + 1) * lh};
+ var x, lh = textHeight(), y = lh * (heightAtLine(doc, pos.line) - (inLineWrap ? displayOffset : 0));
+ if (pos.ch == 0) x = 0;
+ else {
+ var sp = measureLine(getLine(pos.line), pos.ch);
+ x = sp.left;
+ if (options.lineWrapping) y += Math.max(0, sp.top);
+ }
+ return {x: x, y: y, yBot: y + lh};
+ }
+ // Coords must be lineSpace-local
+ function coordsChar(x, y) {
+ if (y < 0) y = 0;
+ var th = textHeight(), cw = charWidth(), heightPos = displayOffset + Math.floor(y / th);
+ var lineNo = lineAtHeight(doc, heightPos);
+ if (lineNo >= doc.size) return {line: doc.size - 1, ch: 0};
+ var lineObj = getLine(lineNo), text = lineObj.text;
+ var tw = options.lineWrapping, innerOff = tw ? heightPos - heightAtLine(doc, lineNo) : 0;
+ if (x <= 0 && innerOff == 0) return {line: lineNo, ch: 0};
+ function getX(len) {
+ var sp = measureLine(lineObj, len);
+ if (tw) {
+ var off = Math.round(sp.top / th);
+ return Math.max(0, sp.left + (off - innerOff) * scroller.clientWidth);
+ }
+ return sp.left;
+ }
+ var from = 0, fromX = 0, to = text.length, toX;
+ // Guess a suitable upper bound for our search.
+ var estimated = Math.min(to, Math.ceil((x + innerOff * scroller.clientWidth * .9) / cw));
+ for (;;) {
+ var estX = getX(estimated);
+ if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2));
+ else {toX = estX; to = estimated; break;}
+ }
+ if (x > toX) return {line: lineNo, ch: to};
+ // Try to guess a suitable lower bound as well.
+ estimated = Math.floor(to * 0.8); estX = getX(estimated);
+ if (estX < x) {from = estimated; fromX = estX;}
+ // Do a binary search between these bounds.
+ for (;;) {
+ if (to - from <= 1) return {line: lineNo, ch: (toX - x > x - fromX) ? from : to};
+ var middle = Math.ceil((from + to) / 2), middleX = getX(middle);
+ if (middleX > x) {to = middle; toX = middleX;}
+ else {from = middle; fromX = middleX;}
+ }
}
function pageCoords(pos) {
var local = localCoords(pos, true), off = eltOffset(lineSpace);
return {x: off.left + local.x, y: off.top + local.y, yBot: off.top + local.yBot};
}
- function lineHeight() {
- var nlines = lineDiv.childNodes.length;
- if (nlines) return (lineDiv.offsetHeight / nlines) || 1;
- measure.innerHTML = "<pre>x</pre>";
- return measure.firstChild.offsetHeight || 1;
+ var cachedHeight, cachedFor;
+ function textHeight() {
+ var offsetHeight = lineDiv.offsetHeight;
+ if (offsetHeight == cachedFor) return cachedHeight;
+ cachedFor = offsetHeight;
+ measure.innerHTML = "<pre>x<br>x<br>x<br>x<br>x<br>x<br>x<br>x<br>x<br>x</pre>";
+ return (cachedHeight = measure.firstChild.offsetHeight / 10 || 1);
+ }
+ var cachedWidth, cachedFor = 0;
+ function charWidth() {
+ if (scroller.clientWidth == cachedFor) return cachedWidth;
+ cachedFor = scroller.clientWidth;
+ return (cachedWidth = stringWidth("x"));
}
function paddingTop() {return lineSpace.offsetTop;}
function paddingLeft() {return lineSpace.offsetLeft;}
@@ -1200,8 +1474,7 @@ var CodeMirror = (function() {
if (!liberal && (x - offW.left > scroller.clientWidth || y - offW.top > scroller.clientHeight))
return null;
var offL = eltOffset(lineSpace, true);
- var line = showingFrom + Math.floor((y - offL.top) / lineHeight());
- return clipPos({line: line, ch: charFromX(clipLine(line), x - offL.left)});
+ return coordsChar(x - offL.left, y - offL.top);
}
function onContextMenu(e) {
var pos = posFromMouse(e);
@@ -1227,7 +1500,7 @@ var CodeMirror = (function() {
prepareInput();
slowPoll();
}
-
+
if (gecko) {
e_stop(e);
var mouseup = connect(window, "mouseup", function() {
@@ -1252,7 +1525,7 @@ var CodeMirror = (function() {
var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"};
function matchBrackets(autoclear) {
- var head = sel.inverted ? sel.from : sel.to, line = lines[head.line], pos = head.ch - 1;
+ var head = sel.inverted ? sel.from : sel.to, line = getLine(head.line), pos = head.ch - 1;
var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)];
if (!match) return;
var ch = match.charAt(0), forward = match.charAt(1) == ">", d = forward ? 1 : -1, st = line.styles;
@@ -1276,18 +1549,16 @@ var CodeMirror = (function() {
}
}
}
- for (var i = head.line, e = forward ? Math.min(i + 100, lines.length) : Math.max(-1, i - 100); i != e; i+=d) {
- var line = lines[i], first = i == head.line;
+ for (var i = head.line, e = forward ? Math.min(i + 100, doc.size) : Math.max(-1, i - 100); i != e; i+=d) {
+ var line = getLine(i), first = i == head.line;
var found = scan(line, first && forward ? pos + 1 : 0, first && !forward ? pos : line.text.length);
if (found) break;
}
if (!found) found = {pos: null, match: false};
var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
var one = markText({line: head.line, ch: pos}, {line: head.line, ch: pos+1}, style),
- two = found.pos != null
- ? markText({line: i, ch: found.pos}, {line: i, ch: found.pos + 1}, style)
- : function() {};
- var clear = operation(function(){one(); two();});
+ two = found.pos != null && markText({line: i, ch: found.pos}, {line: i, ch: found.pos + 1}, style);
+ var clear = operation(function(){one.clear(); two && two.clear();});
if (autoclear) setTimeout(clear, 800);
else bracketHighlighted = clear;
}
@@ -1301,7 +1572,7 @@ var CodeMirror = (function() {
var minindent, minline;
for (var search = n, lim = n - 40; search > lim; --search) {
if (search == 0) return 0;
- var line = lines[search-1];
+ var line = getLine(search-1);
if (line.stateAfter) return search;
var indented = line.indentation();
if (minline == null || minindent > indented) {
@@ -1312,55 +1583,57 @@ var CodeMirror = (function() {
return minline;
}
function getStateBefore(n) {
- var start = findStartLine(n), state = start && lines[start-1].stateAfter;
+ var start = findStartLine(n), state = start && getLine(start-1).stateAfter;
if (!state) state = startState(mode);
else state = copyState(mode, state);
- for (var i = start; i < n; ++i) {
- var line = lines[i];
+ doc.iter(start, n, function(line) {
line.highlight(mode, state);
line.stateAfter = copyState(mode, state);
- }
- if (n < lines.length && !lines[n].stateAfter) work.push(n);
+ });
+ if (start < n) changes.push({from: start, to: n});
+ if (n < doc.size && !getLine(n).stateAfter) work.push(n);
return state;
}
function highlightLines(start, end) {
var state = getStateBefore(start);
- for (var i = start; i < end; ++i) {
- var line = lines[i];
+ doc.iter(start, end, function(line) {
line.highlight(mode, state);
line.stateAfter = copyState(mode, state);
- }
+ });
}
function highlightWorker() {
var end = +new Date + options.workTime;
var foundWork = work.length;
while (work.length) {
- if (!lines[showingFrom].stateAfter) var task = showingFrom;
+ if (!getLine(showingFrom).stateAfter) var task = showingFrom;
else var task = work.pop();
- if (task >= lines.length) continue;
- var start = findStartLine(task), state = start && lines[start-1].stateAfter;
+ if (task >= doc.size) continue;
+ var start = findStartLine(task), state = start && getLine(start-1).stateAfter;
if (state) state = copyState(mode, state);
else state = startState(mode);
- var unchanged = 0, compare = mode.compareStates, realChange = false;
- for (var i = start, l = lines.length; i < l; ++i) {
- var line = lines[i], hadState = line.stateAfter;
+ var unchanged = 0, compare = mode.compareStates, realChange = false,
+ i = start, bail = false;
+ doc.iter(i, doc.size, function(line) {
+ var hadState = line.stateAfter;