aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/main/js/xtext
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-10-30 13:48:52 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-10-31 19:26:13 +0100
commitcdb493b0a47bcf64e8e670b94fa399fcd731f531 (patch)
treeb6b03aec77ef87a2dda7585be7884a30c65d93f5 /language-web/src/main/js/xtext
parentfeat(web): add xtext content assist (diff)
downloadrefinery-cdb493b0a47bcf64e8e670b94fa399fcd731f531.tar.gz
refinery-cdb493b0a47bcf64e8e670b94fa399fcd731f531.tar.zst
refinery-cdb493b0a47bcf64e8e670b94fa399fcd731f531.zip
chore(web): refactor xtext client
Diffstat (limited to 'language-web/src/main/js/xtext')
-rw-r--r--language-web/src/main/js/xtext/CodeMirrorEditorContext.js111
-rw-r--r--language-web/src/main/js/xtext/ContentAssistService.ts133
-rw-r--r--language-web/src/main/js/xtext/ServiceBuilder.js285
-rw-r--r--language-web/src/main/js/xtext/UpdateService.ts271
-rw-r--r--language-web/src/main/js/xtext/ValidationService.ts40
-rw-r--r--language-web/src/main/js/xtext/XtextClient.ts73
-rw-r--r--language-web/src/main/js/xtext/XtextWebSocketClient.ts345
-rw-r--r--language-web/src/main/js/xtext/compatibility.js63
-rw-r--r--language-web/src/main/js/xtext/services/ContentAssistService.js132
-rw-r--r--language-web/src/main/js/xtext/services/FormattingService.js52
-rw-r--r--language-web/src/main/js/xtext/services/HighlightingService.js33
-rw-r--r--language-web/src/main/js/xtext/services/HoverService.js59
-rw-r--r--language-web/src/main/js/xtext/services/LoadResourceService.js42
-rw-r--r--language-web/src/main/js/xtext/services/OccurrencesService.js39
-rw-r--r--language-web/src/main/js/xtext/services/SaveResourceService.js32
-rw-r--r--language-web/src/main/js/xtext/services/UpdateService.js159
-rw-r--r--language-web/src/main/js/xtext/services/ValidationService.js33
-rw-r--r--language-web/src/main/js/xtext/services/XtextService.js280
-rw-r--r--language-web/src/main/js/xtext/xtext-codemirror.d.ts43
-rw-r--r--language-web/src/main/js/xtext/xtext-codemirror.js473
-rw-r--r--language-web/src/main/js/xtext/xtextMessages.ts62
-rw-r--r--language-web/src/main/js/xtext/xtextServiceResults.ts200
22 files changed, 1124 insertions, 1836 deletions
diff --git a/language-web/src/main/js/xtext/CodeMirrorEditorContext.js b/language-web/src/main/js/xtext/CodeMirrorEditorContext.js
deleted file mode 100644
index b829c680..00000000
--- a/language-web/src/main/js/xtext/CodeMirrorEditorContext.js
+++ /dev/null
@@ -1,111 +0,0 @@
1/*******************************************************************************
2 * Copyright (c) 2015 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
10define([], function() {
11
12 /**
13 * An editor context mediates between the Xtext services and the CodeMirror editor framework.
14 */
15 function CodeMirrorEditorContext(editor) {
16 this._editor = editor;
17 this._serverState = {};
18 this._serverStateListeners = [];
19 this._dirty = false;
20 this._dirtyStateListeners = [];
21 };
22
23 CodeMirrorEditorContext.prototype = {
24
25 getServerState: function() {
26 return this._serverState;
27 },
28
29 updateServerState: function(currentText, currentStateId) {
30 this._serverState.text = currentText;
31 this._serverState.stateId = currentStateId;
32 return this._serverStateListeners;
33 },
34
35 addServerStateListener: function(listener) {
36 this._serverStateListeners.push(listener);
37 },
38
39 getCaretOffset: function() {
40 var editor = this._editor;
41 return editor.indexFromPos(editor.getCursor());
42 },
43
44 getLineStart: function(lineNumber) {
45 var editor = this._editor;
46 return editor.indexFromPos({line: lineNumber, ch: 0});
47 },
48
49 getSelection: function() {
50 var editor = this._editor;
51 return {
52 start: editor.indexFromPos(editor.getCursor('from')),
53 end: editor.indexFromPos(editor.getCursor('to'))
54 };
55 },
56
57 getText: function(start, end) {
58 var editor = this._editor;
59 if (start && end) {
60 return editor.getRange(editor.posFromIndex(start), editor.posFromIndex(end));
61 } else {
62 return editor.getValue();
63 }
64 },
65
66 isDirty: function() {
67 return !this._clean;
68 },
69
70 setDirty: function(dirty) {
71 if (dirty != this._dirty) {
72 for (var i = 0; i < this._dirtyStateListeners.length; i++) {
73 this._dirtyStateListeners[i](dirty);
74 }
75 }
76 this._dirty = dirty;
77 },
78
79 addDirtyStateListener: function(listener) {
80 this._dirtyStateListeners.push(listener);
81 },
82
83 clearUndoStack: function() {
84 this._editor.clearHistory();
85 },
86
87 setCaretOffset: function(offset) {
88 var editor = this._editor;
89 editor.setCursor(editor.posFromIndex(offset));
90 },
91
92 setSelection: function(selection) {
93 var editor = this._editor;
94 editor.setSelection(editor.posFromIndex(selection.start), editor.posFromIndex(selection.end));
95 },
96
97 setText: function(text, start, end) {
98 var editor = this._editor;
99 if (!start)
100 start = 0;
101 if (!end)
102 end = editor.getValue().length;
103 var cursor = editor.getCursor();
104 editor.replaceRange(text, editor.posFromIndex(start), editor.posFromIndex(end));
105 editor.setCursor(cursor);
106 }
107
108 };
109
110 return CodeMirrorEditorContext;
111}); \ No newline at end of file
diff --git a/language-web/src/main/js/xtext/ContentAssistService.ts b/language-web/src/main/js/xtext/ContentAssistService.ts
new file mode 100644
index 00000000..91789864
--- /dev/null
+++ b/language-web/src/main/js/xtext/ContentAssistService.ts
@@ -0,0 +1,133 @@
1import type {
2 Completion,
3 CompletionContext,
4 CompletionResult,
5} from '@codemirror/autocomplete';
6import type { ChangeSet, Transaction } from '@codemirror/state';
7
8import { getLogger } from '../logging';
9import type { UpdateService } from './UpdateService';
10
11const log = getLogger('xtext.ContentAssistService');
12
13export class ContentAssistService {
14 updateService: UpdateService;
15
16 lastCompletion: CompletionResult | null = null;
17
18 constructor(updateService: UpdateService) {
19 this.updateService = updateService;
20 }
21
22 onTransaction(transaction: Transaction): void {
23 if (this.shouldInvalidateCachedCompletion(transaction.changes)) {
24 this.lastCompletion = null;
25 }
26 }
27
28 async contentAssist(context: CompletionContext): Promise<CompletionResult> {
29 const tokenBefore = context.tokenBefore(['QualifiedName']);
30 let range: { from: number, to: number };
31 let selection: { selectionStart?: number, selectionEnd?: number };
32 if (tokenBefore === null) {
33 if (!context.explicit) {
34 return {
35 from: context.pos,
36 options: [],
37 };
38 }
39 range = {
40 from: context.pos,
41 to: context.pos,
42 };
43 selection = {};
44 } else {
45 range = {
46 from: tokenBefore.from,
47 to: tokenBefore.to,
48 };
49 selection = {
50 selectionStart: tokenBefore.from,
51 selectionEnd: tokenBefore.to,
52 };
53 }
54 if (!context.explicit && this.shouldReturnCachedCompletion(tokenBefore)) {
55 log.trace('Returning cached completion result');
56 // Postcondition of `shouldReturnCachedCompletion`: `lastCompletion !== null`
57 return {
58 ...this.lastCompletion as CompletionResult,
59 ...range,
60 };
61 }
62 this.lastCompletion = null;
63 const entries = await this.updateService.fetchContentAssist({
64 resource: this.updateService.resourceName,
65 serviceType: 'assist',
66 caretOffset: context.pos,
67 ...selection,
68 }, context);
69 if (context.aborted) {
70 return {
71 ...range,
72 options: [],
73 };
74 }
75 const options: Completion[] = [];
76 entries.forEach((entry) => {
77 options.push({
78 label: entry.proposal,
79 detail: entry.description,
80 info: entry.documentation,
81 type: entry.kind?.toLowerCase(),
82 boost: entry.kind === 'KEYWORD' ? -90 : 0,
83 });
84 });
85 log.debug('Fetched', options.length, 'completions from server');
86 this.lastCompletion = {
87 ...range,
88 options,
89 span: /^[a-zA-Z0-9_:]*$/,
90 };
91 return this.lastCompletion;
92 }
93
94 private shouldReturnCachedCompletion(
95 token: { from: number, to: number, text: string } | null,
96 ) {
97 if (token === null || this.lastCompletion === null) {
98 return false;
99 }
100 const { from, to, text } = token;
101 const { from: lastFrom, to: lastTo, span } = this.lastCompletion;
102 if (!lastTo) {
103 return true;
104 }
105 const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo);
106 return from >= transformedFrom && to <= transformedTo && span && span.exec(text);
107 }
108
109 private shouldInvalidateCachedCompletion(changes: ChangeSet) {
110 if (changes.empty || this.lastCompletion === null) {
111 return false;
112 }
113 const { from: lastFrom, to: lastTo } = this.lastCompletion;
114 if (!lastTo) {
115 return true;
116 }
117 const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo);
118 let invalidate = false;
119 changes.iterChangedRanges((fromA, toA) => {
120 if (fromA < transformedFrom || toA > transformedTo) {
121 invalidate = true;
122 }
123 });
124 return invalidate;
125 }
126
127 private mapRangeInclusive(lastFrom: number, lastTo: number): [number, number] {
128 const changes = this.updateService.computeChangesSinceLastUpdate();
129 const transformedFrom = changes.mapPos(lastFrom);
130 const transformedTo = changes.mapPos(lastTo, 1);
131 return [transformedFrom, transformedTo];
132 }
133}
diff --git a/language-web/src/main/js/xtext/ServiceBuilder.js b/language-web/src/main/js/xtext/ServiceBuilder.js
deleted file mode 100644
index 57fcb310..00000000
--- a/language-web/src/main/js/xtext/ServiceBuilder.js
+++ /dev/null
@@ -1,285 +0,0 @@
1/*******************************************************************************
2 * Copyright (c) 2015 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
10define([
11 'jquery',
12 'xtext/services/XtextService',
13 'xtext/services/LoadResourceService',
14 'xtext/services/SaveResourceService',
15 'xtext/services/HighlightingService',
16 'xtext/services/ValidationService',
17 'xtext/services/UpdateService',
18 'xtext/services/ContentAssistService',
19 'xtext/services/HoverService',
20 'xtext/services/OccurrencesService',
21 'xtext/services/FormattingService',
22 '../logging',
23], function(jQuery, XtextService, LoadResourceService, SaveResourceService, HighlightingService,
24 ValidationService, UpdateService, ContentAssistService, HoverService, OccurrencesService,
25 FormattingService, logging) {
26
27 /**
28 * Builder class for the Xtext services.
29 */
30 function ServiceBuilder(xtextServices) {
31 this.services = xtextServices;
32 };
33
34 /**
35 * Create all the available Xtext services depending on the configuration.
36 */
37 ServiceBuilder.prototype.createServices = function() {
38 var services = this.services;
39 var options = services.options;
40 var editorContext = services.editorContext;
41 editorContext.xtextServices = services;
42 var self = this;
43 if (!options.serviceUrl) {
44 if (!options.baseUrl)
45 options.baseUrl = '/';
46 else if (options.baseUrl.charAt(0) != '/')
47 options.baseUrl = '/' + options.baseUrl;
48 options.serviceUrl = window.location.protocol + '//' + window.location.host + options.baseUrl + 'xtext-service';
49 }
50 if (options.resourceId) {
51 if (!options.xtextLang)
52 options.xtextLang = options.resourceId.split(/[?#]/)[0].split('.').pop();
53 if (options.loadFromServer === undefined)
54 options.loadFromServer = true;
55 if (options.loadFromServer && this.setupPersistenceServices) {
56 services.loadResourceService = new LoadResourceService(options.serviceUrl, options.resourceId, false);
57 services.loadResource = function(addParams) {
58 return services.loadResourceService.invoke(editorContext, ServiceBuilder.mergeOptions(addParams, options));
59 }
60 services.saveResourceService = new SaveResourceService(options.serviceUrl, options.resourceId);
61 services.saveResource = function(addParams) {
62 return services.saveResourceService.invoke(editorContext, ServiceBuilder.mergeOptions(addParams, options));
63 }
64 services.revertResourceService = new LoadResourceService(options.serviceUrl, options.resourceId, true);
65 services.revertResource = function(addParams) {
66 return services.revertResourceService.invoke(editorContext, ServiceBuilder.mergeOptions(addParams, options));
67 }
68 this.setupPersistenceServices();
69 services.loadResource();
70 }
71 } else {
72 if (options.loadFromServer === undefined)
73 options.loadFromServer = false;
74 if (options.xtextLang) {
75 var randomId = Math.floor(Math.random() * 2147483648).toString(16);
76 options.resourceId = randomId + '.' + options.xtextLang;
77 }
78 }
79
80 if (this.setupSyntaxHighlighting) {
81 this.setupSyntaxHighlighting();
82 }
83 if (options.enableHighlightingService || options.enableHighlightingService === undefined) {
84 services.highlightingService = new HighlightingService(options.serviceUrl, options.resourceId);
85 services.computeHighlighting = function(addParams) {
86 return services.highlightingService.invoke(editorContext, ServiceBuilder.mergeOptions(addParams, options));
87 }
88 }
89 if (options.enableValidationService || options.enableValidationService === undefined) {
90 services.validationService = new ValidationService(options.serviceUrl, options.resourceId);
91 services.validate = function(addParams) {
92 return services.validationService.invoke(editorContext, ServiceBuilder.mergeOptions(addParams, options));
93 }
94 }
95 if (this.setupUpdateService) {
96 function refreshDocument() {
97 if (services.highlightingService && self.doHighlighting) {
98 services.highlightingService.setState(undefined);
99 self.doHighlighting();
100 }
101 if (services.validationService && self.doValidation) {
102 services.validationService.setState(undefined);
103 self.doValidation();
104 }
105 }
106 if (!options.sendFullText) {
107 services.updateService = new UpdateService(options.serviceUrl, options.resourceId);
108 services.update = function(addParams) {
109 return services.updateService.invoke(editorContext, ServiceBuilder.mergeOptions(addParams, options));
110 }
111 if (services.saveResourceService)
112 services.saveResourceService._updateService = services.updateService;
113 editorContext.addServerStateListener(refreshDocument);
114 }
115 this.setupUpdateService(refreshDocument);
116 }
117 if ((options.enableContentAssistService || options.enableContentAssistService === undefined)
118 && this.setupContentAssistService) {
119 services.contentAssistService = new ContentAssistService(options.serviceUrl, options.resourceId, services.updateService);
120 services.getContentAssist = function(addParams) {
121 return services.contentAssistService.invoke(editorContext, ServiceBuilder.mergeOptions(addParams, options));
122 }
123 this.setupContentAssistService();
124 }
125 if ((options.enableHoverService || options.enableHoverService === undefined)
126 && this.setupHoverService) {
127 services.hoverService = new HoverService(options.serviceUrl, options.resourceId, services.updateService);
128 services.getHoverInfo = function(addParams) {
129 return services.hoverService.invoke(editorContext, ServiceBuilder.mergeOptions(addParams, options));
130 }
131 this.setupHoverService();
132 }
133 if ((options.enableOccurrencesService || options.enableOccurrencesService === undefined)
134 && this.setupOccurrencesService) {
135 services.occurrencesService = new OccurrencesService(options.serviceUrl, options.resourceId, services.updateService);
136 services.getOccurrences = function(addParams) {
137 return services.occurrencesService.invoke(editorContext, ServiceBuilder.mergeOptions(addParams, options));
138 }
139 this.setupOccurrencesService();
140 }
141 if ((options.enableFormattingService || options.enableFormattingService === undefined)
142 && this.setupFormattingService) {
143 services.formattingService = new FormattingService(options.serviceUrl, options.resourceId, services.updateService);
144 services.format = function(addParams) {
145 return services.formattingService.invoke(editorContext, ServiceBuilder.mergeOptions(addParams, options));
146 }
147 this.setupFormattingService();
148 }
149 if (options.enableGeneratorService || options.enableGeneratorService === undefined) {
150 services.generatorService = new XtextService();
151 services.generatorService.initialize(services, 'generate');
152 services.generatorService._initServerData = function(serverData, editorContext, params) {
153 if (params.allArtifacts)
154 serverData.allArtifacts = params.allArtifacts;
155 else if (params.artifactId)
156 serverData.artifact = params.artifactId;
157 if (params.includeContent !== undefined)
158 serverData.includeContent = params.includeContent;
159 }
160 services.generate = function(addParams) {
161 return services.generatorService.invoke(editorContext, ServiceBuilder.mergeOptions(addParams, options));
162 }
163 }
164
165 if (options.dirtyElement) {
166 var doc = options.document || document;
167 var dirtyElement;
168 if (typeof(options.dirtyElement) === 'string')
169 dirtyElement = jQuery('#' + options.dirtyElement, doc);
170 else
171 dirtyElement = jQuery(options.dirtyElement);
172 var dirtyStatusClass = options.dirtyStatusClass;
173 if (!dirtyStatusClass)
174 dirtyStatusClass = 'dirty';
175 editorContext.addDirtyStateListener(function(dirty) {
176 if (dirty)
177 dirtyElement.addClass(dirtyStatusClass);
178 else
179 dirtyElement.removeClass(dirtyStatusClass);
180 });
181 }
182
183 const log = logging.getLoggerFromRoot('xtext.XtextService');
184 services.successListeners = [function(serviceType, result) {
185 if (log.getLevel() <= log.levels.TRACE) {
186 log.trace('service', serviceType, 'request success', JSON.parse(JSON.stringify(result)));
187 }
188 }];
189 services.errorListeners = [function(serviceType, severity, message, requestData) {
190 const messageParts = ['service', serviceType, 'failed:', message || '(no message)'];
191 if (requestData) {
192 messageParts.push(JSON.parse(JSON.stringify(requestData)));
193 }
194 if (severity === 'warning') {
195 log.warn(...messageParts);
196 } else {
197 log.error(...messageParts);
198 }
199 }];
200 }
201
202 /**
203 * Change the resource associated with this service builder.
204 */
205 ServiceBuilder.prototype.changeResource = function(resourceId) {
206 var services = this.services;
207 var options = services.options;
208 options.resourceId = resourceId;
209 for (var p in services) {
210 if (services.hasOwnProperty(p)) {
211 var service = services[p];
212 if (service._serviceType && jQuery.isFunction(service.initialize))
213 services[p].initialize(options.serviceUrl, service._serviceType, resourceId, services.updateService);
214 }
215 }
216 var knownServerState = services.editorContext.getServerState();
217 delete knownServerState.stateId;
218 delete knownServerState.text;
219 if (options.loadFromServer && jQuery.isFunction(services.loadResource)) {
220 services.loadResource();
221 }
222 }
223
224 /**
225 * Create a copy of the given object.
226 */
227 ServiceBuilder.copy = function(obj) {
228 var copy = {};
229 for (var p in obj) {
230 if (obj.hasOwnProperty(p))
231 copy[p] = obj[p];
232 }
233 return copy;
234 }
235
236 /**
237 * Translate an HTML attribute name to a JS option name.
238 */
239 ServiceBuilder.optionName = function(name) {
240 var prefix = 'data-editor-';
241 if (name.substring(0, prefix.length) === prefix) {
242 var key = name.substring(prefix.length);
243 key = key.replace(/-([a-z])/ig, function(all, character) {
244 return character.toUpperCase();
245 });
246 return key;
247 }
248 return undefined;
249 }
250
251 /**
252 * Copy all default options into the given set of additional options.
253 */
254 ServiceBuilder.mergeOptions = function(options, defaultOptions) {
255 if (options) {
256 for (var p in defaultOptions) {
257 if (defaultOptions.hasOwnProperty(p))
258 options[p] = defaultOptions[p];
259 }
260 return options;
261 } else {
262 return ServiceBuilder.copy(defaultOptions);
263 }
264 }
265
266 /**
267 * Merge all properties of the given parent element with the given default options.
268 */
269 ServiceBuilder.mergeParentOptions = function(parent, defaultOptions) {
270 var options = ServiceBuilder.copy(defaultOptions);
271 for (var attr, j = 0, attrs = parent.attributes, l = attrs.length; j < l; j++) {
272 attr = attrs.item(j);
273 var key = ServiceBuilder.optionName(attr.nodeName);
274 if (key) {
275 var value = attr.nodeValue;
276 if (value === 'true' || value === 'false')
277 value = value === 'true';
278 options[key] = value;
279 }
280 }
281 return options;
282 }
283
284 return ServiceBuilder;
285});
diff --git a/language-web/src/main/js/xtext/UpdateService.ts b/language-web/src/main/js/xtext/UpdateService.ts
new file mode 100644
index 00000000..f8ab7438
--- /dev/null
+++ b/language-web/src/main/js/xtext/UpdateService.ts
@@ -0,0 +1,271 @@
1import {
2 ChangeDesc,
3 ChangeSet,
4 Transaction,
5} from '@codemirror/state';
6import { nanoid } from 'nanoid';
7
8import type { EditorStore } from '../editor/EditorStore';
9import { getLogger } from '../logging';
10import type { XtextWebSocketClient } from './XtextWebSocketClient';
11import { PendingTask } from '../utils/PendingTask';
12import { Timer } from '../utils/Timer';
13import {
14 IContentAssistEntry,
15 isContentAssistResult,
16 isDocumentStateResult,
17 isInvalidStateIdConflictResult,
18} from './xtextServiceResults';
19
20const UPDATE_TIMEOUT_MS = 500;
21
22const WAIT_FOR_UPDATE_TIMEOUT_MS = 1000;
23
24const log = getLogger('xtext.UpdateService');
25
26export interface IAbortSignal {
27 aborted: boolean;
28}
29
30export class UpdateService {
31 resourceName: string;
32
33 xtextStateId: string | null = null;
34
35 private store: EditorStore;
36
37 private pendingUpdate: ChangeDesc | null = null;
38
39 private dirtyChanges: ChangeDesc;
40
41 private webSocketClient: XtextWebSocketClient;
42
43 private updateListeners: PendingTask<void>[] = [];
44
45 private idleUpdateTimer = new Timer(() => {
46 this.handleIdleUpdate();
47 }, UPDATE_TIMEOUT_MS);
48
49 constructor(store: EditorStore, webSocketClient: XtextWebSocketClient) {
50 this.resourceName = `${nanoid(7)}.problem`;
51 this.store = store;
52 this.dirtyChanges = this.newEmptyChangeDesc();
53 this.webSocketClient = webSocketClient;
54 }
55
56 onTransaction(transaction: Transaction): void {
57 const { changes } = transaction;
58 if (!changes.empty) {
59 this.dirtyChanges = this.dirtyChanges.composeDesc(changes.desc);
60 this.idleUpdateTimer.reschedule();
61 }
62 }
63
64 computeChangesSinceLastUpdate(): ChangeDesc {
65 return this.pendingUpdate?.composeDesc(this.dirtyChanges) || this.dirtyChanges;
66 }
67
68 private handleIdleUpdate() {
69 if (!this.webSocketClient.isOpen || this.dirtyChanges.empty) {
70 return;
71 }
72 if (this.pendingUpdate === null) {
73 this.update().catch((error) => {
74 log.error('Unexpected error during scheduled update', error);
75 });
76 }
77 this.idleUpdateTimer.reschedule();
78 }
79
80 private newEmptyChangeDesc() {
81 const changeSet = ChangeSet.of([], this.store.state.doc.length);
82 return changeSet.desc;
83 }
84
85 async updateFullText(): Promise<void> {
86 await this.withUpdate(() => this.doUpdateFullText());
87 }
88
89 private async doUpdateFullText(): Promise<[string, void]> {
90 const result = await this.webSocketClient.send({
91 resource: this.resourceName,
92 serviceType: 'update',
93 fullText: this.store.state.doc.sliceString(0),
94 });
95 if (isDocumentStateResult(result)) {
96 return [result.stateId, undefined];
97 }
98 log.error('Unexpected full text update result:', result);
99 throw new Error('Full text update failed');
100 }
101
102 async update(): Promise<void> {
103 await this.prepareForDeltaUpdate();
104 const delta = this.computeDelta();
105 if (delta === null) {
106 return;
107 }
108 log.trace('Editor delta', delta);
109 await this.withUpdate(async () => {
110 const result = await this.webSocketClient.send({
111 resource: this.resourceName,
112 serviceType: 'update',
113 requiredStateId: this.xtextStateId,
114 ...delta,
115 });
116 if (isDocumentStateResult(result)) {
117 return [result.stateId, undefined];
118 }
119 if (isInvalidStateIdConflictResult(result)) {
120 return this.doFallbackToUpdateFullText();
121 }
122 log.error('Unexpected delta text update result:', result);
123 throw new Error('Delta text update failed');
124 });
125 }
126
127 private doFallbackToUpdateFullText() {
128 if (this.pendingUpdate === null) {
129 throw new Error('Only a pending update can be extended');
130 }
131 log.warn('Delta update failed, performing full text update');
132 this.xtextStateId = null;
133 this.pendingUpdate = this.pendingUpdate.composeDesc(this.dirtyChanges);
134 this.dirtyChanges = this.newEmptyChangeDesc();
135 return this.doUpdateFullText();
136 }
137
138 async fetchContentAssist(
139 params: Record<string, unknown>,
140 signal: IAbortSignal,
141 ): Promise<IContentAssistEntry[]> {
142 await this.prepareForDeltaUpdate();
143 if (signal.aborted) {
144 return [];
145 }
146 const delta = this.computeDelta();
147 if (delta === null) {
148 // Poscondition of `prepareForDeltaUpdate`: `xtextStateId !== null`
149 return this.doFetchContentAssist(params, this.xtextStateId as string);
150 }
151 log.trace('Editor delta', delta);
152 return this.withUpdate(async () => {
153 const result = await this.webSocketClient.send({
154 ...params,
155 requiredStateId: this.xtextStateId,
156 ...delta,
157 });
158 if (isContentAssistResult(result)) {
159 return [result.stateId, result.entries];
160 }
161 if (isInvalidStateIdConflictResult(result)) {
162 const [newStateId] = await this.doFallbackToUpdateFullText();
163 if (signal.aborted) {
164 return [newStateId, []];
165 }
166 const entries = await this.doFetchContentAssist(params, newStateId);
167 return [newStateId, entries];
168 }
169 log.error('Unextpected content assist result with delta update', result);
170 throw new Error('Unexpexted content assist result with delta update');
171 });
172 }
173
174 private async doFetchContentAssist(params: Record<string, unknown>, expectedStateId: string) {
175 const result = await this.webSocketClient.send({
176 ...params,
177 requiredStateId: expectedStateId,
178 });
179 if (isContentAssistResult(result) && result.stateId === expectedStateId) {
180 return result.entries;
181 }
182 log.error('Unexpected content assist result', result);
183 throw new Error('Unexpected content assist result');
184 }
185
186 private computeDelta() {
187 if (this.dirtyChanges.empty) {
188 return null;
189 }
190 let minFromA = Number.MAX_SAFE_INTEGER;
191 let maxToA = 0;
192 let minFromB = Number.MAX_SAFE_INTEGER;
193 let maxToB = 0;
194 this.dirtyChanges.iterChangedRanges((fromA, toA, fromB, toB) => {
195 minFromA = Math.min(minFromA, fromA);
196 maxToA = Math.max(maxToA, toA);
197 minFromB = Math.min(minFromB, fromB);
198 maxToB = Math.max(maxToB, toB);
199 });
200 return {
201 deltaOffset: minFromA,
202 deltaReplaceLength: maxToA - minFromA,
203 deltaText: this.store.state.doc.sliceString(minFromB, maxToB),
204 };
205 }
206
207 private async withUpdate<T>(callback: () => Promise<[string, T]>): Promise<T> {
208 if (this.pendingUpdate !== null) {
209 throw new Error('Another update is pending, will not perform update');
210 }
211 this.pendingUpdate = this.dirtyChanges;
212 this.dirtyChanges = this.newEmptyChangeDesc();
213 let newStateId: string | null = null;
214 try {
215 let result: T;
216 [newStateId, result] = await callback();
217 this.xtextStateId = newStateId;
218 this.pendingUpdate = null;
219 // Copy `updateListeners` so that we don't get into a race condition
220 // if one of the listeners adds another listener.
221 const listeners = this.updateListeners;
222 this.updateListeners = [];
223 listeners.forEach((listener) => {
224 listener.resolve();
225 });
226 return result;
227 } catch (e) {
228 log.error('Error while update', e);
229 if (this.pendingUpdate === null) {
230 log.error('pendingUpdate was cleared during update');
231 } else {
232 this.dirtyChanges = this.pendingUpdate.composeDesc(this.dirtyChanges);
233 }
234 this.pendingUpdate = null;
235 this.webSocketClient.forceReconnectOnError();
236 const listeners = this.updateListeners;
237 this.updateListeners = [];
238 listeners.forEach((listener) => {
239 listener.reject(e);
240 });
241 throw e;
242 }
243 }
244
245 private async prepareForDeltaUpdate() {
246 if (this.pendingUpdate === null) {
247 if (this.xtextStateId === null) {
248 return;
249 }
250 await this.updateFullText();
251 }
252 let nowMs = Date.now();
253 const endMs = nowMs + WAIT_FOR_UPDATE_TIMEOUT_MS;
254 while (this.pendingUpdate !== null && nowMs < endMs) {
255 const timeoutMs = endMs - nowMs;
256 const promise = new Promise((resolve, reject) => {
257 const task = new PendingTask(resolve, reject, timeoutMs);
258 this.updateListeners.push(task);
259 });
260 // We must keep waiting uptil the update has completed,
261 // so the tasks can't be started in parallel.
262 // eslint-disable-next-line no-await-in-loop
263 await promise;
264 nowMs = Date.now();
265 }
266 if (this.pendingUpdate !== null || this.xtextStateId === null) {
267 log.error('No successful update in', WAIT_FOR_UPDATE_TIMEOUT_MS, 'ms');
268 throw new Error('Failed to wait for successful update');
269 }
270 }
271}
diff --git a/language-web/src/main/js/xtext/ValidationService.ts b/language-web/src/main/js/xtext/ValidationService.ts
new file mode 100644
index 00000000..838aa31e
--- /dev/null
+++ b/language-web/src/main/js/xtext/ValidationService.ts
@@ -0,0 +1,40 @@
1import type { Diagnostic } from '@codemirror/lint';
2
3import type { EditorStore } from '../editor/EditorStore';
4import { getLogger } from '../logging';
5import type { UpdateService } from './UpdateService';
6import { isValidationResult } from './xtextServiceResults';
7
8const log = getLogger('xtext.ValidationService');
9
10export class ValidationService {
11 private store: EditorStore;
12
13 private updateService: UpdateService;
14
15 constructor(store: EditorStore, updateService: UpdateService) {
16 this.store = store;
17 this.updateService = updateService;
18 }
19
20 onPush(push: unknown): void {
21 if (!isValidationResult(push)) {
22 log.error('Invalid validation result', push);
23 return;
24 }
25 const allChanges = this.updateService.computeChangesSinceLastUpdate();
26 const diagnostics: Diagnostic[] = [];
27 push.issues.forEach((issue) => {
28 if (issue.severity === 'ignore') {
29 return;
30 }
31 diagnostics.push({
32 from: allChanges.mapPos(issue.offset),
33 to: allChanges.mapPos(issue.offset + issue.length),
34 severity: issue.severity,
35 message: issue.description,
36 });
37 });
38 this.store.updateDiagnostics(diagnostics);
39 }
40}
diff --git a/language-web/src/main/js/xtext/XtextClient.ts b/language-web/src/main/js/xtext/XtextClient.ts
new file mode 100644
index 00000000..f8b06258
--- /dev/null
+++ b/language-web/src/main/js/xtext/XtextClient.ts
@@ -0,0 +1,73 @@
1import type {
2 CompletionContext,
3 CompletionResult,
4} from '@codemirror/autocomplete';
5import type { Transaction } from '@codemirror/state';
6
7import type { EditorStore } from '../editor/EditorStore';
8import { ContentAssistService } from './ContentAssistService';
9import { getLogger } from '../logging';
10import { UpdateService } from './UpdateService';
11import { ValidationService } from './ValidationService';
12import { XtextWebSocketClient } from './XtextWebSocketClient';
13
14const log = getLogger('xtext.XtextClient');
15
16export class XtextClient {
17 webSocketClient: XtextWebSocketClient;
18
19 updateService: UpdateService;
20
21 contentAssistService: ContentAssistService;
22
23 validationService: ValidationService;
24
25 constructor(store: EditorStore) {
26 this.webSocketClient = new XtextWebSocketClient(
27 async () => {
28 this.updateService.xtextStateId = null;
29 await this.updateService.updateFullText();
30 },
31 async (resource, stateId, service, push) => {
32 await this.onPush(resource, stateId, service, push);
33 },
34 );
35 this.updateService = new UpdateService(store, this.webSocketClient);
36 this.contentAssistService = new ContentAssistService(this.updateService);
37 this.validationService = new ValidationService(store, this.updateService);
38 }
39
40 onTransaction(transaction: Transaction): void {
41 // `ContentAssistService.prototype.onTransaction` needs the dirty change desc
42 // _before_ the current edit, so we call it before `updateService`.
43 this.contentAssistService.onTransaction(transaction);
44 this.updateService.onTransaction(transaction);
45 }
46
47 private async onPush(resource: string, stateId: string, service: string, push: unknown) {
48 const { resourceName, xtextStateId } = this.updateService;
49 if (resource !== resourceName) {
50 log.error('Unknown resource name: expected:', resourceName, 'got:', resource);
51 return;
52 }
53 if (stateId !== xtextStateId) {
54 log.error('Unexpected xtext state id: expected:', xtextStateId, 'got:', resource);
55 await this.updateService.updateFullText();
56 }
57 switch (service) {
58 case 'validate':
59 this.validationService.onPush(push);
60 return;
61 case 'highlight':
62 // TODO
63 return;
64 default:
65 log.error('Unknown push service:', service);
66 break;
67 }
68 }
69
70 contentAssist(context: CompletionContext): Promise<CompletionResult> {
71 return this.contentAssistService.contentAssist(context);
72 }
73}
diff --git a/language-web/src/main/js/xtext/XtextWebSocketClient.ts b/language-web/src/main/js/xtext/XtextWebSocketClient.ts
new file mode 100644
index 00000000..5b775500
--- /dev/null
+++ b/language-web/src/main/js/xtext/XtextWebSocketClient.ts
@@ -0,0 +1,345 @@
1import { nanoid } from 'nanoid';
2
3import { getLogger } from '../logging';
4import { PendingTask } from '../utils/PendingTask';
5import { Timer } from '../utils/Timer';
6import {
7 isErrorResponse,
8 isOkResponse,
9 isPushMessage,
10 IXtextWebRequest,
11} from './xtextMessages';
12import { isPongResult } from './xtextServiceResults';
13
14const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1';
15
16const WEBSOCKET_CLOSE_OK = 1000;
17
18const RECONNECT_DELAY_MS = [200, 1000, 5000, 30_000];
19
20const MAX_RECONNECT_DELAY_MS = RECONNECT_DELAY_MS[RECONNECT_DELAY_MS.length - 1];
21
22const BACKGROUND_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
23
24const PING_TIMEOUT_MS = 10 * 1000;
25
26const REQUEST_TIMEOUT_MS = 1000;
27
28const log = getLogger('XtextWebSocketClient');
29
30type ReconnectHandler = () => Promise<void>;
31
32type PushHandler = (
33 resourceId: string,
34 stateId: string,
35 service: string,
36 data: unknown,
37) => Promise<void>;
38
39enum State {
40 Initial,
41 Opening,
42 TabVisible,
43 TabHiddenIdle,
44 TabHiddenWaiting,
45 Error,
46 TimedOut,
47}
48
49export class XtextWebSocketClient {
50 nextMessageId = 0;
51
52 connection!: WebSocket;
53
54 pendingRequests = new Map<string, PendingTask<unknown>>();
55
56 onReconnect: ReconnectHandler;
57
58 onPush: PushHandler;
59
60 state = State.Initial;
61
62 reconnectTryCount = 0;
63
64 idleTimer = new Timer(() => {
65 this.handleIdleTimeout();
66 }, BACKGROUND_IDLE_TIMEOUT_MS);
67
68 pingTimer = new Timer(() => {
69 this.sendPing();
70 }, PING_TIMEOUT_MS);
71
72 reconnectTimer = new Timer(() => {
73 this.handleReconnect();
74 });
75
76 constructor(onReconnect: ReconnectHandler, onPush: PushHandler) {
77 this.onReconnect = onReconnect;
78 this.onPush = onPush;
79 document.addEventListener('visibilitychange', () => {
80 this.handleVisibilityChange();
81 });
82 this.reconnect();
83 }
84
85 private get isLogicallyClosed(): boolean {
86 return this.state === State.Error || this.state === State.TimedOut;
87 }
88
89 get isOpen(): boolean {
90 return this.state === State.TabVisible
91 || this.state === State.TabHiddenIdle
92 || this.state === State.TabHiddenWaiting;
93 }
94
95 private reconnect() {
96 if (this.isOpen || this.state === State.Opening) {
97 log.error('Trying to reconnect from', this.state);
98 return;
99 }
100 this.state = State.Opening;
101 const webSocketServer = window.origin.replace(/^http/, 'ws');
102 const webSocketUrl = `${webSocketServer}/xtext-service`;
103 this.connection = new WebSocket(webSocketUrl, XTEXT_SUBPROTOCOL_V1);
104 this.connection.addEventListener('open', () => {
105 if (this.connection.protocol !== XTEXT_SUBPROTOCOL_V1) {
106 log.error('Unknown subprotocol', this.connection.protocol, 'selected by server');
107 this.forceReconnectOnError();
108 }
109 if (document.visibilityState === 'hidden') {
110 this.handleTabHidden();
111 } else {
112 this.handleTabVisibleConnected();
113 }
114 log.info('Connected to websocket');
115 this.nextMessageId = 0;
116 this.reconnectTryCount = 0;
117 this.pingTimer.schedule();
118 this.onReconnect().catch((error) => {
119 log.error('Unexpected error in onReconnect handler', error);
120 });
121 });
122 this.connection.addEventListener('error', (event) => {
123 log.error('Unexpected websocket error', event);
124 this.forceReconnectOnError();
125 });
126 this.connection.addEventListener('message', (event) => {
127 this.handleMessage(event.data);
128 });
129 this.connection.addEventListener('close', (event) => {
130 if (this.isLogicallyClosed && event.code === WEBSOCKET_CLOSE_OK
131 && this.pendingRequests.size === 0) {
132 log.info('Websocket closed');
133 return;
134 }
135 log.error('Websocket closed unexpectedly', event.code, event.reason);
136 this.forceReconnectOnError();
137 });
138 }
139
140 private handleVisibilityChange() {
141 if (document.visibilityState === 'hidden') {
142 if (this.state === State.TabVisible) {
143 this.handleTabHidden();
144 }
145 return;
146 }
147 this.idleTimer.cancel();
148 if (this.state === State.TabHiddenIdle || this.state === State.TabHiddenWaiting) {
149 this.handleTabVisibleConnected();
150 return;
151 }
152 if (this.state === State.TimedOut) {
153 this.reconnect();
154 }
155 }
156
157 private handleTabHidden() {
158 log.debug('Tab hidden while websocket is connected');
159 this.state = State.TabHiddenIdle;
160 this.idleTimer.schedule();
161 }
162
163 private handleTabVisibleConnected() {
164 log.debug('Tab visible while websocket is connected');
165 this.state = State.TabVisible;
166 }
167
168 private handleIdleTimeout() {
169 log.trace('Waiting for pending tasks before disconnect');
170 if (this.state === State.TabHiddenIdle) {
171 this.state = State.TabHiddenWaiting;
172 this.handleWaitingForDisconnect();
173 }
174 }
175
176 private handleWaitingForDisconnect() {
177 if (this.state !== State.TabHiddenWaiting) {
178 return;
179 }
180 const pending = this.pendingRequests.size;
181 if (pending === 0) {
182 log.info('Closing idle websocket');
183 this.state = State.TimedOut;
184 this.closeConnection(1000, 'idle timeout');
185 return;
186 }
187 log.info('Waiting for', pending, 'pending requests before closing websocket');
188 }
189
190 private sendPing() {
191 if (!this.isOpen) {
192 return;
193 }
194 const ping = nanoid();
195 log.trace('Ping', ping);
196 this.send({ ping }).then((result) => {
197 if (isPongResult(result) && result.pong === ping) {
198 log.trace('Pong', ping);
199 this.pingTimer.schedule();
200 } else {
201 log.error('Invalid pong');
202 this.forceReconnectOnError();
203 }
204 }).catch((error) => {
205 log.error('Error while waiting for ping', error);
206 this.forceReconnectOnError();
207 });
208 }
209
210 send(request: unknown): Promise<unknown> {
211 if (!this.isOpen) {
212 throw new Error('Not open');
213 }
214 const messageId = this.nextMessageId.toString(16);
215 if (messageId in this.pendingRequests) {
216 log.error('Message id wraparound still pending', messageId);
217 this.rejectRequest(messageId, new Error('Message id wraparound'));
218 }
219 if (this.nextMessageId >= Number.MAX_SAFE_INTEGER) {
220 this.nextMessageId = 0;
221 } else {
222 this.nextMessageId += 1;
223 }
224 const message = JSON.stringify({
225 id: messageId,
226 request,
227 } as IXtextWebRequest);
228 log.trace('Sending message', message);
229 return new Promise((resolve, reject) => {
230 const task = new PendingTask(resolve, reject, REQUEST_TIMEOUT_MS, () => {
231 this.removePendingRequest(messageId);
232 });
233 this.pendingRequests.set(messageId, task);
234 this.connection.send(message);
235 });
236 }
237
238 private handleMessage(messageStr: unknown) {
239 if (typeof messageStr !== 'string') {
240 log.error('Unexpected binary message', messageStr);
241 this.forceReconnectOnError();
242 return;
243 }
244 log.trace('Incoming websocket message', messageStr);
245 let message: unknown;
246 try {
247 message = JSON.parse(messageStr);
248 } catch (error) {
249 log.error('Json parse error', error);
250 this.forceReconnectOnError();
251 return;
252 }
253 if (isOkResponse(message)) {
254 this.resolveRequest(message.id, message.response);
255 } else if (isErrorResponse(message)) {
256 this.rejectRequest(message.id, new Error(`${message.error} error: ${message.message}`));
257 if (message.error === 'server') {
258 log.error('Reconnecting due to server error: ', message.message);
259 this.forceReconnectOnError();
260 }
261 } else if (isPushMessage(message)) {
262 this.onPush(
263 message.resource,
264 message.stateId,
265 message.service,
266 message.push,
267 ).catch((error) => {
268 log.error('Unexpected error in onPush handler', error);
269 });
270 } else {
271 log.error('Unexpected websocket message', message);
272 this.forceReconnectOnError();
273 }
274 }
275
276 private resolveRequest(messageId: string, value: unknown) {
277 const pendingRequest = this.pendingRequests.get(messageId);
278 if (pendingRequest) {
279 pendingRequest.resolve(value);
280 this.removePendingRequest(messageId);
281 return;
282 }
283 log.error('Trying to resolve unknown request', messageId, 'with', value);
284 }
285
286 private rejectRequest(messageId: string, reason?: unknown) {
287 const pendingRequest = this.pendingRequests.get(messageId);
288 if (pendingRequest) {
289 pendingRequest.reject(reason);
290 this.removePendingRequest(messageId);
291 return;
292 }
293 log.error('Trying to reject unknown request', messageId, 'with', reason);
294 }
295
296 private removePendingRequest(messageId: string) {
297 this.pendingRequests.delete(messageId);
298 this.handleWaitingForDisconnect();
299 }
300
301 forceReconnectOnError(): void {
302 if (this.isLogicallyClosed) {
303 return;
304 }
305 this.abortPendingRequests();
306 this.closeConnection(1000, 'reconnecting due to error');
307 log.error('Reconnecting after delay due to error');
308 this.handleErrorState();
309 }
310
311 private abortPendingRequests() {
312 this.pendingRequests.forEach((request) => {
313 request.reject(new Error('Websocket disconnect'));
314 });
315 this.pendingRequests.clear();
316 }
317
318 private closeConnection(code: number, reason: string) {
319 this.pingTimer.cancel();
320 const { readyState } = this.connection;
321 if (readyState !== WebSocket.CLOSING && readyState !== WebSocket.CLOSED) {
322 this.connection.close(code, reason);
323 }
324 }
325
326 private handleErrorState() {
327 this.state = State.Error;
328 this.reconnectTryCount += 1;
329 const delay = RECONNECT_DELAY_MS[this.reconnectTryCount - 1] || MAX_RECONNECT_DELAY_MS;
330 log.info('Reconnecting in', delay, 'ms');
331 this.reconnectTimer.schedule(delay);
332 }
333
334 private handleReconnect() {
335 if (this.state !== State.Error) {
336 log.error('Unexpected reconnect in', this.state);
337 return;
338 }
339 if (document.visibilityState === 'hidden') {
340 this.state = State.TimedOut;
341 } else {
342 this.reconnect();
343 }
344 }
345}
diff --git a/language-web/src/main/js/xtext/compatibility.js b/language-web/src/main/js/xtext/compatibility.js
deleted file mode 100644
index c877fc56..00000000
--- a/language-web/src/main/js/xtext/compatibility.js
+++ /dev/null
@@ -1,63 +0,0 @@
1/*******************************************************************************
2 * Copyright (c) 2015 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
10define([], function() {
11
12 if (!Function.prototype.bind) {
13 Function.prototype.bind = function(target) {
14 if (typeof this !== 'function')
15 throw new TypeError('bind target is not callable');
16 var args = Array.prototype.slice.call(arguments, 1);
17 var unboundFunc = this;
18 var nopFunc = function() {};
19 boundFunc = function() {
20 var localArgs = Array.prototype.slice.call(arguments);
21 return unboundFunc.apply(this instanceof nopFunc ? this : target,
22 args.concat(localArgs));
23 };
24 nopFunc.prototype = this.prototype;
25 boundFunc.prototype = new nopFunc();
26 return boundFunc;
27 }
28 }
29
30 if (!Array.prototype.map) {
31 Array.prototype.map = function(callback, thisArg) {
32 if (this == null)
33 throw new TypeError('this is null');
34 if (typeof callback !== 'function')
35 throw new TypeError('callback is not callable');
36 var srcArray = Object(this);
37 var len = srcArray.length >>> 0;
38 var tgtArray = new Array(len);
39 for (var i = 0; i < len; i++) {
40 if (i in srcArray)
41 tgtArray[i] = callback.call(thisArg, srcArray[i], i, srcArray);
42 }
43 return tgtArray;
44 }
45 }
46
47 if (!Array.prototype.forEach) {
48 Array.prototype.forEach = function(callback, thisArg) {
49 if (this == null)
50 throw new TypeError('this is null');
51 if (typeof callback !== 'function')
52 throw new TypeError('callback is not callable');
53 var srcArray = Object(this);
54 var len = srcArray.length >>> 0;
55 for (var i = 0; i < len; i++) {
56 if (i in srcArray)
57 callback.call(thisArg, srcArray[i], i, srcArray);
58 }
59 }
60 }
61
62 return {};
63});
diff --git a/language-web/src/main/js/xtext/services/ContentAssistService.js b/language-web/src/main/js/xtext/services/ContentAssistService.js
deleted file mode 100644
index 1686570d..00000000
--- a/language-web/src/main/js/xtext/services/ContentAssistService.js
+++ /dev/null
@@ -1,132 +0,0 @@
1/*******************************************************************************
2 * Copyright (c) 2015 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
10define(['xtext/services/XtextService', 'jquery'], function(XtextService, jQuery) {
11
12 /**
13 * Service class for content assist proposals. The proposals are returned as promise of
14 * a Deferred object.
15 */
16 function ContentAssistService(serviceUrl, resourceId, updateService) {
17 this.initialize(serviceUrl, 'assist', resourceId, updateService);
18 }
19
20 ContentAssistService.prototype = new XtextService();
21
22 ContentAssistService.prototype.invoke = function(editorContext, params, deferred) {
23 if (deferred === undefined) {
24 deferred = jQuery.Deferred();
25 }
26 var serverData = {
27 contentType: params.contentType
28 };
29 if (params.offset)
30 serverData.caretOffset = params.offset;
31 else
32 serverData.caretOffset = editorContext.getCaretOffset();
33 var selection = params.selection ? params.selection : editorContext.getSelection();
34 if (selection.start != serverData.caretOffset || selection.end != serverData.caretOffset) {
35 serverData.selectionStart = selection.start;
36 serverData.selectionEnd = selection.end;
37 }
38 var currentText;
39 var httpMethod = 'GET';
40 var onComplete = undefined;
41 var knownServerState = editorContext.getServerState();
42 if (params.sendFullText) {
43 serverData.fullText = editorContext.getText();
44 httpMethod = 'POST';
45 } else {
46 serverData.requiredStateId = knownServerState.stateId;
47 if (this._updateService) {
48 if (knownServerState.text === undefined || knownServerState.updateInProgress) {
49 var self = this;
50 this._updateService.addCompletionCallback(function() {
51 self.invoke(editorContext, params, deferred);
52 });
53 return deferred.promise();
54 }
55 knownServerState.updateInProgress = true;
56 onComplete = this._updateService.onComplete.bind(this._updateService);
57 currentText = editorContext.getText();
58 this._updateService.computeDelta(knownServerState.text, currentText, serverData);
59 if (serverData.deltaText !== undefined) {
60 httpMethod = 'POST';
61 }
62 }
63 }
64
65 var self = this;
66 self.sendRequest(editorContext, {
67 type: httpMethod,
68 data: serverData,
69
70 success: function(result) {
71 if (result.conflict) {
72 // The server has lost its session state and the resource is loaded from the server
73 if (self._increaseRecursionCount(editorContext)) {
74 if (onComplete) {
75 delete knownServerState.updateInProgress;
76 delete knownServerState.text;
77 delete knownServerState.stateId;
78 self._updateService.addCompletionCallback(function() {
79 self.invoke(editorContext, params, deferred);
80 });
81 self._updateService.invoke(editorContext, params);
82 } else {
83 var paramsCopy = {};
84 for (var p in params) {
85 if (params.hasOwnProperty(p))
86 paramsCopy[p] = params[p];
87 }
88 paramsCopy.sendFullText = true;
89 self.invoke(editorContext, paramsCopy, deferred);
90 }
91 } else {
92 deferred.reject(result.conflict);
93 }
94 return false;
95 }
96 if (onComplete && result.stateId !== undefined && result.stateId != editorContext.getServerState().stateId) {
97 var listeners = editorContext.updateServerState(currentText, result.stateId);
98 for (var i = 0; i < listeners.length; i++) {
99 self._updateService.addCompletionCallback(listeners[i], params);
100 }
101 }
102 deferred.resolve(result.entries);
103 },
104
105 error: function(xhr, textStatus, errorThrown) {
106 if (onComplete && xhr.status == 404 && !params.loadFromServer && knownServerState.text !== undefined) {
107 // The server has lost its session state and the resource is not loaded from the server
108 delete knownServerState.updateInProgress;
109 delete knownServerState.text;
110 delete knownServerState.stateId;
111 self._updateService.addCompletionCallback(function() {
112 self.invoke(editorContext, params, deferred);
113 });
114 self._updateService.invoke(editorContext, params);
115 return true;
116 }
117 deferred.reject(errorThrown);
118 },
119
120 complete: onComplete
121 }, !params.sendFullText);
122 var result = deferred.promise();
123 if (onComplete) {
124 result.always(function() {
125 knownServerState.updateInProgress = false;
126 });
127 }
128 return result;
129 };
130
131 return ContentAssistService;
132});
diff --git a/language-web/src/main/js/xtext/services/FormattingService.js b/language-web/src/main/js/xtext/services/FormattingService.js
deleted file mode 100644
index f59099ee..00000000
--- a/language-web/src/main/js/xtext/services/FormattingService.js
+++ /dev/null
@@ -1,52 +0,0 @@
1/*******************************************************************************
2 * Copyright (c) 2015 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
10define(['xtext/services/XtextService', 'jquery'], function(XtextService, jQuery) {
11
12 /**
13 * Service class for formatting text.
14 */
15 function FormattingService(serviceUrl, resourceId, updateService) {
16 this.initialize(serviceUrl, 'format', resourceId, updateService);
17 };
18
19 FormattingService.prototype = new XtextService();
20
21 FormattingService.prototype._initServerData = function(serverData, editorContext, params) {
22 var selection = params.selection ? params.selection : editorContext.getSelection();
23 if (selection.end > selection.start) {
24 serverData.selectionStart = selection.start;
25 serverData.selectionEnd = selection.end;
26 }
27 return {
28 httpMethod: 'POST'
29 };
30 };
31
32 FormattingService.prototype._processResult = function(result, editorContext) {
33 // The text update may be asynchronous, so we have to compute the new text ourselves
34 var newText;
35 if (result.replaceRegion) {
36 var fullText = editorContext.getText();
37 var start = result.replaceRegion.offset;
38 var end = result.replaceRegion.offset + result.replaceRegion.length;
39 editorContext.setText(result.formattedText, start, end);
40 newText = fullText.substring(0, start) + result.formattedText + fullText.substring(end);
41 } else {
42 editorContext.setText(result.formattedText);
43 newText = result.formattedText;
44 }
45 var listeners = editorContext.updateServerState(newText, result.stateId);
46 for (var i = 0; i < listeners.length; i++) {
47 listeners[i]({});
48 }
49 };
50
51 return FormattingService;
52}); \ No newline at end of file
diff --git a/language-web/src/main/js/xtext/services/HighlightingService.js b/language-web/src/main/js/xtext/services/HighlightingService.js
deleted file mode 100644
index 5a5ac8ba..00000000
--- a/language-web/src/main/js/xtext/services/HighlightingService.js
+++ /dev/null
@@ -1,33 +0,0 @@
1/*******************************************************************************
2 * Copyright (c) 2015 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
10define(['xtext/services/XtextService', 'jquery'], function(XtextService, jQuery) {
11
12 /**
13 * Service class for semantic highlighting.
14 */
15 function HighlightingService(serviceUrl, resourceId) {
16 this.initialize(serviceUrl, 'highlight', resourceId);
17 };
18
19 HighlightingService.prototype = new XtextService();
20
21 HighlightingService.prototype._checkPreconditions = function(editorContext, params) {
22 return this._state === undefined;
23 }
24
25 HighlightingService.prototype._onConflict = function(editorContext, cause) {
26 this.setState(undefined);
27 return {
28 suppressForcedUpdate: true
29 };
30 };
31
32 return HighlightingService;
33}); \ No newline at end of file
diff --git a/language-web/src/main/js/xtext/services/HoverService.js b/language-web/src/main/js/xtext/services/HoverService.js
deleted file mode 100644
index 03c5a52b..00000000
--- a/language-web/src/main/js/xtext/services/HoverService.js
+++ /dev/null
@@ -1,59 +0,0 @@
1/*******************************************************************************
2 * Copyright (c) 2015 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
10define(['xtext/services/XtextService', 'jquery'], function(XtextService, jQuery) {
11
12 /**
13 * Service class for hover information.
14 */
15 function HoverService(serviceUrl, resourceId, updateService) {
16 this.initialize(serviceUrl, 'hover', resourceId, updateService);
17 };
18
19 HoverService.prototype = new XtextService();
20
21 HoverService.prototype._initServerData = function(serverData, editorContext, params) {
22 // In order to display hover info for a selected completion proposal while the content
23 // assist popup is shown, the selected proposal is passed as parameter
24 if (params.proposal && params.proposal.proposal)
25 serverData.proposal = params.proposal.proposal;
26 if (params.offset)
27 serverData.caretOffset = params.offset;
28 else
29 serverData.caretOffset = editorContext.getCaretOffset();
30 var selection = params.selection ? params.selection : editorContext.getSelection();
31 if (selection.start != serverData.caretOffset || selection.end != serverData.caretOffset) {
32 serverData.selectionStart = selection.start;
33 serverData.selectionEnd = selection.end;
34 }
35 };
36
37 HoverService.prototype._getSuccessCallback = function(editorContext, params, deferred) {
38 var delay = params.mouseHoverDelay;
39 if (!delay)
40 delay = 500;
41 var showTime = new Date().getTime() + delay;
42 return function(result) {
43 if (result.conflict || !result.title && !result.content) {
44 deferred.reject();
45 } else {
46 var remainingTimeout = Math.max(0, showTime - new Date().getTime());
47 setTimeout(function() {
48 if (!params.sendFullText && result.stateId !== undefined
49 && result.stateId != editorContext.getServerState().stateId)
50 deferred.reject();
51 else
52 deferred.resolve(result);
53 }, remainingTimeout);
54 }
55 };
56 };
57
58 return HoverService;
59}); \ No newline at end of file
diff --git a/language-web/src/main/js/xtext/services/LoadResourceService.js b/language-web/src/main/js/xtext/services/LoadResourceService.js
deleted file mode 100644
index b5a315c3..00000000
--- a/language-web/src/main/js/xtext/services/LoadResourceService.js
+++ /dev/null
@@ -1,42 +0,0 @@
1/*******************************************************************************
2 * Copyright (c) 2015 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
10define(['xtext/services/XtextService', 'jquery'], function(XtextService, jQuery) {
11
12 /**
13 * Service class for loading resources. The resulting text is passed to the editor context.
14 */
15 function LoadResourceService(serviceUrl, resourceId, revert) {
16 this.initialize(serviceUrl, revert ? 'revert' : 'load', resourceId);
17 };
18
19 LoadResourceService.prototype = new XtextService();
20
21 LoadResourceService.prototype._initServerData = function(serverData, editorContext, params) {
22 return {
23 suppressContent: true,
24 httpMethod: this._serviceType == 'revert' ? 'POST' : 'GET'
25 };
26 };
27
28 LoadResourceService.prototype._getSuccessCallback = function(editorContext, params, deferred) {
29 return function(result) {
30 editorContext.setText(result.fullText);
31 editorContext.clearUndoStack();
32 editorContext.setDirty(result.dirty);
33 var listeners = editorContext.updateServerState(result.fullText, result.stateId);
34 for (var i = 0; i < listeners.length; i++) {
35 listeners[i](params);
36 }
37 deferred.resolve(result);
38 }
39 }
40
41 return LoadResourceService;
42}); \ No newline at end of file
diff --git a/language-web/src/main/js/xtext/services/OccurrencesService.js b/language-web/src/main/js/xtext/services/OccurrencesService.js
deleted file mode 100644
index 2e2d0b1a..00000000
--- a/language-web/src/main/js/xtext/services/OccurrencesService.js
+++ /dev/null
@@ -1,39 +0,0 @@
1/*******************************************************************************
2 * Copyright (c) 2015 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
10define(['xtext/services/XtextService', 'jquery'], function(XtextService, jQuery) {
11
12 /**
13 * Service class for marking occurrences.
14 */
15 function OccurrencesService(serviceUrl, resourceId, updateService) {
16 this.initialize(serviceUrl, 'occurrences', resourceId, updateService);
17 };
18
19 OccurrencesService.prototype = new XtextService();
20
21 OccurrencesService.prototype._initServerData = function(serverData, editorContext, params) {
22 if (params.offset)
23 serverData.caretOffset = params.offset;
24 else
25 serverData.caretOffset = editorContext.getCaretOffset();
26 };
27
28 OccurrencesService.prototype._getSuccessCallback = function(editorContext, params, deferred) {
29 return function(result) {
30 if (result.conflict || !params.sendFullText && result.stateId !== undefined
31 && result.stateId != editorContext.getServerState().stateId)
32 deferred.reject();
33 else
34 deferred.resolve(result);
35 }
36 }
37
38 return OccurrencesService;
39}); \ No newline at end of file
diff --git a/language-web/src/main/js/xtext/services/SaveResourceService.js b/language-web/src/main/js/xtext/services/SaveResourceService.js
deleted file mode 100644
index 66cdaff5..00000000
--- a/language-web/src/main/js/xtext/services/SaveResourceService.js
+++ /dev/null
@@ -1,32 +0,0 @@
1/*******************************************************************************
2 * Copyright (c) 2015 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
10define(['xtext/services/XtextService', 'jquery'], function(XtextService, jQuery) {
11
12 /**
13 * Service class for saving resources.
14 */
15 function SaveResourceService(serviceUrl, resourceId) {
16 this.initialize(serviceUrl, 'save', resourceId);
17 };
18
19 SaveResourceService.prototype = new XtextService();
20
21 SaveResourceService.prototype._initServerData = function(serverData, editorContext, params) {
22 return {
23 httpMethod: 'POST'
24 };
25 };
26
27 SaveResourceService.prototype._processResult = function(result, editorContext) {
28 editorContext.setDirty(false);
29 };
30
31 return SaveResourceService;
32}); \ No newline at end of file
diff --git a/language-web/src/main/js/xtext/services/UpdateService.js b/language-web/src/main/js/xtext/services/UpdateService.js
deleted file mode 100644
index b78d846d..00000000
--- a/language-web/src/main/js/xtext/services/UpdateService.js
+++ /dev/null
@@ -1,159 +0,0 @@
1/*******************************************************************************
2 * Copyright (c) 2015 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
10define(['xtext/services/XtextService', 'jquery'], function(XtextService, jQuery) {
11
12 /**
13 * Service class for updating the server-side representation of a resource.
14 * This service only makes sense with a stateful server, where an update request is sent
15 * after each modification. This can greatly improve response times compared to the
16 * stateless alternative, where the full text content is sent with each service request.
17 */
18 function UpdateService(serviceUrl, resourceId) {
19 this.initialize(serviceUrl, 'update', resourceId, this);
20 this._completionCallbacks = [];
21 };
22
23 UpdateService.prototype = new XtextService();
24
25 /**
26 * Compute a delta between two versions of a text. If a difference is found, the result
27 * contains three properties:
28 * deltaText - the text to insert into s1
29 * deltaOffset - the text insertion offset
30 * deltaReplaceLength - the number of characters that shall be replaced by the inserted text
31 */
32 UpdateService.prototype.computeDelta = function(s1, s2, result) {
33 var start = 0, s1length = s1.length, s2length = s2.length;
34 while (start < s1length && start < s2length && s1.charCodeAt(start) === s2.charCodeAt(start)) {
35 start++;
36 }
37 if (start === s1length && start === s2length) {
38 return;
39 }
40 result.deltaOffset = start;
41 if (start === s1length) {
42 result.deltaText = s2.substring(start, s2length);
43 result.deltaReplaceLength = 0;
44 return;
45 } else if (start === s2length) {
46 result.deltaText = '';
47 result.deltaReplaceLength = s1length - start;
48 return;
49 }
50
51 var end1 = s1length - 1, end2 = s2length - 1;
52 while (end1 >= start && end2 >= start && s1.charCodeAt(end1) === s2.charCodeAt(end2)) {
53 end1--;
54 end2--;
55 }
56 result.deltaText = s2.substring(start, end2 + 1);
57 result.deltaReplaceLength = end1 - start + 1;
58 };
59
60 /**
61 * Invoke all completion callbacks and clear the list afterwards.
62 */
63 UpdateService.prototype.onComplete = function(xhr, textStatus) {
64 var callbacks = this._completionCallbacks;
65 this._completionCallbacks = [];
66 for (var i = 0; i < callbacks.length; i++) {
67 var callback = callbacks[i].callback;
68 var params = callbacks[i].params;
69 callback(params);
70 }
71 }
72
73 /**
74 * Add a callback to be invoked when the service call has completed.
75 */
76 UpdateService.prototype.addCompletionCallback = function(callback, params) {
77 this._completionCallbacks.push({callback: callback, params: params});
78 }
79
80 UpdateService.prototype.invoke = function(editorContext, params, deferred) {
81 if (deferred === undefined) {
82 deferred = jQuery.Deferred();
83 }
84 var knownServerState = editorContext.getServerState();
85 if (knownServerState.updateInProgress) {
86 var self = this;
87 this.addCompletionCallback(function() { self.invoke(editorContext, params, deferred) });
88 return deferred.promise();
89 }
90
91 var serverData = {
92 contentType: params.contentType
93 };
94 var currentText = editorContext.getText();
95 if (params.sendFullText || knownServerState.text === undefined) {
96 serverData.fullText = currentText;
97 } else {
98 this.computeDelta(knownServerState.text, currentText, serverData);
99 if (serverData.deltaText === undefined) {
100 if (params.forceUpdate) {
101 serverData.deltaText = '';
102 serverData.deltaOffset = editorContext.getCaretOffset();
103 serverData.deltaReplaceLength = 0;
104 } else {
105 deferred.resolve(knownServerState);
106 this.onComplete();
107 return deferred.promise();
108 }
109 }
110 serverData.requiredStateId = knownServerState.stateId;
111 }
112
113 knownServerState.updateInProgress = true;
114 var self = this;
115 self.sendRequest(editorContext, {
116 type: 'PUT',
117 data: serverData,
118
119 success: function(result) {
120 if (result.conflict) {
121 // The server has lost its session state and the resource is loaded from the server
122 if (knownServerState.text !== undefined) {
123 delete knownServerState.updateInProgress;
124 delete knownServerState.text;
125 delete knownServerState.stateId;
126 self.invoke(editorContext, params, deferred);
127 } else {
128 deferred.reject(result.conflict);
129 }
130 return false;
131 }
132 var listeners = editorContext.updateServerState(currentText, result.stateId);
133 for (var i = 0; i < listeners.length; i++) {
134 self.addCompletionCallback(listeners[i], params);
135 }
136 deferred.resolve(result);
137 },
138
139 error: function(xhr, textStatus, errorThrown) {
140 if (xhr.status == 404 && !params.loadFromServer && knownServerState.text !== undefined) {
141 // The server has lost its session state and the resource is not loaded from the server
142 delete knownServerState.updateInProgress;
143 delete knownServerState.text;
144 delete knownServerState.stateId;
145 self.invoke(editorContext, params, deferred);
146 return true;
147 }
148 deferred.reject(errorThrown);
149 },
150
151 complete: self.onComplete.bind(self)
152 }, true);
153 return deferred.promise().always(function() {
154 knownServerState.updateInProgress = false;
155 });
156 };
157
158 return UpdateService;
159}); \ No newline at end of file
diff --git a/language-web/src/main/js/xtext/services/ValidationService.js b/language-web/src/main/js/xtext/services/ValidationService.js
deleted file mode 100644
index 85c9953d..00000000
--- a/language-web/src/main/js/xtext/services/ValidationService.js
+++ /dev/null
@@ -1,33 +0,0 @@
1/*******************************************************************************
2 * Copyright (c) 2015 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
10define(['xtext/services/XtextService', 'jquery'], function(XtextService, jQuery) {
11
12 /**
13 * Service class for validation.
14 */
15 function ValidationService(serviceUrl, resourceId) {
16 this.initialize(serviceUrl, 'validate', resourceId);
17 };
18
19 ValidationService.prototype = new XtextService();
20
21 ValidationService.prototype._checkPreconditions = function(editorContext, params) {
22 return this._state === undefined;
23 }
24
25 ValidationService.prototype._onConflict = function(editorContext, cause) {
26 this.setState(undefined);
27 return {
28 suppressForcedUpdate: true
29 };
30 };
31
32 return ValidationService;
33}); \ No newline at end of file
diff --git a/language-web/src/main/js/xtext/services/XtextService.js b/language-web/src/main/js/xtext/services/XtextService.js
deleted file mode 100644
index d3a4842f..00000000
--- a/language-web/src/main/js/xtext/services/XtextService.js
+++ /dev/null
@@ -1,280 +0,0 @@
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
10define(['jquery'], function(jQuery) {
11
12 var globalState = {};
13
14 /**
15 * Generic service implementation that can serve as superclass for specialized services.
16 */
17 function XtextService() {};
18
19 /**
20 * Initialize the request metadata for this service class. Two variants:
21 * - initialize(serviceUrl, serviceType, resourceId, updateService)
22 * - initialize(xtextServices, serviceType)
23 */
24 XtextService.prototype.initialize = function() {
25 this._serviceType = arguments[1];
26 if (typeof(arguments[0]) === 'string') {
27 this._requestUrl = arguments[0] + '/' + this._serviceType;
28 var resourceId = arguments[2];
29 if (resourceId)
30 this._encodedResourceId = encodeURIComponent(resourceId);
31 this._updateService = arguments[3];
32 } else {
33 var xtextServices = arguments[0];
34 if (xtextServices.options) {
35 this._requestUrl = xtextServices.options.serviceUrl + '/' + this._serviceType;
36 var resourceId = xtextServices.options.resourceId;
37 if (resourceId)
38 this._encodedResourceId = encodeURIComponent(resourceId);
39 }
40 this._updateService = xtextServices.updateService;
41 }
42 }
43
44 XtextService.prototype.setState = function(state) {
45 this._state = state;
46 }
47
48 /**
49 * Invoke the service with default service behavior.
50 */
51 XtextService.prototype.invoke = function(editorContext, params, deferred, callbacks) {
52 if (deferred === undefined) {
53 deferred = jQuery.Deferred();
54 }
55 if (jQuery.isFunction(this._checkPreconditions) && !this._checkPreconditions(editorContext, params)) {
56 deferred.reject();
57 return deferred.promise();
58 }
59 var serverData = {
60 contentType: params.contentType
61 };
62 var initResult;
63 if (jQuery.isFunction(this._initServerData))
64 initResult = this._initServerData(serverData, editorContext, params);
65 var httpMethod = 'GET';
66 if (initResult && initResult.httpMethod)
67 httpMethod = initResult.httpMethod;
68 var self = this;
69 if (!(initResult && initResult.suppressContent)) {
70 if (params.sendFullText) {
71 serverData.fullText = editorContext.getText();
72 httpMethod = 'POST';
73 } else {
74 var knownServerState = editorContext.getServerState();
75 if (knownServerState.updateInProgress) {
76 if (self._updateService) {
77 self._updateService.addCompletionCallback(function() {
78 self.invoke(editorContext, params, deferred);
79 });
80 } else {
81 deferred.reject();
82 }
83 return deferred.promise();
84 }
85 if (knownServerState.stateId !== undefined) {
86 serverData.requiredStateId = knownServerState.stateId;
87 }
88 }
89 }
90
91 var onSuccess;
92 if (jQuery.isFunction(this._getSuccessCallback)) {
93 onSuccess = this._getSuccessCallback(editorContext, params, deferred);
94 } else {
95 onSuccess = function(result) {
96 if (result.conflict) {
97 if (self._increaseRecursionCount(editorContext)) {
98 var onConflictResult;
99 if (jQuery.isFunction(self._onConflict)) {
100 onConflictResult = self._onConflict(editorContext, result.conflict);
101 }
102 if (!(onConflictResult && onConflictResult.suppressForcedUpdate) && !params.sendFullText
103 && result.conflict == 'invalidStateId' && self._updateService) {
104 self._updateService.addCompletionCallback(function() {
105 self.invoke(editorContext, params, deferred);
106 });
107 var knownServerState = editorContext.getServerState();
108 delete knownServerState.stateId;
109 delete knownServerState.text;
110 self._updateService.invoke(editorContext, params);
111 } else {
112 self.invoke(editorContext, params, deferred);
113 }
114 } else {
115 deferred.reject();
116 }
117 return false;
118 }
119 if (jQuery.isFunction(self._processResult)) {
120 var processedResult = self._processResult(result, editorContext);
121 if (processedResult) {
122 deferred.resolve(processedResult);
123 return true;
124 }
125 }
126 deferred.resolve(result);
127 };
128 }
129
130 var onError = function(xhr, textStatus, errorThrown) {
131 if (xhr.status == 404 && !params.loadFromServer && self._increaseRecursionCount(editorContext)) {
132 var onConflictResult;
133 if (jQuery.isFunction(self._onConflict)) {
134 onConflictResult = self._onConflict(editorContext, errorThrown);
135 }
136 var knownServerState = editorContext.getServerState();
137 if (!(onConflictResult && onConflictResult.suppressForcedUpdate)
138 && knownServerState.text !== undefined && self._updateService) {
139 self._updateService.addCompletionCallback(function() {
140 self.invoke(editorContext, params, deferred);
141 });
142 delete knownServerState.stateId;
143 delete knownServerState.text;
144 self._updateService.invoke(editorContext, params);
145 return true;
146 }
147 }
148 deferred.reject(errorThrown);
149 }
150
151 self.sendRequest(editorContext, {
152 type: httpMethod,
153 data: serverData,
154 success: onSuccess,
155 error: onError
156 }, !params.sendFullText);
157 return deferred.promise().always(function() {
158 self._recursionCount = undefined;
159 });
160 }
161
162 /**
163 * Send an HTTP request to invoke the service.
164 */
165 XtextService.prototype.sendRequest = function(editorContext, settings, needsSession) {
166 var self = this;
167 self.setState('started');
168 var corsEnabled = editorContext.xtextServices.options['enableCors'];
169 if(corsEnabled) {
170 settings.crossDomain = true;
171 settings.xhrFields = {withCredentials: true};
172 }
173 var onSuccess = settings.success;
174 settings.success = function(result) {
175 var accepted = true;
176 if (jQuery.isFunction(onSuccess)) {
177 accepted = onSuccess(result);
178 }
179 if (accepted || accepted === undefined) {
180 self.setState('finished');
181 if (editorContext.xtextServices) {
182 var successListeners = editorContext.xtextServices.successListeners;
183 if (successListeners) {
184 for (var i = 0; i < successListeners.length; i++) {
185 var listener = successListeners[i];
186 if (jQuery.isFunction(listener)) {
187 listener(self._serviceType, result);
188 }
189 }
190 }
191 }
192 }
193 };
194
195 var onError = settings.error;
196 settings.error = function(xhr, textStatus, errorThrown) {
197 var resolved = false;
198 if (jQuery.isFunction(onError)) {
199 resolved = onError(xhr, textStatus, errorThrown);
200 }
201 if (!resolved) {
202 self.setState(undefined);
203 self._reportError(editorContext, textStatus, errorThrown, xhr);
204 }
205 };
206
207 settings.async = true;
208 var requestUrl = self._requestUrl;
209 if (!settings.data.resource && self._encodedResourceId) {
210 if (requestUrl.indexOf('?') >= 0)
211 requestUrl += '&resource=' + self._encodedResourceId;
212 else
213 requestUrl += '?resource=' + self._encodedResourceId;
214 }
215
216 if (needsSession && globalState._initPending) {
217 // We have to wait until the initial request has finished to make sure the client has
218 // received a valid session id
219 if (!globalState._waitingRequests)
220 globalState._waitingRequests = [];
221 globalState._waitingRequests.push({requestUrl: requestUrl, settings: settings});
222 } else {
223 if (needsSession && !globalState._initDone) {
224 globalState._initPending = true;
225 var onComplete = settings.complete;
226 settings.complete = function(xhr, textStatus) {
227 if (jQuery.isFunction(onComplete)) {
228 onComplete(xhr, textStatus);
229 }
230 delete globalState._initPending;
231 globalState._initDone = true;
232 if (globalState._waitingRequests) {
233 for (var i = 0; i < globalState._waitingRequests.length; i++) {
234 var request = globalState._waitingRequests[i];
235 jQuery.ajax(request.requestUrl, request.settings);
236 }
237 delete globalState._waitingRequests;
238 }
239 }
240 }
241 jQuery.ajax(requestUrl, settings);
242 }
243 }
244
245 /**
246 * Use this in case of a conflict before retrying the service invocation. If the number
247 * of retries exceeds the limit, an error is reported and the function returns false.
248 */
249 XtextService.prototype._increaseRecursionCount = function(editorContext) {
250 if (this._recursionCount === undefined)
251 this._recursionCount = 1;
252 else
253 this._recursionCount++;
254
255 if (this._recursionCount >= 10) {
256 this._reportError(editorContext, 'warning', 'Xtext service request failed after 10 attempts.', {});
257 return false;
258 }
259 return true;
260 },
261
262 /**
263 * Report an error to the listeners.
264 */
265 XtextService.prototype._reportError = function(editorContext, severity, message, requestData) {
266 if (editorContext.xtextServices) {
267 var errorListeners = editorContext.xtextServices.errorListeners;
268 if (errorListeners) {
269 for (var i = 0; i < errorListeners.length; i++) {
270 var listener = errorListeners[i];
271 if (jQuery.isFunction(listener)) {
272 listener(this._serviceType, severity, message, requestData);
273 }
274 }
275 }
276 }
277 }
278
279 return XtextService;
280});
diff --git a/language-web/src/main/js/xtext/xtext-codemirror.d.ts b/language-web/src/main/js/xtext/xtext-codemirror.d.ts
deleted file mode 100644
index fff850b8..00000000
--- a/language-web/src/main/js/xtext/xtext-codemirror.d.ts
+++ /dev/null
@@ -1,43 +0,0 @@
1import { Editor } from 'codemirror';
2
3export function createEditor(options: IXtextOptions): IXtextCodeMirrorEditor;
4
5export function createServices(editor: Editor, options: IXtextOptions): IXtextServices;
6
7export function removeServices(editor: Editor): void;
8
9export interface IXtextOptions {
10 baseUrl?: string;
11 contentType?: string;
12 dirtyElement?: string | Element;
13 dirtyStatusClass?: string;
14 document?: Document;
15 enableContentAssistService?: boolean;
16 enableCors?: boolean;
17 enableFormattingAction?: boolean;
18 enableFormattingService?: boolean;
19 enableGeneratorService?: boolean;
20 enableHighlightingService?: boolean;
21 enableOccurrencesService?: boolean;
22 enableSaveAction?: boolean;
23 enableValidationService?: boolean;
24 loadFromServer?: boolean;
25 mode?: string;
26 parent?: string | Element;
27 parentClass?: string;
28 resourceId?: string;
29 selectionUpdateDelay?: number;
30 sendFullText?: boolean;
31 serviceUrl?: string;
32 showErrorDialogs?: boolean;
33 syntaxDefinition?: string;
34 textUpdateDelay?: number;
35 xtextLang?: string;
36}
37
38export interface IXtextCodeMirrorEditor extends Editor {
39 xtextServices: IXtextServices;
40}
41
42export interface IXtextServices {
43}
diff --git a/language-web/src/main/js/xtext/xtext-codemirror.js b/language-web/src/main/js/xtext/xtext-codemirror.js
deleted file mode 100644
index d246172a..00000000
--- a/language-web/src/main/js/xtext/xtext-codemirror.js
+++ /dev/null
@@ -1,473 +0,0 @@
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 from: cursor,
283 to: cursor
284 };
285 }});
286 });
287 }};
288 this.editor.addKeyMap(services.contentAssistKeyMap);
289 }
290
291 /**
292 * Semantic highlighting service.
293 */
294 CodeMirrorServiceBuilder.prototype.doHighlighting = function() {
295 var services = this.services;
296 var editorContext = services.editorContext;
297 var editor = this.editor;
298 services.computeHighlighting().always(function() {
299 var highlightingMarkers = editorContext._highlightingMarkers;
300 if (highlightingMarkers) {
301 for (var i = 0; i < highlightingMarkers.length; i++) {
302 highlightingMarkers[i].clear();
303 }
304 }
305 editorContext._highlightingMarkers = [];
306 }).done(function(result) {
307 for (var i = 0; i < result.regions.length; ++i) {
308 var region = result.regions[i];
309 var from = editor.posFromIndex(region.offset);
310 var to = editor.posFromIndex(region.offset + region.length);
311 region.styleClasses.forEach(function(styleClass) {
312 var marker = editor.markText(from, to, {className: styleClass});
313 editorContext._highlightingMarkers.push(marker);
314 });
315 }
316 });
317 }
318
319 var annotationWeight = {
320 error: 30,
321 warning: 20,
322 info: 10
323 };
324 CodeMirrorServiceBuilder.prototype._getAnnotationWeight = function(annotation) {
325 if (annotationWeight[annotation] !== undefined)
326 return annotationWeight[annotation];
327 else
328 return 0;
329 }
330
331 CodeMirrorServiceBuilder.prototype._clearAnnotations = function(annotations) {
332 var editor = this.editor;
333 editor.clearGutter('annotations-gutter');
334 for (var i = 0; i < annotations.length; i++) {
335 var annotation = annotations[i];
336 if (annotation) {
337 annotations[i] = undefined;
338 }
339 }
340 }
341
342 CodeMirrorServiceBuilder.prototype._refreshAnnotations = function(annotations) {
343 var editor = this.editor;
344 for (var i = 0; i < annotations.length; i++) {
345 var annotation = annotations[i];
346 if (annotation) {
347 var classProp = ' class="xtext-annotation_' + annotation.type + '"';
348 var titleProp = annotation.description ? ' title="' + annotation.description.replace(/"/g, '&quot;') + '"' : '';
349 var element = jQuery('<div' + classProp + titleProp + '></div>').get(0);
350 editor.setGutterMarker(i, 'annotations-gutter', element);
351 }
352 }
353 }
354
355 /**
356 * Validation service.
357 */
358 CodeMirrorServiceBuilder.prototype.doValidation = function() {
359 var services = this.services;
360 var editorContext = services.editorContext;
361 var editor = this.editor;
362 var self = this;
363 services.validate().always(function() {
364 if (editorContext._validationAnnotations)
365 self._clearAnnotations(editorContext._validationAnnotations);
366 else
367 editorContext._validationAnnotations = [];
368 var validationMarkers = editorContext._validationMarkers;
369 if (validationMarkers) {
370 for (var i = 0; i < validationMarkers.length; i++) {
371 validationMarkers[i].clear();
372 }
373 }
374 editorContext._validationMarkers = [];
375 }).done(function(result) {
376 var validationAnnotations = editorContext._validationAnnotations;
377 for (var i = 0; i < result.issues.length; i++) {
378 var entry = result.issues[i];
379 var annotation = validationAnnotations[entry.line - 1];
380 var weight = self._getAnnotationWeight(entry.severity);
381 if (annotation) {
382 if (annotation.weight < weight) {
383 annotation.type = entry.severity;
384 annotation.weight = weight;
385 }
386 if (annotation.description)
387 annotation.description += '\n' + entry.description;
388 else
389 annotation.description = entry.description;
390 } else {
391 validationAnnotations[entry.line - 1] = {
392 type: entry.severity,
393 weight: weight,
394 description: entry.description
395 };
396 }
397 var from = editor.posFromIndex(entry.offset);
398 var to = editor.posFromIndex(entry.offset + entry.length);
399 var marker = editor.markText(from, to, {
400 className: 'xtext-marker_' + entry.severity,
401 title: entry.description
402 });
403 editorContext._validationMarkers.push(marker);
404 }
405 self._refreshAnnotations(validationAnnotations);
406 });
407 }
408
409 /**
410 * Occurrences service.
411 */
412 CodeMirrorServiceBuilder.prototype.setupOccurrencesService = function() {
413 var services = this.services;
414 var editorContext = services.editorContext;
415 var selectionUpdateDelay = services.options.selectionUpdateDelay;
416 if (!selectionUpdateDelay)
417 selectionUpdateDelay = 550;
418 var editor = this.editor;
419 var self = this;
420 services.cursorActivityListener = function() {
421 if (editorContext._selectionChangeTimeout) {
422 clearTimeout(editorContext._selectionChangeTimeout);
423 }
424 editorContext._selectionChangeTimeout = setTimeout(function() {
425 var params = ServiceBuilder.copy(services.options);
426 var cursor = editor.getCursor();
427 params.offset = editor.indexFromPos(cursor);
428 services.occurrencesService.invoke(editorContext, params).always(function() {
429 var occurrenceMarkers = editorContext._occurrenceMarkers;
430 if (occurrenceMarkers) {
431 for (var i = 0; i < occurrenceMarkers.length; i++)  {
432 occurrenceMarkers[i].clear();
433 }
434 }
435 editorContext._occurrenceMarkers = [];
436 }).done(function(occurrencesResult) {
437 for (var i = 0; i < occurrencesResult.readRegions.length; i++) {
438 var region = occurrencesResult.readRegions[i];
439 var from = editor.posFromIndex(region.offset);
440 var to = editor.posFromIndex(region.offset + region.length);
441 var marker = editor.markText(from, to, {className: 'xtext-marker_read'});
442 editorContext._occurrenceMarkers.push(marker);
443 }
444 for (var i = 0; i < occurrencesResult.writeRegions.length; i++) {
445 var region = occurrencesResult.writeRegions[i];
446 var from = editor.posFromIndex(region.offset);
447 var to = editor.posFromIndex(region.offset + region.length);
448 var marker = editor.markText(from, to, {className: 'xtext-marker_write'});
449 editorContext._occurrenceMarkers.push(marker);
450 }
451 });
452 }, selectionUpdateDelay);
453 }
454 editor.on('cursorActivity', services.cursorActivityListener);
455 }
456
457 /**
458 * Formatting service.
459 */
460 CodeMirrorServiceBuilder.prototype.setupFormattingService = function() {
461 var services = this.services;
462 if (services.options.enableFormattingAction) {
463 var userAgent = navigator.userAgent.toLowerCase();
464 var formatFunction = function(editor) {
465 services.format();
466 };
467 services.formatKeyMap = /mac os/.test(userAgent) ? {'Shift-Cmd-F': formatFunction}: {'Shift-Ctrl-S': formatFunction};
468 this.editor.addKeyMap(services.formatKeyMap);
469 }
470 }
471
472 return exports;
473});
diff --git a/language-web/src/main/js/xtext/xtextMessages.ts b/language-web/src/main/js/xtext/xtextMessages.ts
new file mode 100644
index 00000000..68737958
--- /dev/null
+++ b/language-web/src/main/js/xtext/xtextMessages.ts
@@ -0,0 +1,62 @@
1export interface IXtextWebRequest {
2 id: string;
3
4 request: unknown;
5}
6
7export interface IXtextWebOkResponse {
8 id: string;
9
10 response: unknown;
11}
12
13export function isOkResponse(response: unknown): response is IXtextWebOkResponse {
14 const okResponse = response as IXtextWebOkResponse;
15 return typeof okResponse === 'object'
16 && typeof okResponse.id === 'string'
17 && typeof okResponse.response !== 'undefined';
18}
19
20export const VALID_XTEXT_WEB_ERROR_KINDS = ['request', 'server'] as const;
21
22export type XtextWebErrorKind = typeof VALID_XTEXT_WEB_ERROR_KINDS[number];
23
24export function isXtextWebErrorKind(value: unknown): value is XtextWebErrorKind {
25 return typeof value === 'string'
26 && VALID_XTEXT_WEB_ERROR_KINDS.includes(value as XtextWebErrorKind);
27}
28
29export interface IXtextWebErrorResponse {
30 id: string;
31
32 error: XtextWebErrorKind;
33
34 message: string;
35}
36
37export function isErrorResponse(response: unknown): response is IXtextWebErrorResponse {
38 const errorResponse = response as IXtextWebErrorResponse;
39 return typeof errorResponse === 'object'
40 && typeof errorResponse.id === 'string'
41 && isXtextWebErrorKind(errorResponse.error)
42 && typeof errorResponse.message === 'string';
43}
44
45export interface IXtextWebPushMessage {
46 resource: string;
47
48 stateId: string;
49
50 service: string;
51
52 push: unknown;
53}
54
55export function isPushMessage(response: unknown): response is IXtextWebPushMessage {
56 const pushMessage = response as IXtextWebPushMessage;
57 return typeof pushMessage === 'object'
58 && typeof pushMessage.resource === 'string'
59 && typeof pushMessage.stateId === 'string'
60 && typeof pushMessage.service === 'string'
61 && typeof pushMessage.push !== 'undefined';
62}
diff --git a/language-web/src/main/js/xtext/xtextServiceResults.ts b/language-web/src/main/js/xtext/xtextServiceResults.ts
new file mode 100644
index 00000000..6c3d9daf
--- /dev/null
+++ b/language-web/src/main/js/xtext/xtextServiceResults.ts
@@ -0,0 +1,200 @@
1export interface IPongResult {
2 pong: string;
3}
4
5export function isPongResult(result: unknown): result is IPongResult {
6 const pongResult = result as IPongResult;
7 return typeof pongResult === 'object'
8 && typeof pongResult.pong === 'string';
9}
10
11export interface IDocumentStateResult {
12 stateId: string;
13}
14
15export function isDocumentStateResult(result: unknown): result is IDocumentStateResult {
16 const documentStateResult = result as IDocumentStateResult;
17 return typeof documentStateResult === 'object'
18 && typeof documentStateResult.stateId === 'string';
19}
20
21export const VALID_CONFLICTS = ['invalidStateId', 'canceled'] as const;
22
23export type Conflict = typeof VALID_CONFLICTS[number];
24
25export function isConflict(value: unknown): value is Conflict {
26 return typeof value === 'string' && VALID_CONFLICTS.includes(value as Conflict);
27}
28
29export interface IServiceConflictResult {
30 conflict: Conflict;
31}
32
33export function isServiceConflictResult(result: unknown): result is IServiceConflictResult {
34 const serviceConflictResult = result as IServiceConflictResult;
35 return typeof serviceConflictResult === 'object'
36 && isConflict(serviceConflictResult.conflict);
37}
38
39export function isInvalidStateIdConflictResult(result: unknown): boolean {
40 return isServiceConflictResult(result) && result.conflict === 'invalidStateId';
41}
42
43export const VALID_SEVERITIES = ['error', 'warning', 'info', 'ignore'] as const;
44
45export type Severity = typeof VALID_SEVERITIES[number];
46
47export function isSeverity(value: unknown): value is Severity {
48 return typeof value === 'string' && VALID_SEVERITIES.includes(value as Severity);
49}
50
51export interface IIssue {
52 description: string;
53
54 severity: Severity;
55
56 line: number;
57
58 column: number;
59
60 offset: number;
61
62 length: number;
63}
64
65export function isIssue(value: unknown): value is IIssue {
66 const issue = value as IIssue;
67 return typeof issue === 'object'
68 && typeof issue.description === 'string'
69 && isSeverity(issue.severity)
70 && typeof issue.line === 'number'
71 && typeof issue.column === 'number'
72 && typeof issue.offset === 'number'
73 && typeof issue.length === 'number';
74}
75
76export interface IValidationResult {
77 issues: IIssue[];
78}
79
80function isArrayOfType<T>(value: unknown, check: (entry: unknown) => entry is T): value is T[] {
81 return Array.isArray(value) && (value as T[]).every(check);
82}
83
84export function isValidationResult(result: unknown): result is IValidationResult {
85 const validationResult = result as IValidationResult;
86 return typeof validationResult === 'object'
87 && isArrayOfType(validationResult.issues, isIssue);
88}
89
90export interface IReplaceRegion {
91 offset: number;
92
93 length: number;
94
95 text: string;
96}
97
98export function isReplaceRegion(value: unknown): value is IReplaceRegion {
99 const replaceRegion = value as IReplaceRegion;
100 return typeof replaceRegion === 'object'
101 && typeof replaceRegion.offset === 'number'
102 && typeof replaceRegion.length === 'number'
103 && typeof replaceRegion.text === 'string';
104}
105
106export interface ITextRegion {
107 offset: number;
108
109 length: number;
110}
111
112export function isTextRegion(value: unknown): value is ITextRegion {
113 const textRegion = value as ITextRegion;
114 return typeof textRegion === 'object'
115 && typeof textRegion.offset === 'number'
116 && typeof textRegion.length === 'number';
117}
118
119export const VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS = [
120 'TEXT',
121 'METHOD',
122 'FUNCTION',
123 'CONSTRUCTOR',
124 'FIELD',
125 'VARIABLE',
126 'CLASS',
127 'INTERFACE',
128 'MODULE',
129 'PROPERTY',
130 'UNIT',
131 'VALUE',
132 'ENUM',
133 'KEYWORD',
134 'SNIPPET',
135 'COLOR',
136 'FILE',
137 'REFERENCE',
138 'UNKNOWN',
139] as const;
140
141export type XtextContentAssistEntryKind = typeof VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS[number];
142
143export function isXtextContentAssistEntryKind(
144 value: unknown,
145): value is XtextContentAssistEntryKind {
146 return typeof value === 'string'
147 && VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS.includes(value as XtextContentAssistEntryKind);
148}
149
150export interface IContentAssistEntry {
151 prefix: string;
152
153 proposal: string;
154
155 label?: string;
156
157 description?: string;
158
159 documentation?: string;
160
161 escapePosition?: number;
162
163 textReplacements: IReplaceRegion[];
164
165 editPositions: ITextRegion[];
166
167 kind: XtextContentAssistEntryKind | string;
168}
169
170function isStringOrUndefined(value: unknown): value is string | undefined {
171 return typeof value === 'string' || typeof value === 'undefined';
172}
173
174function isNumberOrUndefined(value: unknown): value is number | undefined {
175 return typeof value === 'number' || typeof value === 'undefined';
176}
177
178export function isContentAssistEntry(value: unknown): value is IContentAssistEntry {
179 const entry = value as IContentAssistEntry;
180 return typeof entry === 'object'
181 && typeof entry.prefix === 'string'
182 && typeof entry.proposal === 'string'
183 && isStringOrUndefined(entry.label)
184 && isStringOrUndefined(entry.description)
185 && isStringOrUndefined(entry.documentation)
186 && isNumberOrUndefined(entry.escapePosition)
187 && isArrayOfType(entry.textReplacements, isReplaceRegion)
188 && isArrayOfType(entry.editPositions, isTextRegion)
189 && typeof entry.kind === 'string';
190}
191
192export interface IContentAssistResult extends IDocumentStateResult {
193 entries: IContentAssistEntry[];
194}
195
196export function isContentAssistResult(result: unknown): result is IContentAssistResult {
197 const contentAssistResult = result as IContentAssistResult;
198 return isDocumentStateResult(result)
199 && isArrayOfType(contentAssistResult.entries, isContentAssistEntry);
200}