aboutsummaryrefslogtreecommitdiffstats
path: root/language-web
diff options
context:
space:
mode:
Diffstat (limited to 'language-web')
-rw-r--r--language-web/src/main/js/editor/EditorButtons.tsx9
-rw-r--r--language-web/src/main/js/editor/EditorStore.ts6
-rw-r--r--language-web/src/main/js/xtext/UpdateService.ts79
-rw-r--r--language-web/src/main/js/xtext/XtextClient.ts6
-rw-r--r--language-web/src/main/js/xtext/xtextServiceResults.ts7
5 files changed, 93 insertions, 14 deletions
diff --git a/language-web/src/main/js/editor/EditorButtons.tsx b/language-web/src/main/js/editor/EditorButtons.tsx
index 09ce33dd..150aa00d 100644
--- a/language-web/src/main/js/editor/EditorButtons.tsx
+++ b/language-web/src/main/js/editor/EditorButtons.tsx
@@ -7,6 +7,7 @@ import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
7import CheckIcon from '@mui/icons-material/Check'; 7import CheckIcon from '@mui/icons-material/Check';
8import ErrorIcon from '@mui/icons-material/Error'; 8import ErrorIcon from '@mui/icons-material/Error';
9import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; 9import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
10import FormatPaint from '@mui/icons-material/FormatPaint';
10import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; 11import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
11import RedoIcon from '@mui/icons-material/Redo'; 12import RedoIcon from '@mui/icons-material/Redo';
12import SearchIcon from '@mui/icons-material/Search'; 13import SearchIcon from '@mui/icons-material/Search';
@@ -47,7 +48,6 @@ export const EditorButtons = observer(() => {
47 disabled={!editorStore.canUndo} 48 disabled={!editorStore.canUndo}
48 onClick={() => editorStore.undo()} 49 onClick={() => editorStore.undo()}
49 aria-label="Undo" 50 aria-label="Undo"
50 value="undo"
51 > 51 >
52 <UndoIcon fontSize="small" /> 52 <UndoIcon fontSize="small" />
53 </IconButton> 53 </IconButton>
@@ -55,7 +55,6 @@ export const EditorButtons = observer(() => {
55 disabled={!editorStore.canRedo} 55 disabled={!editorStore.canRedo}
56 onClick={() => editorStore.redo()} 56 onClick={() => editorStore.redo()}
57 aria-label="Redo" 57 aria-label="Redo"
58 value="redo"
59 > 58 >
60 <RedoIcon fontSize="small" /> 59 <RedoIcon fontSize="small" />
61 </IconButton> 60 </IconButton>
@@ -88,6 +87,12 @@ export const EditorButtons = observer(() => {
88 {getLintIcon(editorStore.highestDiagnosticLevel)} 87 {getLintIcon(editorStore.highestDiagnosticLevel)}
89 </ToggleButton> 88 </ToggleButton>
90 </ToggleButtonGroup> 89 </ToggleButtonGroup>
90 <IconButton
91 onClick={() => editorStore.formatText()}
92 aria-label="Automatic format"
93 >
94 <FormatPaint fontSize="small" />
95 </IconButton>
91 </Stack> 96 </Stack>
92 ); 97 );
93}); 98});
diff --git a/language-web/src/main/js/editor/EditorStore.ts b/language-web/src/main/js/editor/EditorStore.ts
index 059233f4..5760de28 100644
--- a/language-web/src/main/js/editor/EditorStore.ts
+++ b/language-web/src/main/js/editor/EditorStore.ts
@@ -115,6 +115,7 @@ export class EditorStore {
115 lineNumbers(), 115 lineNumbers(),
116 foldGutter(), 116 foldGutter(),
117 keymap.of([ 117 keymap.of([
118 { key: 'Mod-Shift-f', run: () => this.formatText() },
118 ...closeBracketsKeymap, 119 ...closeBracketsKeymap,
119 ...commentKeymap, 120 ...commentKeymap,
120 ...completionKeymap, 121 ...completionKeymap,
@@ -280,4 +281,9 @@ export class EditorStore {
280 toggleLintPanel(): void { 281 toggleLintPanel(): void {
281 this.setLintPanelOpen(!this.showLintPanel); 282 this.setLintPanelOpen(!this.showLintPanel);
282 } 283 }
284
285 formatText(): boolean {
286 this.client.formatText();
287 return true;
288 }
283} 289}
diff --git a/language-web/src/main/js/xtext/UpdateService.ts b/language-web/src/main/js/xtext/UpdateService.ts
index fa48c5ab..e78944a9 100644
--- a/language-web/src/main/js/xtext/UpdateService.ts
+++ b/language-web/src/main/js/xtext/UpdateService.ts
@@ -1,6 +1,8 @@
1import { 1import {
2 ChangeDesc, 2 ChangeDesc,
3 ChangeSet, 3 ChangeSet,
4 ChangeSpec,
5 StateEffect,
4 Transaction, 6 Transaction,
5} from '@codemirror/state'; 7} from '@codemirror/state';
6import { nanoid } from 'nanoid'; 8import { nanoid } from 'nanoid';
@@ -14,6 +16,7 @@ import {
14 ContentAssistEntry, 16 ContentAssistEntry,
15 contentAssistResult, 17 contentAssistResult,
16 documentStateResult, 18 documentStateResult,
19 formattingResult,
17 isConflictResult, 20 isConflictResult,
18} from './xtextServiceResults'; 21} from './xtextServiceResults';
19 22
@@ -23,6 +26,8 @@ const WAIT_FOR_UPDATE_TIMEOUT_MS = 1000;
23 26
24const log = getLogger('xtext.UpdateService'); 27const log = getLogger('xtext.UpdateService');
25 28
29const setDirtyChanges = StateEffect.define<ChangeSet>();
30
26export interface IAbortSignal { 31export interface IAbortSignal {
27 aborted: boolean; 32 aborted: boolean;
28} 33}
@@ -38,12 +43,12 @@ export class UpdateService {
38 * The changes being synchronized to the server if a full or delta text update is running, 43 * The changes being synchronized to the server if a full or delta text update is running,
39 * `null` otherwise. 44 * `null` otherwise.
40 */ 45 */
41 private pendingUpdate: ChangeDesc | null = null; 46 private pendingUpdate: ChangeSet | null = null;
42 47
43 /** 48 /**
44 * Local changes not yet sychronized to the server and not part of the running update, if any. 49 * Local changes not yet sychronized to the server and not part of the running update, if any.
45 */ 50 */
46 private dirtyChanges: ChangeDesc; 51 private dirtyChanges: ChangeSet;
47 52
48 private readonly webSocketClient: XtextWebSocketClient; 53 private readonly webSocketClient: XtextWebSocketClient;
49 54
@@ -59,7 +64,7 @@ export class UpdateService {
59 constructor(store: EditorStore, webSocketClient: XtextWebSocketClient) { 64 constructor(store: EditorStore, webSocketClient: XtextWebSocketClient) {
60 this.resourceName = `${nanoid(7)}.problem`; 65 this.resourceName = `${nanoid(7)}.problem`;
61 this.store = store; 66 this.store = store;
62 this.dirtyChanges = this.newEmptyChangeDesc(); 67 this.dirtyChanges = this.newEmptyChangeSet();
63 this.webSocketClient = webSocketClient; 68 this.webSocketClient = webSocketClient;
64 } 69 }
65 70
@@ -71,8 +76,19 @@ export class UpdateService {
71 } 76 }
72 77
73 onTransaction(transaction: Transaction): void { 78 onTransaction(transaction: Transaction): void {
79 const setDirtyChangesEffect = transaction.effects.find(
80 (effect) => effect.is(setDirtyChanges),
81 ) as StateEffect<ChangeSet> | undefined;
82 if (setDirtyChangesEffect) {
83 const { value } = setDirtyChangesEffect;
84 if (this.pendingUpdate !== null) {
85 this.pendingUpdate = ChangeSet.empty(value.length);
86 }
87 this.dirtyChanges = value;
88 return;
89 }
74 if (transaction.docChanged) { 90 if (transaction.docChanged) {
75 this.dirtyChanges = this.dirtyChanges.composeDesc(transaction.changes.desc); 91 this.dirtyChanges = this.dirtyChanges.compose(transaction.changes);
76 this.idleUpdateTimer.reschedule(); 92 this.idleUpdateTimer.reschedule();
77 } 93 }
78 } 94 }
@@ -86,7 +102,7 @@ export class UpdateService {
86 * @return the summary of changes since the last update 102 * @return the summary of changes since the last update
87 */ 103 */
88 computeChangesSinceLastUpdate(): ChangeDesc { 104 computeChangesSinceLastUpdate(): ChangeDesc {
89 return this.pendingUpdate?.composeDesc(this.dirtyChanges) || this.dirtyChanges; 105 return this.pendingUpdate?.composeDesc(this.dirtyChanges.desc) || this.dirtyChanges.desc;
90 } 106 }
91 107
92 private handleIdleUpdate() { 108 private handleIdleUpdate() {
@@ -101,9 +117,8 @@ export class UpdateService {
101 this.idleUpdateTimer.reschedule(); 117 this.idleUpdateTimer.reschedule();
102 } 118 }
103 119
104 private newEmptyChangeDesc() { 120 private newEmptyChangeSet() {
105 const changeSet = ChangeSet.of([], this.store.state.doc.length); 121 return ChangeSet.of([], this.store.state.doc.length);
106 return changeSet.desc;
107 } 122 }
108 123
109 async updateFullText(): Promise<void> { 124 async updateFullText(): Promise<void> {
@@ -160,8 +175,8 @@ export class UpdateService {
160 } 175 }
161 log.warn('Delta update failed, performing full text update'); 176 log.warn('Delta update failed, performing full text update');
162 this.xtextStateId = null; 177 this.xtextStateId = null;
163 this.pendingUpdate = this.pendingUpdate.composeDesc(this.dirtyChanges); 178 this.pendingUpdate = this.pendingUpdate.compose(this.dirtyChanges);
164 this.dirtyChanges = this.newEmptyChangeDesc(); 179 this.dirtyChanges = this.newEmptyChangeSet();
165 return this.doUpdateFullText(); 180 return this.doUpdateFullText();
166 } 181 }
167 182
@@ -188,6 +203,7 @@ export class UpdateService {
188 return [stateId, resultEntries]; 203 return [stateId, resultEntries];
189 } 204 }
190 if (isConflictResult(result, 'invalidStateId')) { 205 if (isConflictResult(result, 'invalidStateId')) {
206 log.warn('Server state invalid during content assist');
191 const [newStateId] = await this.doFallbackToUpdateFullText(); 207 const [newStateId] = await this.doFallbackToUpdateFullText();
192 // We must finish this state update transaction to prepare for any push events 208 // We must finish this state update transaction to prepare for any push events
193 // before querying for content assist, so we just return `null` and will query 209 // before querying for content assist, so we just return `null` and will query
@@ -219,6 +235,31 @@ export class UpdateService {
219 return entries; 235 return entries;
220 } 236 }
221 237
238 async formatText(): Promise<void> {
239 await this.update();
240 let { from, to } = this.store.state.selection.main;
241 if (to <= from) {
242 from = 0;
243 to = this.store.state.doc.length;
244 }
245 log.debug('Formatting from', from, 'to', to);
246 await this.withUpdate(async () => {
247 const result = await this.webSocketClient.send({
248 resource: this.resourceName,
249 serviceType: 'format',
250 selectionStart: from,
251 selectionEnd: to,
252 });
253 const { stateId, formattedText } = formattingResult.parse(result);
254 this.applyBeforeDirtyChanges({
255 from,
256 to,
257 insert: formattedText,
258 });
259 return [stateId, null];
260 });
261 }
262
222 private computeDelta() { 263 private computeDelta() {
223 if (this.dirtyChanges.empty) { 264 if (this.dirtyChanges.empty) {
224 return null; 265 return null;
@@ -240,6 +281,20 @@ export class UpdateService {
240 }; 281 };
241 } 282 }
242 283
284 private applyBeforeDirtyChanges(changeSpec: ChangeSpec) {
285 const pendingChanges = this.pendingUpdate?.compose(this.dirtyChanges) || this.dirtyChanges;
286 const revertChanges = pendingChanges.invert(this.store.state.doc);
287 const applyBefore = ChangeSet.of(changeSpec, revertChanges.newLength);
288 const redoChanges = pendingChanges.map(applyBefore.desc);
289 const changeSet = revertChanges.compose(applyBefore).compose(redoChanges);
290 this.store.dispatch({
291 changes: changeSet,
292 effects: [
293 setDirtyChanges.of(redoChanges),
294 ],
295 });
296 }
297
243 /** 298 /**
244 * Executes an asynchronous callback that updates the state on the server. 299 * Executes an asynchronous callback that updates the state on the server.
245 * 300 *
@@ -266,7 +321,7 @@ export class UpdateService {
266 throw new Error('Another update is pending, will not perform update'); 321 throw new Error('Another update is pending, will not perform update');
267 } 322 }
268 this.pendingUpdate = this.dirtyChanges; 323 this.pendingUpdate = this.dirtyChanges;
269 this.dirtyChanges = this.newEmptyChangeDesc(); 324 this.dirtyChanges = this.newEmptyChangeSet();
270 let newStateId: string | null = null; 325 let newStateId: string | null = null;
271 try { 326 try {
272 let result: T; 327 let result: T;
@@ -280,7 +335,7 @@ export class UpdateService {
280 if (this.pendingUpdate === null) { 335 if (this.pendingUpdate === null) {
281 log.error('pendingUpdate was cleared during update'); 336 log.error('pendingUpdate was cleared during update');
282 } else { 337 } else {
283 this.dirtyChanges = this.pendingUpdate.composeDesc(this.dirtyChanges); 338 this.dirtyChanges = this.pendingUpdate.compose(this.dirtyChanges);
284 } 339 }
285 this.pendingUpdate = null; 340 this.pendingUpdate = null;
286 this.webSocketClient.forceReconnectOnError(); 341 this.webSocketClient.forceReconnectOnError();
diff --git a/language-web/src/main/js/xtext/XtextClient.ts b/language-web/src/main/js/xtext/XtextClient.ts
index 3922b230..0898e725 100644
--- a/language-web/src/main/js/xtext/XtextClient.ts
+++ b/language-web/src/main/js/xtext/XtextClient.ts
@@ -77,4 +77,10 @@ export class XtextClient {
77 contentAssist(context: CompletionContext): Promise<CompletionResult> { 77 contentAssist(context: CompletionContext): Promise<CompletionResult> {
78 return this.contentAssistService.contentAssist(context); 78 return this.contentAssistService.contentAssist(context);
79 } 79 }
80
81 formatText(): void {
82 this.updateService.formatText().catch((e) => {
83 log.error('Error while formatting text', e);
84 });
85 }
80} 86}
diff --git a/language-web/src/main/js/xtext/xtextServiceResults.ts b/language-web/src/main/js/xtext/xtextServiceResults.ts
index b6867e2f..f79b059c 100644
--- a/language-web/src/main/js/xtext/xtextServiceResults.ts
+++ b/language-web/src/main/js/xtext/xtextServiceResults.ts
@@ -103,3 +103,10 @@ export const occurrencesResult = documentStateResult.extend({
103}); 103});
104 104
105export type OccurrencesResult = z.infer<typeof occurrencesResult>; 105export type OccurrencesResult = z.infer<typeof occurrencesResult>;
106
107export const formattingResult = documentStateResult.extend({
108 formattedText: z.string(),
109 replaceRegion: textRegion,
110});
111
112export type FormattingResult = z.infer<typeof formattingResult>;