maglev-webtools 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE.txt +25 -0
- data/README.rdoc +121 -0
- data/bin/webtools +15 -0
- data/lib/web_tools/#debugger.rb# +212 -0
- data/lib/web_tools/browser.rb +45 -0
- data/lib/web_tools/debugger.rb +219 -0
- data/lib/web_tools/info.rb +29 -0
- data/lib/web_tools/middleware/debugger.rb +118 -0
- data/lib/web_tools/support/app_model.rb +117 -0
- data/lib/web_tools/support/code_browser.rb +109 -0
- data/lib/web_tools/support/ruby.rb +132 -0
- data/lib/web_tools/support/service_helper.rb +22 -0
- data/lib/web_tools/support/smalltalk_extensions.rb +65 -0
- data/lib/web_tools/support/smalltalk_tools.rb +16 -0
- data/lib/web_tools/ui.rb +67 -0
- data/lib/web_tools.rb +10 -0
- data/public/images/favicon.ico +0 -0
- data/public/javascript/CodeMirror/LICENSE +23 -0
- data/public/javascript/CodeMirror/css/Smalltalk.css +34 -0
- data/public/javascript/CodeMirror/js/codemirror.js +582 -0
- data/public/javascript/CodeMirror/js/editor.js +1671 -0
- data/public/javascript/CodeMirror/js/highlight.js +68 -0
- data/public/javascript/CodeMirror/js/parseSmalltalk.js +126 -0
- data/public/javascript/CodeMirror/js/parsedummy.js +32 -0
- data/public/javascript/CodeMirror/js/select.js +699 -0
- data/public/javascript/CodeMirror/js/stringstream.js +159 -0
- data/public/javascript/CodeMirror/js/tokenize.js +57 -0
- data/public/javascript/CodeMirror/js/undo.js +413 -0
- data/public/javascript/CodeMirror/js/util.js +133 -0
- data/public/javascript/CodeMirror/testSmalltalkParser.html +116 -0
- data/public/javascript/ace/ace-uncompressed.js +17299 -0
- data/public/javascript/ace/ace.js +1 -0
- data/public/javascript/ace/keybinding-emacs.js +1 -0
- data/public/javascript/ace/keybinding-vim.js +1 -0
- data/public/javascript/ace/mode-c_cpp.js +1 -0
- data/public/javascript/ace/mode-clojure.js +1 -0
- data/public/javascript/ace/mode-coffee.js +1 -0
- data/public/javascript/ace/mode-csharp.js +1 -0
- data/public/javascript/ace/mode-css.js +1 -0
- data/public/javascript/ace/mode-groovy.js +1 -0
- data/public/javascript/ace/mode-html.js +1 -0
- data/public/javascript/ace/mode-java.js +1 -0
- data/public/javascript/ace/mode-javascript.js +1 -0
- data/public/javascript/ace/mode-json.js +1 -0
- data/public/javascript/ace/mode-lua.js +1 -0
- data/public/javascript/ace/mode-markdown.js +1 -0
- data/public/javascript/ace/mode-ocaml.js +1 -0
- data/public/javascript/ace/mode-perl.js +1 -0
- data/public/javascript/ace/mode-php.js +1 -0
- data/public/javascript/ace/mode-python.js +1 -0
- data/public/javascript/ace/mode-ruby.js +1 -0
- data/public/javascript/ace/mode-scad.js +1 -0
- data/public/javascript/ace/mode-scala.js +1 -0
- data/public/javascript/ace/mode-scss.js +1 -0
- data/public/javascript/ace/mode-svg.js +1 -0
- data/public/javascript/ace/mode-textile.js +1 -0
- data/public/javascript/ace/mode-xml.js +1 -0
- data/public/javascript/ace/theme-clouds.js +1 -0
- data/public/javascript/ace/theme-clouds_midnight.js +1 -0
- data/public/javascript/ace/theme-cobalt.js +1 -0
- data/public/javascript/ace/theme-crimson_editor.js +1 -0
- data/public/javascript/ace/theme-dawn.js +1 -0
- data/public/javascript/ace/theme-eclipse.js +1 -0
- data/public/javascript/ace/theme-idle_fingers.js +1 -0
- data/public/javascript/ace/theme-kr_theme.js +1 -0
- data/public/javascript/ace/theme-merbivore.js +1 -0
- data/public/javascript/ace/theme-merbivore_soft.js +1 -0
- data/public/javascript/ace/theme-mono_industrial.js +1 -0
- data/public/javascript/ace/theme-monokai.js +1 -0
- data/public/javascript/ace/theme-pastel_on_dark.js +1 -0
- data/public/javascript/ace/theme-solarized_dark.js +1 -0
- data/public/javascript/ace/theme-solarized_light.js +1 -0
- data/public/javascript/ace/theme-textmate.js +1 -0
- data/public/javascript/ace/theme-twilight.js +1 -0
- data/public/javascript/ace/theme-vibrant_ink.js +1 -0
- data/public/javascript/ace/worker-coffee.js +1 -0
- data/public/javascript/ace/worker-css.js +1 -0
- data/public/javascript/ace/worker-javascript.js +1 -0
- data/public/javascript/webtools/#debugger.coffee# +253 -0
- data/public/javascript/webtools/browser.js +260 -0
- data/public/javascript/webtools/debugger.coffee +286 -0
- data/public/javascript/webtools/debugger.js +366 -0
- data/public/javascript/webtools/sessions.coffee +17 -0
- data/public/javascript/webtools/sessions.js +27 -0
- data/public/javascript/webtools/version.coffee +14 -0
- data/public/javascript/webtools/version.js +20 -0
- data/public/stylesheets/base/images/ui-anim_basic_16x16.gif +0 -0
- data/public/stylesheets/base/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/public/stylesheets/base/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/public/stylesheets/base/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/public/stylesheets/base/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/public/stylesheets/base/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/public/stylesheets/base/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/public/stylesheets/base/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/public/stylesheets/base/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/public/stylesheets/base/images/ui-icons_222222_256x240.png +0 -0
- data/public/stylesheets/base/images/ui-icons_2e83ff_256x240.png +0 -0
- data/public/stylesheets/base/images/ui-icons_454545_256x240.png +0 -0
- data/public/stylesheets/base/images/ui-icons_888888_256x240.png +0 -0
- data/public/stylesheets/base/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/public/stylesheets/base/jquery.ui.accordion.css +12 -0
- data/public/stylesheets/base/jquery.ui.all.css +2 -0
- data/public/stylesheets/base/jquery.ui.autocomplete.css +39 -0
- data/public/stylesheets/base/jquery.ui.base.css +11 -0
- data/public/stylesheets/base/jquery.ui.button.css +35 -0
- data/public/stylesheets/base/jquery.ui.core.css +37 -0
- data/public/stylesheets/base/jquery.ui.datepicker.css +61 -0
- data/public/stylesheets/base/jquery.ui.dialog.css +13 -0
- data/public/stylesheets/base/jquery.ui.progressbar.css +4 -0
- data/public/stylesheets/base/jquery.ui.resizable.css +13 -0
- data/public/stylesheets/base/jquery.ui.selectable.css +3 -0
- data/public/stylesheets/base/jquery.ui.slider.css +17 -0
- data/public/stylesheets/base/jquery.ui.tabs.css +11 -0
- data/public/stylesheets/base/jquery.ui.theme.css +247 -0
- data/public/stylesheets/jquery.contextMenu.css +62 -0
- data/public/stylesheets/reset.css +18 -0
- data/public/stylesheets/webtools.css +53 -0
- data/public/test.html +47 -0
- data/views/browser.rhtml +63 -0
- data/views/debugger.rhtml +59 -0
- data/views/index.rhtml +8 -0
- data/views/layout.rhtml +24 -0
- data/views/sessions.rhtml +12 -0
- data/views/version.rhtml +10 -0
- metadata +316 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/* String streams are the things fed to parsers (which can feed them
|
|
2
|
+
* to a tokenizer if they want). They provide peek and next methods
|
|
3
|
+
* for looking at the current character (next 'consumes' this
|
|
4
|
+
* character, peek does not), and a get method for retrieving all the
|
|
5
|
+
* text that was consumed since the last time get was called.
|
|
6
|
+
*
|
|
7
|
+
* An easy mistake to make is to let a StopIteration exception finish
|
|
8
|
+
* the token stream while there are still characters pending in the
|
|
9
|
+
* string stream (hitting the end of the buffer while parsing a
|
|
10
|
+
* token). To make it easier to detect such errors, the stringstreams
|
|
11
|
+
* throw an exception when this happens.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Make a stringstream stream out of an iterator that returns strings.
|
|
15
|
+
// This is applied to the result of traverseDOM (see codemirror.js),
|
|
16
|
+
// and the resulting stream is fed to the parser.
|
|
17
|
+
var stringStream = function(source){
|
|
18
|
+
// String that's currently being iterated over.
|
|
19
|
+
var current = "";
|
|
20
|
+
// Position in that string.
|
|
21
|
+
var pos = 0;
|
|
22
|
+
// Accumulator for strings that have been iterated over but not
|
|
23
|
+
// get()-ed yet.
|
|
24
|
+
var accum = "";
|
|
25
|
+
// Make sure there are more characters ready, or throw
|
|
26
|
+
// StopIteration.
|
|
27
|
+
function ensureChars() {
|
|
28
|
+
while (pos == current.length) {
|
|
29
|
+
accum += current;
|
|
30
|
+
current = ""; // In case source.next() throws
|
|
31
|
+
pos = 0;
|
|
32
|
+
try {current = source.next();}
|
|
33
|
+
catch (e) {
|
|
34
|
+
if (e != StopIteration) throw e;
|
|
35
|
+
else return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
// peek: -> character
|
|
43
|
+
// Return the next character in the stream.
|
|
44
|
+
peek: function() {
|
|
45
|
+
if (!ensureChars()) return null;
|
|
46
|
+
return current.charAt(pos);
|
|
47
|
+
},
|
|
48
|
+
// next: -> character
|
|
49
|
+
// Get the next character, throw StopIteration if at end, check
|
|
50
|
+
// for unused content.
|
|
51
|
+
next: function() {
|
|
52
|
+
if (!ensureChars()) {
|
|
53
|
+
if (accum.length > 0)
|
|
54
|
+
throw "End of stringstream reached without emptying buffer ('" + accum + "').";
|
|
55
|
+
else
|
|
56
|
+
throw StopIteration;
|
|
57
|
+
}
|
|
58
|
+
return current.charAt(pos++);
|
|
59
|
+
},
|
|
60
|
+
// get(): -> string
|
|
61
|
+
// Return the characters iterated over since the last call to
|
|
62
|
+
// .get().
|
|
63
|
+
get: function() {
|
|
64
|
+
var temp = accum;
|
|
65
|
+
accum = "";
|
|
66
|
+
if (pos > 0){
|
|
67
|
+
temp += current.slice(0, pos);
|
|
68
|
+
current = current.slice(pos);
|
|
69
|
+
pos = 0;
|
|
70
|
+
}
|
|
71
|
+
return temp;
|
|
72
|
+
},
|
|
73
|
+
// Push a string back into the stream.
|
|
74
|
+
push: function(str) {
|
|
75
|
+
current = current.slice(0, pos) + str + current.slice(pos);
|
|
76
|
+
},
|
|
77
|
+
lookAhead: function(str, consume, skipSpaces, caseInsensitive) {
|
|
78
|
+
function cased(str) {return caseInsensitive ? str.toLowerCase() : str;}
|
|
79
|
+
str = cased(str);
|
|
80
|
+
var found = false;
|
|
81
|
+
|
|
82
|
+
var _accum = accum, _pos = pos;
|
|
83
|
+
if (skipSpaces) this.nextWhileMatches(/[\s\u00a0]/);
|
|
84
|
+
|
|
85
|
+
while (true) {
|
|
86
|
+
var end = pos + str.length, left = current.length - pos;
|
|
87
|
+
if (end <= current.length) {
|
|
88
|
+
found = str == cased(current.slice(pos, end));
|
|
89
|
+
pos = end;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
else if (str.slice(0, left) == cased(current.slice(pos))) {
|
|
93
|
+
accum += current; current = "";
|
|
94
|
+
try {current = source.next();}
|
|
95
|
+
catch (e) {if (e != StopIteration) throw e; break;}
|
|
96
|
+
pos = 0;
|
|
97
|
+
str = str.slice(left);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!(found && consume)) {
|
|
105
|
+
current = accum.slice(_accum.length) + current;
|
|
106
|
+
pos = _pos;
|
|
107
|
+
accum = _accum;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return found;
|
|
111
|
+
},
|
|
112
|
+
// Wont't match past end of line.
|
|
113
|
+
lookAheadRegex: function(regex, consume) {
|
|
114
|
+
if (regex.source.charAt(0) != "^")
|
|
115
|
+
throw new Error("Regexps passed to lookAheadRegex must start with ^");
|
|
116
|
+
|
|
117
|
+
// Fetch the rest of the line
|
|
118
|
+
while (current.indexOf("\n", pos) == -1) {
|
|
119
|
+
try {current += source.next();}
|
|
120
|
+
catch (e) {if (e != StopIteration) throw e; break;}
|
|
121
|
+
}
|
|
122
|
+
var matched = current.slice(pos).match(regex);
|
|
123
|
+
if (matched && consume) pos += matched[0].length;
|
|
124
|
+
return matched;
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// Utils built on top of the above
|
|
128
|
+
// more: -> boolean
|
|
129
|
+
// Produce true if the stream isn't empty.
|
|
130
|
+
more: function() {
|
|
131
|
+
return this.peek() !== null;
|
|
132
|
+
},
|
|
133
|
+
applies: function(test) {
|
|
134
|
+
var next = this.peek();
|
|
135
|
+
return (next !== null && test(next));
|
|
136
|
+
},
|
|
137
|
+
nextWhile: function(test) {
|
|
138
|
+
var next;
|
|
139
|
+
while ((next = this.peek()) !== null && test(next))
|
|
140
|
+
this.next();
|
|
141
|
+
},
|
|
142
|
+
matches: function(re) {
|
|
143
|
+
var next = this.peek();
|
|
144
|
+
return (next !== null && re.test(next));
|
|
145
|
+
},
|
|
146
|
+
nextWhileMatches: function(re) {
|
|
147
|
+
var next;
|
|
148
|
+
while ((next = this.peek()) !== null && re.test(next))
|
|
149
|
+
this.next();
|
|
150
|
+
},
|
|
151
|
+
equals: function(ch) {
|
|
152
|
+
return ch === this.peek();
|
|
153
|
+
},
|
|
154
|
+
endOfLine: function() {
|
|
155
|
+
var next = this.peek();
|
|
156
|
+
return next == null || next == "\n";
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// A framework for simple tokenizers. Takes care of newlines and
|
|
2
|
+
// white-space, and of getting the text from the source stream into
|
|
3
|
+
// the token object. A state is a function of two arguments -- a
|
|
4
|
+
// string stream and a setState function. The second can be used to
|
|
5
|
+
// change the tokenizer's state, and can be ignored for stateless
|
|
6
|
+
// tokenizers. This function should advance the stream over a token
|
|
7
|
+
// and return a string or object containing information about the next
|
|
8
|
+
// token, or null to pass and have the (new) state be called to finish
|
|
9
|
+
// the token. When a string is given, it is wrapped in a {style, type}
|
|
10
|
+
// object. In the resulting object, the characters consumed are stored
|
|
11
|
+
// under the content property. Any whitespace following them is also
|
|
12
|
+
// automatically consumed, and added to the value property. (Thus,
|
|
13
|
+
// content is the actual meaningful part of the token, while value
|
|
14
|
+
// contains all the text it spans.)
|
|
15
|
+
|
|
16
|
+
function tokenizer(source, state) {
|
|
17
|
+
// Newlines are always a separate token.
|
|
18
|
+
function isWhiteSpace(ch) {
|
|
19
|
+
// The messy regexp is because IE's regexp matcher is of the
|
|
20
|
+
// opinion that non-breaking spaces are no whitespace.
|
|
21
|
+
return ch != "\n" && /^[\s\u00a0]*$/.test(ch);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var tokenizer = {
|
|
25
|
+
state: state,
|
|
26
|
+
|
|
27
|
+
take: function(type) {
|
|
28
|
+
if (typeof(type) == "string")
|
|
29
|
+
type = {style: type, type: type};
|
|
30
|
+
|
|
31
|
+
type.content = (type.content || "") + source.get();
|
|
32
|
+
if (!/\n$/.test(type.content))
|
|
33
|
+
source.nextWhile(isWhiteSpace);
|
|
34
|
+
type.value = type.content + source.get();
|
|
35
|
+
return type;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
next: function () {
|
|
39
|
+
if (!source.more()) throw StopIteration;
|
|
40
|
+
|
|
41
|
+
var type;
|
|
42
|
+
if (source.equals("\n")) {
|
|
43
|
+
source.next();
|
|
44
|
+
return this.take("whitespace");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (source.applies(isWhiteSpace))
|
|
48
|
+
type = "whitespace";
|
|
49
|
+
else
|
|
50
|
+
while (!type)
|
|
51
|
+
type = this.state(source, function(s) {tokenizer.state = s;});
|
|
52
|
+
|
|
53
|
+
return this.take(type);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
return tokenizer;
|
|
57
|
+
}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage and control for undo information within a CodeMirror
|
|
3
|
+
* editor. 'Why on earth is such a complicated mess required for
|
|
4
|
+
* that?', I hear you ask. The goal, in implementing this, was to make
|
|
5
|
+
* the complexity of storing and reverting undo information depend
|
|
6
|
+
* only on the size of the edited or restored content, not on the size
|
|
7
|
+
* of the whole document. This makes it necessary to use a kind of
|
|
8
|
+
* 'diff' system, which, when applied to a DOM tree, causes some
|
|
9
|
+
* complexity and hackery.
|
|
10
|
+
*
|
|
11
|
+
* In short, the editor 'touches' BR elements as it parses them, and
|
|
12
|
+
* the UndoHistory stores these. When nothing is touched in commitDelay
|
|
13
|
+
* milliseconds, the changes are committed: It goes over all touched
|
|
14
|
+
* nodes, throws out the ones that did not change since last commit or
|
|
15
|
+
* are no longer in the document, and assembles the rest into zero or
|
|
16
|
+
* more 'chains' -- arrays of adjacent lines. Links back to these
|
|
17
|
+
* chains are added to the BR nodes, while the chain that previously
|
|
18
|
+
* spanned these nodes is added to the undo history. Undoing a change
|
|
19
|
+
* means taking such a chain off the undo history, restoring its
|
|
20
|
+
* content (text is saved per line) and linking it back into the
|
|
21
|
+
* document.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// A history object needs to know about the DOM container holding the
|
|
25
|
+
// document, the maximum amount of undo levels it should store, the
|
|
26
|
+
// delay (of no input) after which it commits a set of changes, and,
|
|
27
|
+
// unfortunately, the 'parent' window -- a window that is not in
|
|
28
|
+
// designMode, and on which setTimeout works in every browser.
|
|
29
|
+
function UndoHistory(container, maxDepth, commitDelay, editor) {
|
|
30
|
+
this.container = container;
|
|
31
|
+
this.maxDepth = maxDepth; this.commitDelay = commitDelay;
|
|
32
|
+
this.editor = editor;
|
|
33
|
+
// This line object represents the initial, empty editor.
|
|
34
|
+
var initial = {text: "", from: null, to: null};
|
|
35
|
+
// As the borders between lines are represented by BR elements, the
|
|
36
|
+
// start of the first line and the end of the last one are
|
|
37
|
+
// represented by null. Since you can not store any properties
|
|
38
|
+
// (links to line objects) in null, these properties are used in
|
|
39
|
+
// those cases.
|
|
40
|
+
this.first = initial; this.last = initial;
|
|
41
|
+
// Similarly, a 'historyTouched' property is added to the BR in
|
|
42
|
+
// front of lines that have already been touched, and 'firstTouched'
|
|
43
|
+
// is used for the first line.
|
|
44
|
+
this.firstTouched = false;
|
|
45
|
+
// History is the set of committed changes, touched is the set of
|
|
46
|
+
// nodes touched since the last commit.
|
|
47
|
+
this.history = []; this.redoHistory = []; this.touched = []; this.lostundo = 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
UndoHistory.prototype = {
|
|
51
|
+
// Schedule a commit (if no other touches come in for commitDelay
|
|
52
|
+
// milliseconds).
|
|
53
|
+
scheduleCommit: function() {
|
|
54
|
+
var self = this;
|
|
55
|
+
parent.clearTimeout(this.commitTimeout);
|
|
56
|
+
this.commitTimeout = parent.setTimeout(function(){self.tryCommit();}, this.commitDelay);
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// Mark a node as touched. Null is a valid argument.
|
|
60
|
+
touch: function(node) {
|
|
61
|
+
this.setTouched(node);
|
|
62
|
+
this.scheduleCommit();
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Undo the last change.
|
|
66
|
+
undo: function() {
|
|
67
|
+
// Make sure pending changes have been committed.
|
|
68
|
+
this.commit();
|
|
69
|
+
|
|
70
|
+
if (this.history.length) {
|
|
71
|
+
// Take the top diff from the history, apply it, and store its
|
|
72
|
+
// shadow in the redo history.
|
|
73
|
+
var item = this.history.pop();
|
|
74
|
+
this.redoHistory.push(this.updateTo(item, "applyChain"));
|
|
75
|
+
this.notifyEnvironment();
|
|
76
|
+
return this.chainNode(item);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Redo the last undone change.
|
|
81
|
+
redo: function() {
|
|
82
|
+
this.commit();
|
|
83
|
+
if (this.redoHistory.length) {
|
|
84
|
+
// The inverse of undo, basically.
|
|
85
|
+
var item = this.redoHistory.pop();
|
|
86
|
+
this.addUndoLevel(this.updateTo(item, "applyChain"));
|
|
87
|
+
this.notifyEnvironment();
|
|
88
|
+
return this.chainNode(item);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
clear: function() {
|
|
93
|
+
this.history = [];
|
|
94
|
+
this.redoHistory = [];
|
|
95
|
+
this.lostundo = 0;
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// Ask for the size of the un/redo histories.
|
|
99
|
+
historySize: function() {
|
|
100
|
+
return {undo: this.history.length, redo: this.redoHistory.length, lostundo: this.lostundo};
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// Push a changeset into the document.
|
|
104
|
+
push: function(from, to, lines) {
|
|
105
|
+
var chain = [];
|
|
106
|
+
for (var i = 0; i < lines.length; i++) {
|
|
107
|
+
var end = (i == lines.length - 1) ? to : document.createElement("br");
|
|
108
|
+
chain.push({from: from, to: end, text: cleanText(lines[i])});
|
|
109
|
+
from = end;
|
|
110
|
+
}
|
|
111
|
+
this.pushChains([chain], from == null && to == null);
|
|
112
|
+
this.notifyEnvironment();
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
pushChains: function(chains, doNotHighlight) {
|
|
116
|
+
this.commit(doNotHighlight);
|
|
117
|
+
this.addUndoLevel(this.updateTo(chains, "applyChain"));
|
|
118
|
+
this.redoHistory = [];
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// Retrieve a DOM node from a chain (for scrolling to it after undo/redo).
|
|
122
|
+
chainNode: function(chains) {
|
|
123
|
+
for (var i = 0; i < chains.length; i++) {
|
|
124
|
+
var start = chains[i][0], node = start && (start.from || start.to);
|
|
125
|
+
if (node) return node;
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// Clear the undo history, make the current document the start
|
|
130
|
+
// position.
|
|
131
|
+
reset: function() {
|
|
132
|
+
this.history = []; this.redoHistory = []; this.lostundo = 0;
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
textAfter: function(br) {
|
|
136
|
+
return this.after(br).text;
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
nodeAfter: function(br) {
|
|
140
|
+
return this.after(br).to;
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
nodeBefore: function(br) {
|
|
144
|
+
return this.before(br).from;
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// Commit unless there are pending dirty nodes.
|
|
148
|
+
tryCommit: function() {
|
|
149
|
+
if (!window || !window.parent || !window.UndoHistory) return; // Stop when frame has been unloaded
|
|
150
|
+
if (this.editor.highlightDirty()) this.commit(true);
|
|
151
|
+
else this.scheduleCommit();
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// Check whether the touched nodes hold any changes, if so, commit
|
|
155
|
+
// them.
|
|
156
|
+
commit: function(doNotHighlight) {
|
|
157
|
+
parent.clearTimeout(this.commitTimeout);
|
|
158
|
+
// Make sure there are no pending dirty nodes.
|
|
159
|
+
if (!doNotHighlight) this.editor.highlightDirty(true);
|
|
160
|
+
// Build set of chains.
|
|
161
|
+
var chains = this.touchedChains(), self = this;
|
|
162
|
+
|
|
163
|
+
if (chains.length) {
|
|
164
|
+
this.addUndoLevel(this.updateTo(chains, "linkChain"));
|
|
165
|
+
this.redoHistory = [];
|
|
166
|
+
this.notifyEnvironment();
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
// [ end of public interface ]
|
|
171
|
+
|
|
172
|
+
// Update the document with a given set of chains, return its
|
|
173
|
+
// shadow. updateFunc should be "applyChain" or "linkChain". In the
|
|
174
|
+
// second case, the chains are taken to correspond the the current
|
|
175
|
+
// document, and only the state of the line data is updated. In the
|
|
176
|
+
// first case, the content of the chains is also pushed iinto the
|
|
177
|
+
// document.
|
|
178
|
+
updateTo: function(chains, updateFunc) {
|
|
179
|
+
var shadows = [], dirty = [];
|
|
180
|
+
for (var i = 0; i < chains.length; i++) {
|
|
181
|
+
shadows.push(this.shadowChain(chains[i]));
|
|
182
|
+
dirty.push(this[updateFunc](chains[i]));
|
|
183
|
+
}
|
|
184
|
+
if (updateFunc == "applyChain")
|
|
185
|
+
this.notifyDirty(dirty);
|
|
186
|
+
return shadows;
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// Notify the editor that some nodes have changed.
|
|
190
|
+
notifyDirty: function(nodes) {
|
|
191
|
+
forEach(nodes, method(this.editor, "addDirtyNode"))
|
|
192
|
+
this.editor.scheduleHighlight();
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
notifyEnvironment: function() {
|
|
196
|
+
if (this.onChange) this.onChange(this.editor);
|
|
197
|
+
// Used by the line-wrapping line-numbering code.
|
|
198
|
+
if (window.frameElement && window.frameElement.CodeMirror.updateNumbers)
|
|
199
|
+
window.frameElement.CodeMirror.updateNumbers();
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
// Link a chain into the DOM nodes (or the first/last links for null
|
|
203
|
+
// nodes).
|
|
204
|
+
linkChain: function(chain) {
|
|
205
|
+
for (var i = 0; i < chain.length; i++) {
|
|
206
|
+
var line = chain[i];
|
|
207
|
+
if (line.from) line.from.historyAfter = line;
|
|
208
|
+
else this.first = line;
|
|
209
|
+
if (line.to) line.to.historyBefore = line;
|
|
210
|
+
else this.last = line;
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
// Get the line object after/before a given node.
|
|
215
|
+
after: function(node) {
|
|
216
|
+
return node ? node.historyAfter : this.first;
|
|
217
|
+
},
|
|
218
|
+
before: function(node) {
|
|
219
|
+
return node ? node.historyBefore : this.last;
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
// Mark a node as touched if it has not already been marked.
|
|
223
|
+
setTouched: function(node) {
|
|
224
|
+
if (node) {
|
|
225
|
+
if (!node.historyTouched) {
|
|
226
|
+
this.touched.push(node);
|
|
227
|
+
node.historyTouched = true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
this.firstTouched = true;
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
// Store a new set of undo info, throw away info if there is more of
|
|
236
|
+
// it than allowed.
|
|
237
|
+
addUndoLevel: function(diffs) {
|
|
238
|
+
this.history.push(diffs);
|
|
239
|
+
if (this.history.length > this.maxDepth) {
|
|
240
|
+
this.history.shift();
|
|
241
|
+
lostundo += 1;
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
// Build chains from a set of touched nodes.
|
|
246
|
+
touchedChains: function() {
|
|
247
|
+
var self = this;
|
|
248
|
+
|
|
249
|
+
// The temp system is a crummy hack to speed up determining
|
|
250
|
+
// whether a (currently touched) node has a line object associated
|
|
251
|
+
// with it. nullTemp is used to store the object for the first
|
|
252
|
+
// line, other nodes get it stored in their historyTemp property.
|
|
253
|
+
var nullTemp = null;
|
|
254
|
+
function temp(node) {return node ? node.historyTemp : nullTemp;}
|
|
255
|
+
function setTemp(node, line) {
|
|
256
|
+
if (node) node.historyTemp = line;
|
|
257
|
+
else nullTemp = line;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function buildLine(node) {
|
|
261
|
+
var text = [];
|
|
262
|
+
for (var cur = node ? node.nextSibling : self.container.firstChild;
|
|
263
|
+
cur && (!isBR(cur) || cur.hackBR); cur = cur.nextSibling)
|
|
264
|
+
if (!cur.hackBR && cur.currentText) text.push(cur.currentText);
|
|
265
|
+
return {from: node, to: cur, text: cleanText(text.join(""))};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Filter out unchanged lines and nodes that are no longer in the
|
|
269
|
+
// document. Build up line objects for remaining nodes.
|
|
270
|
+
var lines = [];
|
|
271
|
+
if (self.firstTouched) self.touched.push(null);
|
|
272
|
+
forEach(self.touched, function(node) {
|
|
273
|
+
if (node && (node.parentNode != self.container || node.hackBR)) return;
|
|
274
|
+
|
|
275
|
+
if (node) node.historyTouched = false;
|
|
276
|
+
else self.firstTouched = false;
|
|
277
|
+
|
|
278
|
+
var line = buildLine(node), shadow = self.after(node);
|
|
279
|
+
if (!shadow || shadow.text != line.text || shadow.to != line.to) {
|
|
280
|
+
lines.push(line);
|
|
281
|
+
setTemp(node, line);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Get the BR element after/before the given node.
|
|
286
|
+
function nextBR(node, dir) {
|
|
287
|
+
var link = dir + "Sibling", search = node[link];
|
|
288
|
+
while (search && !isBR(search))
|
|
289
|
+
search = search[link];
|
|
290
|
+
return search;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Assemble line objects into chains by scanning the DOM tree
|
|
294
|
+
// around them.
|
|
295
|
+
var chains = []; self.touched = [];
|
|
296
|
+
forEach(lines, function(line) {
|
|
297
|
+
// Note that this makes the loop skip line objects that have
|
|
298
|
+
// been pulled into chains by lines before them.
|
|
299
|
+
if (!temp(line.from)) return;
|
|
300
|
+
|
|
301
|
+
var chain = [], curNode = line.from, safe = true;
|
|
302
|
+
// Put any line objects (referred to by temp info) before this
|
|
303
|
+
// one on the front of the array.
|
|
304
|
+
while (true) {
|
|
305
|
+
var curLine = temp(curNode);
|
|
306
|
+
if (!curLine) {
|
|
307
|
+
if (safe) break;
|
|
308
|
+
else curLine = buildLine(curNode);
|
|
309
|
+
}
|
|
310
|
+
chain.unshift(curLine);
|
|
311
|
+
setTemp(curNode, null);
|
|
312
|
+
if (!curNode) break;
|
|
313
|
+
safe = self.after(curNode);
|
|
314
|
+
curNode = nextBR(curNode, "previous");
|
|
315
|
+
}
|
|
316
|
+
curNode = line.to; safe = self.before(line.from);
|
|
317
|
+
// Add lines after this one at end of array.
|
|
318
|
+
while (true) {
|
|
319
|
+
if (!curNode) break;
|
|
320
|
+
var curLine = temp(curNode);
|
|
321
|
+
if (!curLine) {
|
|
322
|
+
if (safe) break;
|
|
323
|
+
else curLine = buildLine(curNode);
|
|
324
|
+
}
|
|
325
|
+
chain.push(curLine);
|
|
326
|
+
setTemp(curNode, null);
|
|
327
|
+
safe = self.before(curNode);
|
|
328
|
+
curNode = nextBR(curNode, "next");
|
|
329
|
+
}
|
|
330
|
+
chains.push(chain);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
return chains;
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
// Find the 'shadow' of a given chain by following the links in the
|
|
337
|
+
// DOM nodes at its start and end.
|
|
338
|
+
shadowChain: function(chain) {
|
|
339
|
+
var shadows = [], next = this.after(chain[0].from), end = chain[chain.length - 1].to;
|
|
340
|
+
while (true) {
|
|
341
|
+
shadows.push(next);
|
|
342
|
+
var nextNode = next.to;
|
|
343
|
+
if (!nextNode || nextNode == end)
|
|
344
|
+
break;
|
|
345
|
+
else
|
|
346
|
+
next = nextNode.historyAfter || this.before(end);
|
|
347
|
+
// (The this.before(end) is a hack -- FF sometimes removes
|
|
348
|
+
// properties from BR nodes, in which case the best we can hope
|
|
349
|
+
// for is to not break.)
|
|
350
|
+
}
|
|
351
|
+
return shadows;
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
// Update the DOM tree to contain the lines specified in a given
|
|
355
|
+
// chain, link this chain into the DOM nodes.
|
|
356
|
+
applyChain: function(chain) {
|
|
357
|
+
// Some attempt is made to prevent the cursor from jumping
|
|
358
|
+
// randomly when an undo or redo happens. It still behaves a bit
|
|
359
|
+
// strange sometimes.
|
|
360
|
+
var cursor = select.cursorPos(this.container, false), self = this;
|
|
361
|
+
|
|
362
|
+
// Remove all nodes in the DOM tree between from and to (null for
|
|
363
|
+
// start/end of container).
|
|
364
|
+
function removeRange(from, to) {
|
|
365
|
+
var pos = from ? from.nextSibling : self.container.firstChild;
|
|
366
|
+
while (pos != to) {
|
|
367
|
+
var temp = pos.nextSibling;
|
|
368
|
+
removeElement(pos);
|
|
369
|
+
pos = temp;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
var start = chain[0].from, end = chain[chain.length - 1].to;
|
|
374
|
+
// Clear the space where this change has to be made.
|
|
375
|
+
removeRange(start, end);
|
|
376
|
+
|
|
377
|
+
// Insert the content specified by the chain into the DOM tree.
|
|
378
|
+
for (var i = 0; i < chain.length; i++) {
|
|
379
|
+
var line = chain[i];
|
|
380
|
+
// The start and end of the space are already correct, but BR
|
|
381
|
+
// tags inside it have to be put back.
|
|
382
|
+
if (i > 0)
|
|
383
|
+
self.container.insertBefore(line.from, end);
|
|
384
|
+
|
|
385
|
+
// Add the text.
|
|
386
|
+
var node = makePartSpan(fixSpaces(line.text));
|
|
387
|
+
self.container.insertBefore(node, end);
|
|
388
|
+
// See if the cursor was on this line. Put it back, adjusting
|
|
389
|
+
// for changed line length, if it was.
|
|
390
|
+
if (cursor && cursor.node == line.from) {
|
|
391
|
+
var cursordiff = 0;
|
|
392
|
+
var prev = this.after(line.from);
|
|
393
|
+
if (prev && i == chain.length - 1) {
|
|
394
|
+
// Only adjust if the cursor is after the unchanged part of
|
|
395
|
+
// the line.
|
|
396
|
+
for (var match = 0; match < cursor.offset &&
|
|
397
|
+
line.text.charAt(match) == prev.text.charAt(match); match++){}
|
|
398
|
+
if (cursor.offset > match)
|
|
399
|
+
cursordiff = line.text.length - prev.text.length;
|
|
400
|
+
}
|
|
401
|
+
select.setCursorPos(this.container, {node: line.from, offset: Math.max(0, cursor.offset + cursordiff)});
|
|
402
|
+
}
|
|
403
|
+
// Cursor was in removed line, this is last new line.
|
|
404
|
+
else if (cursor && (i == chain.length - 1) && cursor.node && cursor.node.parentNode != this.container) {
|
|
405
|
+
select.setCursorPos(this.container, {node: line.from, offset: line.text.length});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Anchor the chain in the DOM tree.
|
|
410
|
+
this.linkChain(chain);
|
|
411
|
+
return start;
|
|
412
|
+
}
|
|
413
|
+
};
|