diff options
Diffstat (limited to 'language-web/src/main/js/xtext')
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 | |||
10 | define([], 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 @@ | |||
1 | import type { | ||
2 | Completion, | ||
3 | CompletionContext, | ||
4 | CompletionResult, | ||
5 | } from '@codemirror/autocomplete'; | ||
6 | import type { ChangeSet, Transaction } from '@codemirror/state'; | ||
7 | |||
8 | import { getLogger } from '../logging'; | ||
9 | import type { UpdateService } from './UpdateService'; | ||
10 | |||
11 | const log = getLogger('xtext.ContentAssistService'); | ||
12 | |||
13 | export 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 | |||
10 | define([ | ||
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 @@ | |||
1 | import { | ||
2 | ChangeDesc, | ||
3 | ChangeSet, | ||
4 | Transaction, | ||
5 | } from '@codemirror/state'; | ||
6 | import { nanoid } from 'nanoid'; | ||
7 | |||
8 | import type { EditorStore } from '../editor/EditorStore'; | ||
9 | import { getLogger } from '../logging'; | ||
10 | import type { XtextWebSocketClient } from './XtextWebSocketClient'; | ||
11 | import { PendingTask } from '../utils/PendingTask'; | ||
12 | import { Timer } from '../utils/Timer'; | ||
13 | import { | ||
14 | IContentAssistEntry, | ||
15 | isContentAssistResult, | ||
16 | isDocumentStateResult, | ||
17 | isInvalidStateIdConflictResult, | ||
18 | } from './xtextServiceResults'; | ||
19 | |||
20 | const UPDATE_TIMEOUT_MS = 500; | ||
21 | |||
22 | const WAIT_FOR_UPDATE_TIMEOUT_MS = 1000; | ||
23 | |||
24 | const log = getLogger('xtext.UpdateService'); | ||
25 | |||
26 | export interface IAbortSignal { | ||
27 | aborted: boolean; | ||
28 | } | ||
29 | |||
30 | export 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 @@ | |||
1 | import type { Diagnostic } from '@codemirror/lint'; | ||
2 | |||
3 | import type { EditorStore } from '../editor/EditorStore'; | ||
4 | import { getLogger } from '../logging'; | ||
5 | import type { UpdateService } from './UpdateService'; | ||
6 | import { isValidationResult } from './xtextServiceResults'; | ||
7 | |||
8 | const log = getLogger('xtext.ValidationService'); | ||
9 | |||
10 | export 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 @@ | |||
1 | import type { | ||
2 | CompletionContext, | ||
3 | CompletionResult, | ||
4 | } from '@codemirror/autocomplete'; | ||
5 | import type { Transaction } from '@codemirror/state'; | ||
6 | |||
7 | import type { EditorStore } from '../editor/EditorStore'; | ||
8 | import { ContentAssistService } from './ContentAssistService'; | ||
9 | import { getLogger } from '../logging'; | ||
10 | import { UpdateService } from './UpdateService'; | ||
11 | import { ValidationService } from './ValidationService'; | ||
12 | import { XtextWebSocketClient } from './XtextWebSocketClient'; | ||
13 | |||
14 | const log = getLogger('xtext.XtextClient'); | ||
15 | |||
16 | export 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 @@ | |||
1 | import { nanoid } from 'nanoid'; | ||
2 | |||
3 | import { getLogger } from '../logging'; | ||
4 | import { PendingTask } from '../utils/PendingTask'; | ||
5 | import { Timer } from '../utils/Timer'; | ||
6 | import { | ||
7 | isErrorResponse, | ||
8 | isOkResponse, | ||
9 | isPushMessage, | ||
10 | IXtextWebRequest, | ||
11 | } from './xtextMessages'; | ||
12 | import { isPongResult } from './xtextServiceResults'; | ||
13 | |||
14 | const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1'; | ||
15 | |||
16 | const WEBSOCKET_CLOSE_OK = 1000; | ||
17 | |||
18 | const RECONNECT_DELAY_MS = [200, 1000, 5000, 30_000]; | ||
19 | |||
20 | const MAX_RECONNECT_DELAY_MS = RECONNECT_DELAY_MS[RECONNECT_DELAY_MS.length - 1]; | ||
21 | |||
22 | const BACKGROUND_IDLE_TIMEOUT_MS = 5 * 60 * 1000; | ||
23 | |||
24 | const PING_TIMEOUT_MS = 10 * 1000; | ||
25 | |||
26 | const REQUEST_TIMEOUT_MS = 1000; | ||
27 | |||
28 | const log = getLogger('XtextWebSocketClient'); | ||
29 | |||
30 | type ReconnectHandler = () => Promise<void>; | ||
31 | |||
32 | type PushHandler = ( | ||
33 | resourceId: string, | ||
34 | stateId: string, | ||
35 | service: string, | ||
36 | data: unknown, | ||
37 | ) => Promise<void>; | ||
38 | |||
39 | enum State { | ||
40 | Initial, | ||
41 | Opening, | ||
42 | TabVisible, | ||
43 | TabHiddenIdle, | ||
44 | TabHiddenWaiting, | ||
45 | Error, | ||
46 | TimedOut, | ||
47 | } | ||
48 | |||
49 | export 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 | |||
10 | define([], 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 | |||
10 | define(['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 | |||
10 | define(['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 | |||
10 | define(['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 | |||
10 | define(['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 | |||
10 | define(['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 | |||
10 | define(['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 | |||
10 | define(['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 | |||
10 | define(['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 | |||
10 | define(['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 | |||
10 | define(['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 @@ | |||
1 | import { Editor } from 'codemirror'; | ||
2 | |||
3 | export function createEditor(options: IXtextOptions): IXtextCodeMirrorEditor; | ||
4 | |||
5 | export function createServices(editor: Editor, options: IXtextOptions): IXtextServices; | ||
6 | |||
7 | export function removeServices(editor: Editor): void; | ||
8 | |||
9 | export 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 | |||
38 | export interface IXtextCodeMirrorEditor extends Editor { | ||
39 | xtextServices: IXtextServices; | ||
40 | } | ||
41 | |||
42 | export 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 | */ | ||
76 | define([ | ||
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, '"') + '"' : ''; | ||
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 @@ | |||
1 | export interface IXtextWebRequest { | ||
2 | id: string; | ||
3 | |||
4 | request: unknown; | ||
5 | } | ||
6 | |||
7 | export interface IXtextWebOkResponse { | ||
8 | id: string; | ||
9 | |||
10 | response: unknown; | ||
11 | } | ||
12 | |||
13 | export 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 | |||
20 | export const VALID_XTEXT_WEB_ERROR_KINDS = ['request', 'server'] as const; | ||
21 | |||
22 | export type XtextWebErrorKind = typeof VALID_XTEXT_WEB_ERROR_KINDS[number]; | ||
23 | |||
24 | export function isXtextWebErrorKind(value: unknown): value is XtextWebErrorKind { | ||
25 | return typeof value === 'string' | ||
26 | && VALID_XTEXT_WEB_ERROR_KINDS.includes(value as XtextWebErrorKind); | ||
27 | } | ||
28 | |||
29 | export interface IXtextWebErrorResponse { | ||
30 | id: string; | ||
31 | |||
32 | error: XtextWebErrorKind; | ||
33 | |||
34 | message: string; | ||
35 | } | ||
36 | |||
37 | export 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 | |||
45 | export interface IXtextWebPushMessage { | ||
46 | resource: string; | ||
47 | |||
48 | stateId: string; | ||
49 | |||
50 | service: string; | ||
51 | |||
52 | push: unknown; | ||
53 | } | ||
54 | |||
55 | export 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 @@ | |||
1 | export interface IPongResult { | ||
2 | pong: string; | ||
3 | } | ||
4 | |||
5 | export 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 | |||
11 | export interface IDocumentStateResult { | ||
12 | stateId: string; | ||
13 | } | ||
14 | |||
15 | export 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 | |||
21 | export const VALID_CONFLICTS = ['invalidStateId', 'canceled'] as const; | ||
22 | |||
23 | export type Conflict = typeof VALID_CONFLICTS[number]; | ||
24 | |||
25 | export function isConflict(value: unknown): value is Conflict { | ||
26 | return typeof value === 'string' && VALID_CONFLICTS.includes(value as Conflict); | ||
27 | } | ||
28 | |||
29 | export interface IServiceConflictResult { | ||
30 | conflict: Conflict; | ||
31 | } | ||
32 | |||
33 | export function isServiceConflictResult(result: unknown): result is IServiceConflictResult { | ||
34 | const serviceConflictResult = result as IServiceConflictResult; | ||
35 | return typeof serviceConflictResult === 'object' | ||
36 | && isConflict(serviceConflictResult.conflict); | ||
37 | } | ||
38 | |||
39 | export function isInvalidStateIdConflictResult(result: unknown): boolean { | ||
40 | return isServiceConflictResult(result) && result.conflict === 'invalidStateId'; | ||
41 | } | ||
42 | |||
43 | export const VALID_SEVERITIES = ['error', 'warning', 'info', 'ignore'] as const; | ||
44 | |||
45 | export type Severity = typeof VALID_SEVERITIES[number]; | ||
46 | |||
47 | export function isSeverity(value: unknown): value is Severity { | ||
48 | return typeof value === 'string' && VALID_SEVERITIES.includes(value as Severity); | ||
49 | } | ||
50 | |||
51 | export 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 | |||
65 | export 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 | |||
76 | export interface IValidationResult { | ||
77 | issues: IIssue[]; | ||
78 | } | ||
79 | |||
80 | function 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 | |||
84 | export 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 | |||
90 | export interface IReplaceRegion { | ||
91 | offset: number; | ||
92 | |||
93 | length: number; | ||
94 | |||
95 | text: string; | ||
96 | } | ||
97 | |||
98 | export 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 | |||
106 | export interface ITextRegion { | ||
107 | offset: number; | ||
108 | |||
109 | length: number; | ||
110 | } | ||
111 | |||
112 | export 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 | |||
119 | export 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 | |||
141 | export type XtextContentAssistEntryKind = typeof VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS[number]; | ||
142 | |||
143 | export 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 | |||
150 | export 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 | |||
170 | function isStringOrUndefined(value: unknown): value is string | undefined { | ||
171 | return typeof value === 'string' || typeof value === 'undefined'; | ||
172 | } | ||
173 | |||
174 | function isNumberOrUndefined(value: unknown): value is number | undefined { | ||
175 | return typeof value === 'number' || typeof value === 'undefined'; | ||
176 | } | ||
177 | |||
178 | export 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 | |||
192 | export interface IContentAssistResult extends IDocumentStateResult { | ||
193 | entries: IContentAssistEntry[]; | ||
194 | } | ||
195 | |||
196 | export function isContentAssistResult(result: unknown): result is IContentAssistResult { | ||
197 | const contentAssistResult = result as IContentAssistResult; | ||
198 | return isDocumentStateResult(result) | ||
199 | && isArrayOfType(contentAssistResult.entries, isContentAssistEntry); | ||
200 | } | ||