diff options
Diffstat (limited to 'language-web/src/main')
-rw-r--r-- | language-web/src/main/js/editor/EditorButtons.tsx | 9 | ||||
-rw-r--r-- | language-web/src/main/js/editor/EditorStore.ts | 6 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/UpdateService.ts | 79 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/XtextClient.ts | 6 | ||||
-rw-r--r-- | language-web/src/main/js/xtext/xtextServiceResults.ts | 7 |
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'; | |||
7 | import CheckIcon from '@mui/icons-material/Check'; | 7 | import CheckIcon from '@mui/icons-material/Check'; |
8 | import ErrorIcon from '@mui/icons-material/Error'; | 8 | import ErrorIcon from '@mui/icons-material/Error'; |
9 | import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; | 9 | import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; |
10 | import FormatPaint from '@mui/icons-material/FormatPaint'; | ||
10 | import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; | 11 | import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; |
11 | import RedoIcon from '@mui/icons-material/Redo'; | 12 | import RedoIcon from '@mui/icons-material/Redo'; |
12 | import SearchIcon from '@mui/icons-material/Search'; | 13 | import 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 @@ | |||
1 | import { | 1 | import { |
2 | ChangeDesc, | 2 | ChangeDesc, |
3 | ChangeSet, | 3 | ChangeSet, |
4 | ChangeSpec, | ||
5 | StateEffect, | ||
4 | Transaction, | 6 | Transaction, |
5 | } from '@codemirror/state'; | 7 | } from '@codemirror/state'; |
6 | import { nanoid } from 'nanoid'; | 8 | import { 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 | ||
24 | const log = getLogger('xtext.UpdateService'); | 27 | const log = getLogger('xtext.UpdateService'); |
25 | 28 | ||
29 | const setDirtyChanges = StateEffect.define<ChangeSet>(); | ||
30 | |||
26 | export interface IAbortSignal { | 31 | export 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 | ||
105 | export type OccurrencesResult = z.infer<typeof occurrencesResult>; | 105 | export type OccurrencesResult = z.infer<typeof occurrencesResult>; |
106 | |||
107 | export const formattingResult = documentStateResult.extend({ | ||
108 | formattedText: z.string(), | ||
109 | replaceRegion: textRegion, | ||
110 | }); | ||
111 | |||
112 | export type FormattingResult = z.infer<typeof formattingResult>; | ||