@adecore/editor-react 0.44.1 → 0.44.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -67,6 +67,7 @@ export declare class CompletionFeature {
67
67
  private key;
68
68
  private commits;
69
69
  private edited;
70
+ private typedIntoShown;
70
71
  private caretMoved;
71
72
  private triggerCharacters;
72
73
  private beginRequest;
@@ -658,7 +658,7 @@ export class CompletionFeature {
658
658
  this.textRevision++;
659
659
  // The rows were given, so typing only filters them.
660
660
  if (this.shown && this.isOpen) {
661
- if (change.source === 'input' && change.changes.length === 1 && Array.from(change.changes[0].text).every(isIdentifierCharacter)) {
661
+ if (change.source === 'input' && change.changes.length === 1 && this.typedIntoShown(change.changes[0])) {
662
662
  this.refilter();
663
663
  } else {
664
664
  this.close();
@@ -702,6 +702,14 @@ export class CompletionFeature {
702
702
  this.close();
703
703
  }
704
704
  }
705
+ /* Whether a change put word characters into the typed prefix of the given rows; anything else leaves their ranges pointing at other text. */
706
+ typedIntoShown({
707
+ range,
708
+ text
709
+ }) {
710
+ const start = this.start;
711
+ return text !== '' && isEmptyRange(range) && start !== null && range.start.line === start.line && range.start.character >= start.character && Array.from(text).every(isIdentifierCharacter);
712
+ }
705
713
  caretMoved(position) {
706
714
  if (this.start === null) {
707
715
  return;
@@ -240,9 +240,9 @@ export class EditorLanguage {
240
240
  async prepareSave(reason = 1) {
241
241
  const {
242
242
  service
243
- } = this.project;
243
+ } = this;
244
244
  await service.willSave?.(this.uri, reason).catch(() => undefined);
245
- if (service.willSaveWaitUntil === undefined) {
245
+ if (this.disposed || service.willSaveWaitUntil === undefined) {
246
246
  return;
247
247
  }
248
248
  const controller = new AbortController();
@@ -251,7 +251,7 @@ export class EditorLanguage {
251
251
  const edits = await service.willSaveWaitUntil(this.uri, reason, {
252
252
  signal: controller.signal
253
253
  });
254
- if (!controller.signal.aborted && edits !== null && edits.length > 0) {
254
+ if (!this.disposed && !controller.signal.aborted && edits !== null && edits.length > 0) {
255
255
  this.editor.applyEdits(edits.map(edit => ({
256
256
  range: edit.range,
257
257
  text: edit.newText
package/dist/history.js CHANGED
@@ -82,7 +82,10 @@ export class HistoryFeature {
82
82
  project,
83
83
  uri
84
84
  } = this.language;
85
- project.edits.map(uri, change.changes);
85
+ // The places belong to the file and the other editors of it hear the same change, so one editor moves them.
86
+ if (project.holdsFirst(uri, editor)) {
87
+ project.edits.map(uri, change.changes);
88
+ }
86
89
  this.editing = true;
87
90
  queueMicrotask(() => {
88
91
  this.editing = false;
@@ -31,6 +31,7 @@ export declare class InlineCompletionFeature {
31
31
  accept(): boolean;
32
32
  cycle(step: 1 | -1): void;
33
33
  hide(): void;
34
+ private tab;
34
35
  private key;
35
36
  private edited;
36
37
  private moved;
@@ -62,7 +62,7 @@ export class InlineCompletionFeature {
62
62
  const {
63
63
  editor
64
64
  } = language;
65
- const offs = [language.onKey(event => this.key(event), KEY_PRIORITY.inlineCompletion), editor.onTextChange(change => this.edited(change)), editor.onCaret(() => queueMicrotask(() => this.moved()))];
65
+ const offs = [language.onKey(event => this.key(event), KEY_PRIORITY.inlineCompletion), language.onKey(event => this.tab(event), KEY_PRIORITY.inlineAccept), editor.onTextChange(change => this.edited(change)), editor.onCaret(() => queueMicrotask(() => this.moved()))];
66
66
  language.onDispose(() => {
67
67
  this.hide();
68
68
  for (const off of offs) {
@@ -139,6 +139,16 @@ export class InlineCompletionFeature {
139
139
  this.language.editor.setGhostText(null);
140
140
  }
141
141
  }
142
+ /* Tab takes the suggestion on screen before any feature that expands on Tab; without one it is left alone. */
143
+ tab(event) {
144
+ if (this.current === null || event.isComposing) {
145
+ return false;
146
+ }
147
+ if (event.key === 'Tab' && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
148
+ return this.accept();
149
+ }
150
+ return false;
151
+ }
142
152
  key(event) {
143
153
  if (isShortcut(this.language.shortcuts.suggestInline, event) && this.enabled) {
144
154
  this.trigger();
@@ -147,9 +157,6 @@ export class InlineCompletionFeature {
147
157
  if (this.current === null || event.isComposing) {
148
158
  return false;
149
159
  }
150
- if (event.key === 'Tab' && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
151
- return this.accept();
152
- }
153
160
  if (event.key === 'Escape') {
154
161
  this.hide();
155
162
  return true;
@@ -4,6 +4,7 @@ export declare const KEY_PRIORITY: {
4
4
  readonly diagnostics: 1500;
5
5
  readonly hover: 1400;
6
6
  readonly completion: 1300;
7
+ readonly inlineAccept: 1250;
7
8
  readonly snippets: 1200;
8
9
  readonly templates: 1150;
9
10
  readonly markup: 1100;
@@ -7,6 +7,8 @@ export const KEY_PRIORITY = {
7
7
  diagnostics: 1500,
8
8
  hover: 1400,
9
9
  completion: 1300,
10
+ // Tab only: a suggestion on screen is taken before a template or an abbreviation expands, so the spacing of a hundred bends here too.
11
+ inlineAccept: 1250,
10
12
  snippets: 1200,
11
13
  // Between snippets and markup, so the spacing of a hundred bends here: Tab has to reach the templates after a running snippet and before Emmet.
12
14
  templates: 1150,
@@ -57,6 +57,7 @@ export declare class ProjectLanguage {
57
57
  noteCaret(place: Place): void;
58
58
  jumpTo(place: Place): void;
59
59
  takeCaret(uri: string, line: number): EditorPosition | null;
60
+ holdsFirst(uri: string, editor: Editor): boolean;
60
61
  openEditors(): readonly {
61
62
  uri: string;
62
63
  editor: Editor;
@@ -278,6 +278,10 @@ export class ProjectLanguage {
278
278
  this.pendingCaret = null;
279
279
  return pending.position;
280
280
  }
281
+ /* Whether the editor is the first to hold the document, the one whose changes stand for the document's. */
282
+ holdsFirst(uri, editor) {
283
+ return this.holders.get(uri)?.editors[0] === editor;
284
+ }
281
285
  /* The editors that hold a document, one per uri in the order the documents were first opened. */
282
286
  openEditors() {
283
287
  return [...this.holders].flatMap(([uri, holder]) => holder.editors[0] === undefined ? [] : [{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/editor-react",
3
- "version": "0.44.1",
3
+ "version": "0.44.3",
4
4
  "type": "module",
5
5
  "description": "Language-enabled editors, feature coordination and React popups over injected language services.",
6
6
  "license": "FSL-1.1-MIT",
@@ -62,9 +62,9 @@
62
62
  "test": "bun test --conditions=source"
63
63
  },
64
64
  "dependencies": {
65
- "@adecore/editor": "0.44.1",
66
- "@adecore/lsp": "0.44.1",
67
- "@adecore/merge": "0.44.1",
65
+ "@adecore/editor": "0.44.3",
66
+ "@adecore/lsp": "0.44.3",
67
+ "@adecore/merge": "0.44.3",
68
68
  "emmet": "^2.4.11",
69
69
  "lucide-react": "^1.61.0",
70
70
  "react-markdown": "^10.1.0",
@@ -73,14 +73,14 @@
73
73
  "zustand": "^5.0.16"
74
74
  },
75
75
  "peerDependencies": {
76
- "@adecore/ui": "^0.44.1",
76
+ "@adecore/ui": "^0.44.3",
77
77
  "i18next": "^26.4.2",
78
78
  "react": "^19.3.0",
79
79
  "react-dom": "^19.3.0",
80
80
  "react-i18next": "^17.0.15"
81
81
  },
82
82
  "devDependencies": {
83
- "@adecore/ui": "0.44.1",
83
+ "@adecore/ui": "0.44.3",
84
84
  "@types/bun": "^1.4.4",
85
85
  "@types/react": "^19.3.0",
86
86
  "@types/react-dom": "^19.3.0",
package/src/completion.ts CHANGED
@@ -728,7 +728,7 @@ export class CompletionFeature {
728
728
 
729
729
  // The rows were given, so typing only filters them.
730
730
  if (this.shown && this.isOpen) {
731
- if (change.source === 'input' && change.changes.length === 1 && Array.from(change.changes[0]!.text).every(isIdentifierCharacter)) {
731
+ if (change.source === 'input' && change.changes.length === 1 && this.typedIntoShown(change.changes[0]!)) {
732
732
  this.refilter();
733
733
  } else {
734
734
  this.close();
@@ -768,6 +768,20 @@ export class CompletionFeature {
768
768
  }
769
769
  }
770
770
 
771
+ /* Whether a change put word characters into the typed prefix of the given rows; anything else leaves their ranges pointing at other text. */
772
+ private typedIntoShown({ range, text }: EditorContentChange): boolean {
773
+ const start = this.start;
774
+
775
+ return (
776
+ text !== '' &&
777
+ isEmptyRange(range) &&
778
+ start !== null &&
779
+ range.start.line === start.line &&
780
+ range.start.character >= start.character &&
781
+ Array.from(text).every(isIdentifierCharacter)
782
+ );
783
+ }
784
+
771
785
  private caretMoved(position: EditorPosition): void {
772
786
  if (this.start === null) {
773
787
  return;
@@ -249,16 +249,16 @@ export class EditorLanguage {
249
249
  * as the specification allows. Call it, then save, then let the service send `didSave`.
250
250
  */
251
251
  async prepareSave(reason: TextDocumentSaveReason = 1): Promise<void> {
252
- const { service } = this.project;
252
+ const { service } = this;
253
253
  await service.willSave?.(this.uri, reason).catch(() => undefined);
254
- if (service.willSaveWaitUntil === undefined) {
254
+ if (this.disposed || service.willSaveWaitUntil === undefined) {
255
255
  return;
256
256
  }
257
257
  const controller = new AbortController();
258
258
  const timer = this.timers.set(() => controller.abort(), SAVE_PARTICIPANT_MS);
259
259
  try {
260
260
  const edits = await service.willSaveWaitUntil(this.uri, reason, { signal: controller.signal });
261
- if (!controller.signal.aborted && edits !== null && edits.length > 0) {
261
+ if (!this.disposed && !controller.signal.aborted && edits !== null && edits.length > 0) {
262
262
  this.editor.applyEdits(edits.map((edit) => ({ range: edit.range, text: edit.newText })));
263
263
  }
264
264
  } catch {
package/src/history.ts CHANGED
@@ -98,7 +98,11 @@ export class HistoryFeature {
98
98
  private edited(change: EditorTextChange): void {
99
99
  const { editor, project, uri } = this.language;
100
100
 
101
- project.edits.map(uri, change.changes);
101
+ // The places belong to the file and the other editors of it hear the same change, so one editor moves them.
102
+ if (project.holdsFirst(uri, editor)) {
103
+ project.edits.map(uri, change.changes);
104
+ }
105
+
102
106
  this.editing = true;
103
107
 
104
108
  queueMicrotask(() => {
@@ -69,6 +69,7 @@ export class InlineCompletionFeature {
69
69
  const { editor } = language;
70
70
  const offs = [
71
71
  language.onKey((event) => this.key(event), KEY_PRIORITY.inlineCompletion),
72
+ language.onKey((event) => this.tab(event), KEY_PRIORITY.inlineAccept),
72
73
  editor.onTextChange((change) => this.edited(change)),
73
74
  editor.onCaret(() => queueMicrotask(() => this.moved()))
74
75
  ];
@@ -146,6 +147,17 @@ export class InlineCompletionFeature {
146
147
  }
147
148
  }
148
149
 
150
+ /* Tab takes the suggestion on screen before any feature that expands on Tab; without one it is left alone. */
151
+ private tab(event: KeyboardEvent): boolean {
152
+ if (this.current === null || event.isComposing) {
153
+ return false;
154
+ }
155
+ if (event.key === 'Tab' && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
156
+ return this.accept();
157
+ }
158
+ return false;
159
+ }
160
+
149
161
  private key(event: KeyboardEvent): boolean {
150
162
  if (isShortcut(this.language.shortcuts.suggestInline, event) && this.enabled) {
151
163
  this.trigger();
@@ -154,9 +166,6 @@ export class InlineCompletionFeature {
154
166
  if (this.current === null || event.isComposing) {
155
167
  return false;
156
168
  }
157
- if (event.key === 'Tab' && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
158
- return this.accept();
159
- }
160
169
  if (event.key === 'Escape') {
161
170
  this.hide();
162
171
  return true;
@@ -9,6 +9,8 @@ export const KEY_PRIORITY = {
9
9
  diagnostics: 1500,
10
10
  hover: 1400,
11
11
  completion: 1300,
12
+ // Tab only: a suggestion on screen is taken before a template or an abbreviation expands, so the spacing of a hundred bends here too.
13
+ inlineAccept: 1250,
12
14
  snippets: 1200,
13
15
  // Between snippets and markup, so the spacing of a hundred bends here: Tab has to reach the templates after a running snippet and before Emmet.
14
16
  templates: 1150,
@@ -323,6 +323,11 @@ export class ProjectLanguage {
323
323
  return pending.position;
324
324
  }
325
325
 
326
+ /* Whether the editor is the first to hold the document, the one whose changes stand for the document's. */
327
+ holdsFirst(uri: string, editor: Editor): boolean {
328
+ return this.holders.get(uri)?.editors[0] === editor;
329
+ }
330
+
326
331
  /* The editors that hold a document, one per uri in the order the documents were first opened. */
327
332
  openEditors(): readonly { uri: string; editor: Editor }[] {
328
333
  return [...this.holders].flatMap(([uri, holder]) => (holder.editors[0] === undefined ? [] : [{ uri, editor: holder.editors[0] }]));