aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/main/js/xtext/xtext-codemirror.js
diff options
context:
space:
mode:
Diffstat (limited to 'language-web/src/main/js/xtext/xtext-codemirror.js')
-rw-r--r--language-web/src/main/js/xtext/xtext-codemirror.js472
1 files changed, 472 insertions, 0 deletions
diff --git a/language-web/src/main/js/xtext/xtext-codemirror.js b/language-web/src/main/js/xtext/xtext-codemirror.js
new file mode 100644
index 00000000..4d50718c
--- /dev/null
+++ b/language-web/src/main/js/xtext/xtext-codemirror.js
@@ -0,0 +1,472 @@
1/*******************************************************************************
2 * Copyright (c) 2015, 2017 itemis AG (http://www.itemis.eu) and others.
3 * This program and the accompanying materials are made available under the
4 * terms of the Eclipse Public License 2.0 which is available at
5 * http://www.eclipse.org/legal/epl-2.0.
6 *
7 * SPDX-License-Identifier: EPL-2.0
8 *******************************************************************************/
9
10/*
11 * Use `createEditor(options)` to create an Xtext editor. You can specify options either
12 * through the function parameter or through `data-editor-x` attributes, where x is an
13 * option name with camelCase converted to hyphen-separated.
14 * In addition to the options supported by CodeMirror (https://codemirror.net/doc/manual.html#config),
15 * the following options are available:
16 *
17 * baseUrl = "/" {String}
18 * The path segment where the Xtext service is found; see serviceUrl option.
19 * contentType {String}
20 * The content type included in requests to the Xtext server.
21 * dirtyElement {String | DOMElement}
22 * An element into which the dirty status class is written when the editor is marked dirty;
23 * it can be either a DOM element or an ID for a DOM element.
24 * dirtyStatusClass = 'dirty' {String}
25 * A CSS class name written into the dirtyElement when the editor is marked dirty.
26 * document {Document}
27 * The document; if not specified, the global document is used.
28 * enableContentAssistService = true {Boolean}
29 * Whether content assist should be enabled.
30 * enableCors = true {Boolean}
31 * Whether CORS should be enabled for service request.
32 * enableFormattingAction = false {Boolean}
33 * Whether the formatting action should be bound to the standard keystroke ctrl+shift+s / cmd+shift+f.
34 * enableFormattingService = true {Boolean}
35 * Whether text formatting should be enabled.
36 * enableGeneratorService = true {Boolean}
37 * Whether code generation should be enabled (must be triggered through JavaScript code).
38 * enableHighlightingService = true {Boolean}
39 * Whether semantic highlighting (computed on the server) should be enabled.
40 * enableOccurrencesService = true {Boolean}
41 * Whether marking occurrences should be enabled.
42 * enableSaveAction = false {Boolean}
43 * Whether the save action should be bound to the standard keystroke ctrl+s / cmd+s.
44 * enableValidationService = true {Boolean}
45 * Whether validation should be enabled.
46 * loadFromServer = true {Boolean}
47 * Whether to load the editor content from the server.
48 * mode {String}
49 * The name of the syntax highlighting mode to use; the mode has to be registered externally
50 * (see CodeMirror documentation).
51 * parent = 'xtext-editor' {String | DOMElement}
52 * The parent element for the view; it can be either a DOM element or an ID for a DOM element.
53 * parentClass = 'xtext-editor' {String}
54 * If the 'parent' option is not given, this option is used to find elements that match the given class name.
55 * resourceId {String}
56 * The identifier of the resource displayed in the text editor; this option is sent to the server to
57 * communicate required information on the respective resource.
58 * selectionUpdateDelay = 550 {Number}
59 * The number of milliseconds to wait after a selection change before Xtext services are invoked.
60 * sendFullText = false {Boolean}
61 * Whether the full text shall be sent to the server with each request; use this if you want
62 * the server to run in stateless mode. If the option is inactive, the server state is updated regularly.
63 * serviceUrl {String}
64 * The URL of the Xtext servlet; if no value is given, it is constructed using the baseUrl option in the form
65 * {location.protocol}//{location.host}{baseUrl}xtext-service
66 * showErrorDialogs = false {Boolean}
67 * Whether errors should be displayed in popup dialogs.
68 * syntaxDefinition {String}
69 * If the 'mode' option is not set, the default mode 'xtext/{xtextLang}' is used. Set this option to
70 * 'none' to suppress this behavior and disable syntax highlighting.
71 * textUpdateDelay = 500 {Number}
72 * The number of milliseconds to wait after a text change before Xtext services are invoked.
73 * xtextLang {String}
74 * The language name (usually the file extension configured for the language).
75 */
76define([
77 'jquery',
78 'codemirror',
79 'codemirror/addon/hint/show-hint',
80 'xtext/compatibility',
81 'xtext/ServiceBuilder',
82 'xtext/CodeMirrorEditorContext',
83 'codemirror/mode/javascript/javascript'
84], function(jQuery, CodeMirror, ShowHint, compatibility, ServiceBuilder, EditorContext) {
85
86 var exports = {};
87
88 /**
89 * Create one or more Xtext editor instances configured with the given options.
90 * The return value is either a CodeMirror editor or an array of CodeMirror editors.
91 */
92 exports.createEditor = function(options) {
93 if (!options)
94 options = {};
95
96 var query;
97 if (jQuery.type(options.parent) === 'string') {
98 query = jQuery('#' + options.parent, options.document);
99 } else if (options.parent) {
100 query = jQuery(options.parent);
101 } else if (jQuery.type(options.parentClass) === 'string') {
102 query = jQuery('.' + options.parentClass, options.document);
103 } else {
104 query = jQuery('#xtext-editor', options.document);
105 if (query.length == 0)
106 query = jQuery('.xtext-editor', options.document);
107 }
108
109 var editors = [];
110 query.each(function(index, parent) {
111 var editorOptions = ServiceBuilder.mergeParentOptions(parent, options);
112 if (!editorOptions.value)
113 editorOptions.value = jQuery(parent).text();
114 var editor = CodeMirror(function(element) {
115 jQuery(parent).empty().append(element);
116 }, editorOptions);
117
118 exports.createServices(editor, editorOptions);
119 editors[index] = editor;
120 });
121
122 if (editors.length == 1)
123 return editors[0];
124 else
125 return editors;
126 }
127
128 function CodeMirrorServiceBuilder(editor, xtextServices) {
129 this.editor = editor;
130 xtextServices.editorContext._highlightingMarkers = [];
131 xtextServices.editorContext._validationMarkers = [];
132 xtextServices.editorContext._occurrenceMarkers = [];
133 ServiceBuilder.call(this, xtextServices);
134 }
135 CodeMirrorServiceBuilder.prototype = new ServiceBuilder();
136
137 /**
138 * Configure Xtext services for the given editor. The editor does not have to be created
139 * with createEditor(options).
140 */
141 exports.createServices = function(editor, options) {
142 if (options.enableValidationService || options.enableValidationService === undefined) {
143 editor.setOption('gutters', ['annotations-gutter']);
144 }
145 var xtextServices = {
146 options: options,
147 editorContext: new EditorContext(editor)
148 };
149 var serviceBuilder = new CodeMirrorServiceBuilder(editor, xtextServices);
150 serviceBuilder.createServices();
151 xtextServices.serviceBuilder = serviceBuilder;
152 editor.xtextServices = xtextServices;
153 return xtextServices;
154 }
155
156 /**
157 * Remove all services and listeners that have been previously created with createServices(editor, options).
158 */
159 exports.removeServices = function(editor) {
160 if (!editor.xtextServices)
161 return;
162 var services = editor.xtextServices;
163 if (services.modelChangeListener)
164 editor.off('changes', services.modelChangeListener);
165 if (services.cursorActivityListener)
166 editor.off('cursorActivity', services.cursorActivityListener);
167 if (services.saveKeyMap)
168 editor.removeKeyMap(services.saveKeyMap);
169 if (services.contentAssistKeyMap)
170 editor.removeKeyMap(services.contentAssistKeyMap);
171 if (services.formatKeyMap)
172 editor.removeKeyMap(services.formatKeyMap);
173 var editorContext = services.editorContext;
174 var highlightingMarkers = editorContext._highlightingMarkers;
175 if (highlightingMarkers) {
176 for (var i = 0; i < highlightingMarkers.length; i++) {
177 highlightingMarkers[i].clear();
178 }
179 }
180 if (editorContext._validationAnnotations)
181 services.serviceBuilder._clearAnnotations(editorContext._validationAnnotations);
182 var validationMarkers = editorContext._validationMarkers;
183 if (validationMarkers) {
184 for (var i = 0; i < validationMarkers.length; i++) {
185 validationMarkers[i].clear();
186 }
187 }
188 var occurrenceMarkers = editorContext._occurrenceMarkers;
189 if (occurrenceMarkers) {
190 for (var i = 0; i < occurrenceMarkers.length; i++)  {
191 occurrenceMarkers[i].clear();
192 }
193 }
194 delete editor.xtextServices;
195 }
196
197 /**
198 * Syntax highlighting (without semantic highlighting).
199 */
200 CodeMirrorServiceBuilder.prototype.setupSyntaxHighlighting = function() {
201 var options = this.services.options;
202 // If the mode option is set, syntax highlighting has already been configured by CM
203 if (!options.mode && options.syntaxDefinition != 'none' && options.xtextLang) {
204 this.editor.setOption('mode', 'xtext/' + options.xtextLang);
205 }
206 }
207
208 /**
209 * Document update service.
210 */
211 CodeMirrorServiceBuilder.prototype.setupUpdateService = function(refreshDocument) {
212 var services = this.services;
213 var editorContext = services.editorContext;
214 var textUpdateDelay = services.options.textUpdateDelay;
215 if (!textUpdateDelay)
216 textUpdateDelay = 500;
217 services.modelChangeListener = function(event) {
218 if (!event._xtext_init)
219 editorContext.setDirty(true);
220 if (editorContext._modelChangeTimeout)
221 clearTimeout(editorContext._modelChangeTimeout);
222 editorContext._modelChangeTimeout = setTimeout(function() {
223 if (services.options.sendFullText)
224 refreshDocument();
225 else
226 services.update();
227 }, textUpdateDelay);
228 }
229 if (!services.options.resourceId || !services.options.loadFromServer)
230 services.modelChangeListener({_xtext_init: true});
231 this.editor.on('changes', services.modelChangeListener);
232 }
233
234 /**
235 * Persistence services: load, save, and revert.
236 */
237 CodeMirrorServiceBuilder.prototype.setupPersistenceServices = function() {
238 var services = this.services;
239 if (services.options.enableSaveAction) {
240 var userAgent = navigator.userAgent.toLowerCase();
241 var saveFunction = function(editor) {
242 services.saveResource();
243 };
244 services.saveKeyMap = /mac os/.test(userAgent) ? {'Cmd-S': saveFunction}: {'Ctrl-S': saveFunction};
245 this.editor.addKeyMap(services.saveKeyMap);
246 }
247 }
248
249 /**
250 * Content assist service.
251 */
252 CodeMirrorServiceBuilder.prototype.setupContentAssistService = function() {
253 var services = this.services;
254 var editorContext = services.editorContext;
255 services.contentAssistKeyMap = {'Ctrl-Space': function(editor) {
256 var params = ServiceBuilder.copy(services.options);
257 var cursor = editor.getCursor();
258 params.offset = editor.indexFromPos(cursor);
259 services.contentAssistService.invoke(editorContext, params).done(function(entries) {
260 editor.showHint({hint: function(editor, options) {
261 return {
262 list: entries.map(function(entry) {
263 var displayText;
264 if (entry.label)
265 displayText = entry.label;
266 else
267 displayText = entry.proposal;
268 if (entry.description)
269 displayText += ' (' + entry.description + ')';
270 var prefixLength = 0
271 if (entry.prefix)
272 prefixLength = entry.prefix.length
273 return {
274 text: entry.proposal,
275 displayText: displayText,
276 from: {
277 line: cursor.line,
278 ch: cursor.ch - prefixLength
279 }
280 };
281 }),
282 to: cursor
283 };
284 }});
285 });
286 }};
287 this.editor.addKeyMap(services.contentAssistKeyMap);
288 }
289
290 /**
291 * Semantic highlighting service.
292 */
293 CodeMirrorServiceBuilder.prototype.doHighlighting = function() {
294 var services = this.services;
295 var editorContext = services.editorContext;
296 var editor = this.editor;
297 services.computeHighlighting().always(function() {
298 var highlightingMarkers = editorContext._highlightingMarkers;
299 if (highlightingMarkers) {
300 for (var i = 0; i < highlightingMarkers.length; i++) {
301 highlightingMarkers[i].clear();
302 }
303 }
304 editorContext._highlightingMarkers = [];
305 }).done(function(result) {
306 for (var i = 0; i < result.regions.length; ++i) {
307 var region = result.regions[i];
308 var from = editor.posFromIndex(region.offset);
309 var to = editor.posFromIndex(region.offset + region.length);
310 region.styleClasses.forEach(function(styleClass) {
311 var marker = editor.markText(from, to, {className: styleClass});
312 editorContext._highlightingMarkers.push(marker);
313 });
314 }
315 });
316 }
317
318 var annotationWeight = {
319 error: 30,
320 warning: 20,
321 info: 10
322 };
323 CodeMirrorServiceBuilder.prototype._getAnnotationWeight = function(annotation) {
324 if (annotationWeight[annotation] !== undefined)
325 return annotationWeight[annotation];
326 else
327 return 0;
328 }
329
330 CodeMirrorServiceBuilder.prototype._clearAnnotations = function(annotations) {
331 var editor = this.editor;
332 for (var i = 0; i < annotations.length; i++) {
333 var annotation = annotations[i];
334 if (annotation) {
335 editor.setGutterMarker(i, 'annotations-gutter', null);
336 annotations[i] = undefined;
337 }
338 }
339 }
340
341 CodeMirrorServiceBuilder.prototype._refreshAnnotations = function(annotations) {
342 var editor = this.editor;
343 for (var i = 0; i < annotations.length; i++) {
344 var annotation = annotations[i];
345 if (annotation) {
346 var classProp = ' class="xtext-annotation_' + annotation.type + '"';
347 var titleProp = annotation.description ? ' title="' + annotation.description.replace(/"/g, '&quot;') + '"' : '';
348 var element = jQuery('<div' + classProp + titleProp + '></div>').get(0);
349 editor.setGutterMarker(i, 'annotations-gutter', element);
350 }
351 }
352 }
353
354 /**
355 * Validation service.
356 */
357 CodeMirrorServiceBuilder.prototype.doValidation = function() {
358 var services = this.services;
359 var editorContext = services.editorContext;
360 var editor = this.editor;
361 var self = this;
362 services.validate().always(function() {
363 if (editorContext._validationAnnotations)
364 self._clearAnnotations(editorContext._validationAnnotations);
365 else
366 editorContext._validationAnnotations = [];
367 var validationMarkers = editorContext._validationMarkers;
368 if (validationMarkers) {
369 for (var i = 0; i < validationMarkers.length; i++) {
370 validationMarkers[i].clear();
371 }
372 }
373 editorContext._validationMarkers = [];
374 }).done(function(result) {
375 var validationAnnotations = editorContext._validationAnnotations;
376 for (var i = 0; i < result.issues.length; i++) {
377 var entry = result.issues[i];
378 var annotation = validationAnnotations[entry.line - 1];
379 var weight = self._getAnnotationWeight(entry.severity);
380 if (annotation) {
381 if (annotation.weight < weight) {
382 annotation.type = entry.severity;
383 annotation.weight = weight;
384 }
385 if (annotation.description)
386 annotation.description += '\n' + entry.description;
387 else
388 annotation.description = entry.description;
389 } else {
390 validationAnnotations[entry.line - 1] = {
391 type: entry.severity,
392 weight: weight,
393 description: entry.description
394 };
395 }
396 var from = editor.posFromIndex(entry.offset);
397 var to = editor.posFromIndex(entry.offset + entry.length);
398 var marker = editor.markText(from, to, {
399 className: 'xtext-marker_' + entry.severity,
400 title: entry.description
401 });
402 editorContext._validationMarkers.push(marker);
403 }
404 self._refreshAnnotations(validationAnnotations);
405 });
406 }
407
408 /**
409 * Occurrences service.
410 */
411 CodeMirrorServiceBuilder.prototype.setupOccurrencesService = function() {
412 var services = this.services;
413 var editorContext = services.editorContext;
414 var selectionUpdateDelay = services.options.selectionUpdateDelay;
415 if (!selectionUpdateDelay)
416 selectionUpdateDelay = 550;
417 var editor = this.editor;
418 var self = this;
419 services.cursorActivityListener = function() {
420 if (editorContext._selectionChangeTimeout) {
421 clearTimeout(editorContext._selectionChangeTimeout);
422 }
423 editorContext._selectionChangeTimeout = setTimeout(function() {
424 var params = ServiceBuilder.copy(services.options);
425 var cursor = editor.getCursor();
426 params.offset = editor.indexFromPos(cursor);
427 services.occurrencesService.invoke(editorContext, params).always(function() {
428 var occurrenceMarkers = editorContext._occurrenceMarkers;
429 if (occurrenceMarkers) {
430 for (var i = 0; i < occurrenceMarkers.length; i++)  {
431 occurrenceMarkers[i].clear();
432 }
433 }
434 editorContext._occurrenceMarkers = [];
435 }).done(function(occurrencesResult) {
436 for (var i = 0; i < occurrencesResult.readRegions.length; i++) {
437 var region = occurrencesResult.readRegions[i];
438 var from = editor.posFromIndex(region.offset);
439 var to = editor.posFromIndex(region.offset + region.length);
440 var marker = editor.markText(from, to, {className: 'xtext-marker_read'});
441 editorContext._occurrenceMarkers.push(marker);
442 }
443 for (var i = 0; i < occurrencesResult.writeRegions.length; i++) {
444 var region = occurrencesResult.writeRegions[i];
445 var from = editor.posFromIndex(region.offset);
446 var to = editor.posFromIndex(region.offset + region.length);
447 var marker = editor.markText(from, to, {className: 'xtext-marker_write'});
448 editorContext._occurrenceMarkers.push(marker);
449 }
450 });
451 }, selectionUpdateDelay);
452 }
453 editor.on('cursorActivity', services.cursorActivityListener);
454 }
455
456 /**
457 * Formatting service.
458 */
459 CodeMirrorServiceBuilder.prototype.setupFormattingService = function() {
460 var services = this.services;
461 if (services.options.enableFormattingAction) {
462 var userAgent = navigator.userAgent.toLowerCase();
463 var formatFunction = function(editor) {
464 services.format();
465 };
466 services.formatKeyMap = /mac os/.test(userAgent) ? {'Shift-Cmd-F': formatFunction}: {'Shift-Ctrl-S': formatFunction};
467 this.editor.addKeyMap(services.formatKeyMap);
468 }
469 }
470
471 return exports;
472});