annotate .cms/lib/codemirror/doc/internals.html @ 1:1d486627aa1e draft default tip

24.10
author Coffee CMS <info@coffee-cms.ru>
date Sat, 12 Oct 2024 02:51:39 +0000
parents 78edf6b517a0
children
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
0
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
1 <!doctype html>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
2
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
3 <title>CodeMirror: Internals</title>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
4 <meta charset="utf-8"/>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
5 <link rel=stylesheet href="docs.css">
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
6 <style>dl dl {margin: 0;} .update {color: #d40 !important}</style>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
7 <script src="activebookmark.js"></script>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
8
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
9 <div id=nav>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
10 <a href="https://codemirror.net/5"><h1>CodeMirror</h1><img id=logo src="logo.png"></a>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
11
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
12 <ul>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
13 <li><a href="../index.html">Home</a>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
14 <li><a href="manual.html">Manual</a>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
15 <li><a href="https://github.com/codemirror/codemirror5">Code</a>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
16 </ul>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
17 <ul>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
18 <li><a href="#top">Introduction</a></li>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
19 <li><a href="#approach">General Approach</a></li>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
20 <li><a href="#input">Input</a></li>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
21 <li><a href="#selection">Selection</a></li>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
22 <li><a href="#update">Intelligent Updating</a></li>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
23 <li><a href="#parse">Parsing</a></li>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
24 <li><a href="#summary">What Gives?</a></li>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
25 <li><a href="#btree">Content Representation</a></li>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
26 <li><a href="#keymap">Key Maps</a></li>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
27 </ul>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
28 </div>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
29
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
30 <article>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
31
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
32 <h2 id=top>(Re-) Implementing A Syntax-Highlighting Editor in JavaScript</h2>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
33
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
34 <p style="font-size: 85%" id="intro">
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
35 <strong>Topic:</strong> JavaScript, code editor implementation<br>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
36 <strong>Author:</strong> Marijn Haverbeke<br>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
37 <strong>Date:</strong> March 2nd 2011 (updated November 13th 2011)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
38 </p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
39
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
40 <p style="padding: 0 3em 0 2em"><strong>Caution</strong>: this text was written briefly after
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
41 version 2 was initially written. It no longer (even including the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
42 update at the bottom) fully represents the current implementation. I'm
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
43 leaving it here as a historic document. For more up-to-date
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
44 information, look at the entries
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
45 tagged <a href="http://marijnhaverbeke.nl/blog/#cm-internals">cm-internals</a>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
46 on my blog.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
47
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
48 <p>This is a followup to
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
49 my <a href="https://codemirror.net/5/story.html">Brutal Odyssey to the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
50 Dark Side of the DOM Tree</a> story. That one describes the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
51 mind-bending process of implementing (what would become) CodeMirror 1.
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
52 This one describes the internals of CodeMirror 2, a complete rewrite
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
53 and rethink of the old code base. I wanted to give this piece another
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
54 Hunter Thompson copycat subtitle, but somehow that would be out of
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
55 place—the process this time around was one of straightforward
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
56 engineering, requiring no serious mind-bending whatsoever.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
57
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
58 <p>So, what is wrong with CodeMirror 1? I'd estimate, by mailing list
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
59 activity and general search-engine presence, that it has been
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
60 integrated into about a thousand systems by now. The most prominent
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
61 one, since a few weeks,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
62 being <a href="http://googlecode.blogspot.com/2011/01/make-quick-fixes-quicker-on-google.html">Google
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
63 code's project hosting</a>. It works, and it's being used widely.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
64
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
65 <p>Still, I did not start replacing it because I was bored. CodeMirror
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
66 1 was heavily reliant on <code>designMode</code>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
67 or <code>contentEditable</code> (depending on the browser). Neither of
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
68 these are well specified (HTML5 tries
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
69 to <a href="http://www.w3.org/TR/html5/editing.html#contenteditable">specify</a>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
70 their basics), and, more importantly, they tend to be one of the more
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
71 obscure and buggy areas of browser functionality—CodeMirror, by using
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
72 this functionality in a non-typical way, was constantly running up
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
73 against browser bugs. WebKit wouldn't show an empty line at the end of
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
74 the document, and in some releases would suddenly get unbearably slow.
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
75 Firefox would show the cursor in the wrong place. Internet Explorer
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
76 would insist on linkifying everything that looked like a URL or email
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
77 address, a behaviour that can't be turned off. Some bugs I managed to
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
78 work around (which was often a frustrating, painful process), others,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
79 such as the Firefox cursor placement, I gave up on, and had to tell
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
80 user after user that they were known problems, but not something I
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
81 could help.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
82
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
83 <p>Also, there is the fact that <code>designMode</code> (which seemed
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
84 to be less buggy than <code>contentEditable</code> in Webkit and
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
85 Firefox, and was thus used by CodeMirror 1 in those browsers) requires
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
86 a frame. Frames are another tricky area. It takes some effort to
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
87 prevent getting tripped up by domain restrictions, they don't
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
88 initialize synchronously, behave strangely in response to the back
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
89 button, and, on several browsers, can't be moved around the DOM
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
90 without having them re-initialize. They did provide a very nice way to
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
91 namespace the library, though—CodeMirror 1 could freely pollute the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
92 namespace inside the frame.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
93
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
94 <p>Finally, working with an editable document means working with
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
95 selection in arbitrary DOM structures. Internet Explorer (8 and
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
96 before) has an utterly different (and awkward) selection API than all
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
97 of the other browsers, and even among the different implementations of
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
98 <code>document.selection</code>, details about how exactly a selection
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
99 is represented vary quite a bit. Add to that the fact that Opera's
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
100 selection support tended to be very buggy until recently, and you can
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
101 imagine why CodeMirror 1 contains 700 lines of selection-handling
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
102 code.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
103
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
104 <p>And that brings us to the main issue with the CodeMirror 1
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
105 code base: The proportion of browser-bug-workarounds to real
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
106 application code was getting dangerously high. By building on top of a
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
107 few dodgy features, I put the system in a vulnerable position—any
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
108 incompatibility and bugginess in these features, I had to paper over
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
109 with my own code. Not only did I have to do some serious stunt-work to
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
110 get it to work on older browsers (as detailed in the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
111 previous <a href="https://codemirror.net/5/story.html">story</a>), things
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
112 also kept breaking in newly released versions, requiring me to come up
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
113 with <em>new</em> scary hacks in order to keep up. This was starting
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
114 to lose its appeal.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
115
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
116 <section id=approach>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
117 <h2>General Approach</h2>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
118
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
119 <p>What CodeMirror 2 does is try to sidestep most of the hairy hacks
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
120 that came up in version 1. I owe a lot to the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
121 <a href="http://ace.ajax.org">ACE</a> editor for inspiration on how to
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
122 approach this.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
123
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
124 <p>I absolutely did not want to be completely reliant on key events to
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
125 generate my input. Every JavaScript programmer knows that key event
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
126 information is horrible and incomplete. Some people (most awesomely
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
127 Mihai Bazon with <a href="http://ymacs.org">Ymacs</a>) have been able
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
128 to build more or less functioning editors by directly reading key
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
129 events, but it takes a lot of work (the kind of never-ending, fragile
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
130 work I described earlier), and will never be able to properly support
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
131 things like multi-keystoke international character
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
132 input. <a href="#keymap" class="update">[see below for caveat]</a></p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
133
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
134 <p>So what I do is focus a hidden textarea, and let the browser
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
135 believe that the user is typing into that. What we show to the user is
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
136 a DOM structure we built to represent his document. If this is updated
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
137 quickly enough, and shows some kind of believable cursor, it feels
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
138 like a real text-input control.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
139
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
140 <p>Another big win is that this DOM representation does not have to
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
141 span the whole document. Some CodeMirror 1 users insisted that they
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
142 needed to put a 30 thousand line XML document into CodeMirror. Putting
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
143 all that into the DOM takes a while, especially since, for some
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
144 reason, an editable DOM tree is slower than a normal one on most
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
145 browsers. If we have full control over what we show, we must only
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
146 ensure that the visible part of the document has been added, and can
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
147 do the rest only when needed. (Fortunately, the <code>onscroll</code>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
148 event works almost the same on all browsers, and lends itself well to
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
149 displaying things only as they are scrolled into view.)</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
150 </section>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
151 <section id="input">
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
152 <h2>Input</h2>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
153
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
154 <p>ACE uses its hidden textarea only as a text input shim, and does
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
155 all cursor movement and things like text deletion itself by directly
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
156 handling key events. CodeMirror's way is to let the browser do its
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
157 thing as much as possible, and not, for example, define its own set of
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
158 key bindings. One way to do this would have been to have the whole
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
159 document inside the hidden textarea, and after each key event update
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
160 the display DOM to reflect what's in that textarea.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
161
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
162 <p>That'd be simple, but it is not realistic. For even medium-sized
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
163 document the editor would be constantly munging huge strings, and get
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
164 terribly slow. What CodeMirror 2 does is put the current selection,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
165 along with an extra line on the top and on the bottom, into the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
166 textarea.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
167
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
168 <p>This means that the arrow keys (and their ctrl-variations), home,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
169 end, etcetera, do not have to be handled specially. We just read the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
170 cursor position in the textarea, and update our cursor to match it.
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
171 Also, copy and paste work pretty much for free, and people get their
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
172 native key bindings, without any special work on my part. For example,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
173 I have emacs key bindings configured for Chrome and Firefox. There is
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
174 no way for a script to detect this. <a class="update"
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
175 href="#keymap">[no longer the case]</a></p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
176
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
177 <p>Of course, since only a small part of the document sits in the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
178 textarea, keys like page up and ctrl-end won't do the right thing.
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
179 CodeMirror is catching those events and handling them itself.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
180 </section>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
181 <section id="selection">
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
182 <h2>Selection</h2>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
183
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
184 <p>Getting and setting the selection range of a textarea in modern
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
185 browsers is trivial—you just use the <code>selectionStart</code>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
186 and <code>selectionEnd</code> properties. On IE you have to do some
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
187 insane stuff with temporary ranges and compensating for the fact that
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
188 moving the selection by a 'character' will treat \r\n as a single
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
189 character, but even there it is possible to build functions that
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
190 reliably set and get the selection range.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
191
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
192 <p>But consider this typical case: When I'm somewhere in my document,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
193 press shift, and press the up arrow, something gets selected. Then, if
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
194 I, still holding shift, press the up arrow again, the top of my
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
195 selection is adjusted. The selection remembers where its <em>head</em>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
196 and its <em>anchor</em> are, and moves the head when we shift-move.
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
197 This is a generally accepted property of selections, and done right by
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
198 every editing component built in the past twenty years.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
199
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
200 <p>But not something that the browser selection APIs expose.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
201
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
202 <p>Great. So when someone creates an 'upside-down' selection, the next
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
203 time CodeMirror has to update the textarea, it'll re-create the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
204 selection as an 'upside-up' selection, with the anchor at the top, and
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
205 the next cursor motion will behave in an unexpected way—our second
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
206 up-arrow press in the example above will not do anything, since it is
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
207 interpreted in exactly the same way as the first.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
208
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
209 <p>No problem. We'll just, ehm, detect that the selection is
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
210 upside-down (you can tell by the way it was created), and then, when
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
211 an upside-down selection is present, and a cursor-moving key is
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
212 pressed in combination with shift, we quickly collapse the selection
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
213 in the textarea to its start, allow the key to take effect, and then
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
214 combine its new head with its old anchor to get the <em>real</em>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
215 selection.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
216
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
217 <p>In short, scary hacks could not be avoided entirely in CodeMirror
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
218 2.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
219
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
220 <p>And, the observant reader might ask, how do you even know that a
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
221 key combo is a cursor-moving combo, if you claim you support any
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
222 native key bindings? Well, we don't, but we can learn. The editor
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
223 keeps a set known cursor-movement combos (initialized to the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
224 predictable defaults), and updates this set when it observes that
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
225 pressing a certain key had (only) the effect of moving the cursor.
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
226 This, of course, doesn't work if the first time the key is used was
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
227 for extending an inverted selection, but it works most of the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
228 time.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
229 </section>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
230 <section id="update">
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
231 <h2>Intelligent Updating</h2>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
232
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
233 <p>One thing that always comes up when you have a complicated internal
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
234 state that's reflected in some user-visible external representation
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
235 (in this case, the displayed code and the textarea's content) is
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
236 keeping the two in sync. The naive way is to just update the display
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
237 every time you change your state, but this is not only error prone
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
238 (you'll forget), it also easily leads to duplicate work on big,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
239 composite operations. Then you start passing around flags indicating
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
240 whether the display should be updated in an attempt to be efficient
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
241 again and, well, at that point you might as well give up completely.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
242
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
243 <p>I did go down that road, but then switched to a much simpler model:
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
244 simply keep track of all the things that have been changed during an
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
245 action, and then, only at the end, use this information to update the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
246 user-visible display.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
247
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
248 <p>CodeMirror uses a concept of <em>operations</em>, which start by
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
249 calling a specific set-up function that clears the state and end by
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
250 calling another function that reads this state and does the required
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
251 updating. Most event handlers, and all the user-visible methods that
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
252 change state are wrapped like this. There's a method
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
253 called <code>operation</code> that accepts a function, and returns
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
254 another function that wraps the given function as an operation.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
255
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
256 <p>It's trivial to extend this (as CodeMirror does) to detect nesting,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
257 and, when an operation is started inside an operation, simply
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
258 increment the nesting count, and only do the updating when this count
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
259 reaches zero again.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
260
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
261 <p>If we have a set of changed ranges and know the currently shown
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
262 range, we can (with some awkward code to deal with the fact that
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
263 changes can add and remove lines, so we're dealing with a changing
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
264 coordinate system) construct a map of the ranges that were left
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
265 intact. We can then compare this map with the part of the document
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
266 that's currently visible (based on scroll offset and editor height) to
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
267 determine whether something needs to be updated.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
268
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
269 <p>CodeMirror uses two update algorithms—a full refresh, where it just
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
270 discards the whole part of the DOM that contains the edited text and
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
271 rebuilds it, and a patch algorithm, where it uses the information
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
272 about changed and intact ranges to update only the out-of-date parts
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
273 of the DOM. When more than 30 percent (which is the current heuristic,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
274 might change) of the lines need to be updated, the full refresh is
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
275 chosen (since it's faster to do than painstakingly finding and
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
276 updating all the changed lines), in the other case it does the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
277 patching (so that, if you scroll a line or select another character,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
278 the whole screen doesn't have to be
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
279 re-rendered). <span class="update">[the full-refresh
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
280 algorithm was dropped, it wasn't really faster than the patching
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
281 one]</span></p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
282
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
283 <p>All updating uses <code>innerHTML</code> rather than direct DOM
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
284 manipulation, since that still seems to be by far the fastest way to
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
285 build documents. There's a per-line function that combines the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
286 highlighting, <a href="manual.html#markText">marking</a>, and
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
287 selection info for that line into a snippet of HTML. The patch updater
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
288 uses this to reset individual lines, the refresh updater builds an
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
289 HTML chunk for the whole visible document at once, and then uses a
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
290 single <code>innerHTML</code> update to do the refresh.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
291 </section>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
292 <section id="parse">
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
293 <h2>Parsers can be Simple</h2>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
294
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
295 <p>When I wrote CodeMirror 1, I
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
296 thought <a href="https://codemirror.net/5/story.html#parser">interruptible
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
297 parsers</a> were a hugely scary and complicated thing, and I used a
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
298 bunch of heavyweight abstractions to keep this supposed complexity
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
299 under control: parsers
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
300 were <a href="http://bob.pythonmac.org/archives/2005/07/06/iteration-in-javascript/">iterators</a>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
301 that consumed input from another iterator, and used funny
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
302 closure-resetting tricks to copy and resume themselves.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
303
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
304 <p>This made for a rather nice system, in that parsers formed strictly
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
305 separate modules, and could be composed in predictable ways.
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
306 Unfortunately, it was quite slow (stacking three or four iterators on
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
307 top of each other), and extremely intimidating to people not used to a
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
308 functional programming style.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
309
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
310 <p>With a few small changes, however, we can keep all those
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
311 advantages, but simplify the API and make the whole thing less
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
312 indirect and inefficient. CodeMirror
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
313 2's <a href="manual.html#modeapi">mode API</a> uses explicit state
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
314 objects, and makes the parser/tokenizer a function that simply takes a
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
315 state and a character stream abstraction, advances the stream one
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
316 token, and returns the way the token should be styled. This state may
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
317 be copied, optionally in a mode-defined way, in order to be able to
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
318 continue a parse at a given point. Even someone who's never touched a
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
319 lambda in his life can understand this approach. Additionally, far
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
320 fewer objects are allocated in the course of parsing now.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
321
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
322 <p>The biggest speedup comes from the fact that the parsing no longer
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
323 has to touch the DOM though. In CodeMirror 1, on an older browser, you
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
324 could <em>see</em> the parser work its way through the document,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
325 managing some twenty lines in each 50-millisecond time slice it got. It
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
326 was reading its input from the DOM, and updating the DOM as it went
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
327 along, which any experienced JavaScript programmer will immediately
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
328 spot as a recipe for slowness. In CodeMirror 2, the parser usually
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
329 finishes the whole document in a single 100-millisecond time slice—it
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
330 manages some 1500 lines during that time on Chrome. All it has to do
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
331 is munge strings, so there is no real reason for it to be slow
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
332 anymore.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
333 </section>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
334 <section id="summary">
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
335 <h2>What Gives?</h2>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
336
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
337 <p>Given all this, what can you expect from CodeMirror 2?</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
338
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
339 <ul>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
340
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
341 <li><strong>Small.</strong> the base library is
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
342 some <span class="update">45k</span> when minified
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
343 now, <span class="update">17k</span> when gzipped. It's smaller than
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
344 its own logo.</li>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
345
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
346 <li><strong>Lightweight.</strong> CodeMirror 2 initializes very
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
347 quickly, and does almost no work when it is not focused. This means
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
348 you can treat it almost like a textarea, have multiple instances on a
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
349 page without trouble.</li>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
350
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
351 <li><strong>Huge document support.</strong> Since highlighting is
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
352 really fast, and no DOM structure is being built for non-visible
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
353 content, you don't have to worry about locking up your browser when a
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
354 user enters a megabyte-sized document.</li>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
355
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
356 <li><strong>Extended API.</strong> Some things kept coming up in the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
357 mailing list, such as marking pieces of text or lines, which were
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
358 extremely hard to do with CodeMirror 1. The new version has proper
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
359 support for these built in.</li>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
360
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
361 <li><strong>Tab support.</strong> Tabs inside editable documents were,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
362 for some reason, a no-go. At least six different people announced they
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
363 were going to add tab support to CodeMirror 1, none survived (I mean,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
364 none delivered a working version). CodeMirror 2 no longer removes tabs
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
365 from your document.</li>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
366
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
367 <li><strong>Sane styling.</strong> <code>iframe</code> nodes aren't
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
368 really known for respecting document flow. Now that an editor instance
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
369 is a plain <code>div</code> element, it is much easier to size it to
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
370 fit the surrounding elements. You don't even have to make it scroll if
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
371 you do not <a href="../demo/resize.html">want to</a>.</li>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
372
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
373 </ul>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
374
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
375 <p>On the downside, a CodeMirror 2 instance is <em>not</em> a native
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
376 editable component. Though it does its best to emulate such a
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
377 component as much as possible, there is functionality that browsers
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
378 just do not allow us to hook into. Doing select-all from the context
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
379 menu, for example, is not currently detected by CodeMirror.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
380
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
381 <p id="changes" style="margin-top: 2em;"><span style="font-weight:
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
382 bold">[Updates from November 13th 2011]</span> Recently, I've made
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
383 some changes to the codebase that cause some of the text above to no
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
384 longer be current. I've left the text intact, but added markers at the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
385 passages that are now inaccurate. The new situation is described
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
386 below.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
387 </section>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
388 <section id="btree">
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
389 <h2>Content Representation</h2>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
390
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
391 <p>The original implementation of CodeMirror 2 represented the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
392 document as a flat array of line objects. This worked well—splicing
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
393 arrays will require the part of the array after the splice to be
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
394 moved, but this is basically just a simple <code>memmove</code> of a
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
395 bunch of pointers, so it is cheap even for huge documents.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
396
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
397 <p>However, I recently added line wrapping and code folding (line
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
398 collapsing, basically). Once lines start taking up a non-constant
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
399 amount of vertical space, looking up a line by vertical position
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
400 (which is needed when someone clicks the document, and to determine
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
401 the visible part of the document during scrolling) can only be done
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
402 with a linear scan through the whole array, summing up line heights as
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
403 you go. Seeing how I've been going out of my way to make big documents
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
404 fast, this is not acceptable.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
405
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
406 <p>The new representation is based on a B-tree. The leaves of the tree
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
407 contain arrays of line objects, with a fixed minimum and maximum size,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
408 and the non-leaf nodes simply hold arrays of child nodes. Each node
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
409 stores both the amount of lines that live below them and the vertical
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
410 space taken up by these lines. This allows the tree to be indexed both
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
411 by line number and by vertical position, and all access has
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
412 logarithmic complexity in relation to the document size.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
413
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
414 <p>I gave line objects and tree nodes parent pointers, to the node
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
415 above them. When a line has to update its height, it can simply walk
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
416 these pointers to the top of the tree, adding or subtracting the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
417 difference in height from each node it encounters. The parent pointers
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
418 also make it cheaper (in complexity terms, the difference is probably
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
419 tiny in normal-sized documents) to find the current line number when
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
420 given a line object. In the old approach, the whole document array had
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
421 to be searched. Now, we can just walk up the tree and count the sizes
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
422 of the nodes coming before us at each level.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
423
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
424 <p>I chose B-trees, not regular binary trees, mostly because they
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
425 allow for very fast bulk insertions and deletions. When there is a big
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
426 change to a document, it typically involves adding, deleting, or
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
427 replacing a chunk of subsequent lines. In a regular balanced tree, all
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
428 these inserts or deletes would have to be done separately, which could
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
429 be really expensive. In a B-tree, to insert a chunk, you just walk
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
430 down the tree once to find where it should go, insert them all in one
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
431 shot, and then break up the node if needed. This breaking up might
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
432 involve breaking up nodes further up, but only requires a single pass
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
433 back up the tree. For deletion, I'm somewhat lax in keeping things
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
434 balanced—I just collapse nodes into a leaf when their child count goes
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
435 below a given number. This means that there are some weird editing
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
436 patterns that may result in a seriously unbalanced tree, but even such
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
437 an unbalanced tree will perform well, unless you spend a day making
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
438 strangely repeating edits to a really big document.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
439 </section>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
440 <section id="keymap">
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
441 <h2>Keymaps</h2>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
442
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
443 <p><a href="#approach">Above</a>, I claimed that directly catching key
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
444 events for things like cursor movement is impractical because it
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
445 requires some browser-specific kludges. I then proceeded to explain
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
446 some awful <a href="#selection">hacks</a> that were needed to make it
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
447 possible for the selection changes to be detected through the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
448 textarea. In fact, the second hack is about as bad as the first.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
449
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
450 <p>On top of that, in the presence of user-configurable tab sizes and
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
451 collapsed and wrapped lines, lining up cursor movement in the textarea
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
452 with what's visible on the screen becomes a nightmare. Thus, I've
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
453 decided to move to a model where the textarea's selection is no longer
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
454 depended on.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
455
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
456 <p>So I moved to a model where all cursor movement is handled by my
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
457 own code. This adds support for a goal column, proper interaction of
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
458 cursor movement with collapsed lines, and makes it possible for
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
459 vertical movement to move through wrapped lines properly, instead of
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
460 just treating them like non-wrapped lines.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
461
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
462 <p>The key event handlers now translate the key event into a string,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
463 something like <code>Ctrl-Home</code> or <code>Shift-Cmd-R</code>, and
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
464 use that string to look up an action to perform. To make keybinding
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
465 customizable, this lookup goes through
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
466 a <a href="manual.html#option_keyMap">table</a>, using a scheme that
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
467 allows such tables to be chained together (for example, the default
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
468 Mac bindings fall through to a table named 'emacsy', which defines
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
469 basic Emacs-style bindings like <code>Ctrl-F</code>, and which is also
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
470 used by the custom Emacs bindings).</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
471
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
472 <p>A new
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
473 option <a href="manual.html#option_extraKeys"><code>extraKeys</code></a>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
474 allows ad-hoc keybindings to be defined in a much nicer way than what
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
475 was possible with the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
476 old <a href="manual.html#option_onKeyEvent"><code>onKeyEvent</code></a>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
477 callback. You simply provide an object mapping key identifiers to
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
478 functions, instead of painstakingly looking at raw key events.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
479
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
480 <p>Built-in commands map to strings, rather than functions, for
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
481 example <code>"goLineUp"</code> is the default action bound to the up
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
482 arrow key. This allows new keymaps to refer to them without
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
483 duplicating any code. New commands can be defined by assigning to
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
484 the <code>CodeMirror.commands</code> object, which maps such commands
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
485 to functions.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
486
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
487 <p>The hidden textarea now only holds the current selection, with no
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
488 extra characters around it. This has a nice advantage: polling for
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
489 input becomes much, much faster. If there's a big selection, this text
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
490 does not have to be read from the textarea every time—when we poll,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
491 just noticing that something is still selected is enough to tell us
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
492 that no new text was typed.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
493
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
494 <p>The reason that cheap polling is important is that many browsers do
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
495 not fire useful events on IME (input method engine) input, which is
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
496 the thing where people inputting a language like Japanese or Chinese
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
497 use multiple keystrokes to create a character or sequence of
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
498 characters. Most modern browsers fire <code>input</code> when the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
499 composing is finished, but many don't fire anything when the character
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
500 is updated <em>during</em> composition. So we poll, whenever the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
501 editor is focused, to provide immediate updates of the display.</p>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
502
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
503 </section>
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
504 </article>