@adecore/editor-react 0.22.0 → 0.23.0
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.
- package/dist/CallHierarchyPanel.d.ts +6 -0
- package/dist/CallHierarchyPanel.js +88 -0
- package/dist/CompletionPopup.js +11 -3
- package/dist/DiffView.d.ts +22 -0
- package/dist/DiffView.js +203 -0
- package/dist/EditorContextMenu.js +1 -0
- package/dist/EditorView.js +18 -1
- package/dist/FindReplace.d.ts +1 -1
- package/dist/FindReplace.js +41 -11
- package/dist/HoverCard.js +2 -2
- package/dist/LanguagePopups.js +4 -1
- package/dist/Markdown.d.ts +2 -1
- package/dist/Markdown.js +8 -4
- package/dist/PeekPanel.d.ts +3 -0
- package/dist/PeekPanel.js +2 -3
- package/dist/PickPopup.js +15 -2
- package/dist/RenameCard.js +10 -5
- package/dist/RenderedDocs.d.ts +9 -0
- package/dist/RenderedDocs.js +84 -0
- package/dist/SplitDiffView.d.ts +9 -0
- package/dist/SplitDiffView.js +276 -0
- package/dist/SymbolPicker.d.ts +3 -0
- package/dist/SymbolPicker.js +4 -3
- package/dist/call-hierarchy-model.d.ts +52 -0
- package/dist/call-hierarchy-model.js +99 -0
- package/dist/call-hierarchy.d.ts +42 -0
- package/dist/call-hierarchy.js +399 -0
- package/dist/code-actions-model.d.ts +2 -0
- package/dist/code-actions-model.js +12 -10
- package/dist/code-actions.js +3 -1
- package/dist/completion-model.d.ts +14 -4
- package/dist/completion-model.js +40 -33
- package/dist/completion.d.ts +4 -0
- package/dist/completion.js +257 -42
- package/dist/diagnostics.js +8 -6
- package/dist/diff-expansion.d.ts +9 -0
- package/dist/diff-expansion.js +41 -0
- package/dist/diff-overlays.d.ts +15 -0
- package/dist/diff-overlays.js +28 -0
- package/dist/diff-syntax.d.ts +16 -0
- package/dist/diff-syntax.js +49 -0
- package/dist/diff-wordmarks.d.ts +5 -0
- package/dist/diff-wordmarks.js +26 -0
- package/dist/doc-comment-model.d.ts +23 -0
- package/dist/doc-comment-model.js +291 -0
- package/dist/editor-language.d.ts +13 -1
- package/dist/editor-language.js +14 -4
- package/dist/editor-react.css +79 -0
- package/dist/format-on-type.d.ts +14 -0
- package/dist/format-on-type.js +96 -0
- package/dist/host-types.d.ts +17 -0
- package/dist/hover.d.ts +4 -0
- package/dist/hover.js +15 -2
- package/dist/index.d.ts +19 -5
- package/dist/index.js +9 -1
- package/dist/inlay-hints.d.ts +47 -2
- package/dist/inlay-hints.js +353 -16
- package/dist/language-ids.js +4 -2
- package/dist/locales/en.json +114 -3
- package/dist/locales/nl.json +114 -3
- package/dist/models.d.ts +6 -2
- package/dist/models.js +5 -1
- package/dist/patch-decorations.d.ts +27 -0
- package/dist/patch-decorations.js +182 -0
- package/dist/patch-layout.d.ts +40 -0
- package/dist/patch-layout.js +304 -0
- package/dist/peek.js +1 -0
- package/dist/pick.d.ts +1 -0
- package/dist/pick.js +11 -1
- package/dist/popups.d.ts +17 -0
- package/dist/popups.js +1 -0
- package/dist/project-language.d.ts +8 -0
- package/dist/project-language.js +34 -2
- package/dist/refresher.d.ts +1 -0
- package/dist/refresher.js +5 -0
- package/dist/rename-model.d.ts +1 -1
- package/dist/rename-model.js +3 -7
- package/dist/rename.js +5 -8
- package/dist/rendered-docs.d.ts +62 -0
- package/dist/rendered-docs.js +368 -0
- package/dist/row-host.d.ts +2 -0
- package/dist/row-host.js +11 -3
- package/dist/semantic-delta.d.ts +2 -0
- package/dist/semantic-delta.js +27 -0
- package/dist/semantic-model.js +16 -0
- package/dist/semantic-tokens.d.ts +26 -1
- package/dist/semantic-tokens.js +174 -14
- package/dist/shortcut-keys.d.ts +2 -0
- package/dist/shortcut-keys.js +6 -2
- package/dist/signature.js +1 -1
- package/dist/snippet-field-transforms.d.ts +30 -0
- package/dist/snippet-field-transforms.js +116 -0
- package/dist/snippet-indent.d.ts +2 -0
- package/dist/snippet-indent.js +49 -0
- package/dist/snippet-range-order.d.ts +3 -0
- package/dist/snippet-range-order.js +19 -0
- package/dist/snippet-session.d.ts +42 -6
- package/dist/snippet-session.js +650 -53
- package/dist/snippet-transform-client.d.ts +19 -0
- package/dist/snippet-transform-client.js +66 -0
- package/dist/snippet-transform-format.d.ts +15 -0
- package/dist/snippet-transform-format.js +302 -0
- package/dist/snippet-transform-protocol.d.ts +45 -0
- package/dist/snippet-transform-protocol.js +8 -0
- package/dist/snippet-transform-queue.d.ts +32 -0
- package/dist/snippet-transform-queue.js +173 -0
- package/dist/snippet-transform-worker.d.ts +10 -0
- package/dist/snippet-transform-worker.js +41 -0
- package/dist/snippet-variable-names.d.ts +1 -0
- package/dist/snippet-variable-names.js +36 -0
- package/dist/snippet-variables.d.ts +7 -0
- package/dist/snippet-variables.js +66 -0
- package/dist/snippet.d.ts +30 -3
- package/dist/snippet.js +402 -87
- package/dist/split-alignment.d.ts +6 -0
- package/dist/split-alignment.js +38 -0
- package/dist/split-scroll.d.ts +14 -0
- package/dist/split-scroll.js +45 -0
- package/dist/symbols.d.ts +2 -1
- package/dist/symbols.js +39 -7
- package/dist/testing.d.ts +5 -1
- package/dist/testing.js +12 -0
- package/dist/workspace-edit.d.ts +25 -3
- package/dist/workspace-edit.js +282 -143
- package/package.json +10 -5
- package/src/CallHierarchyPanel.tsx +238 -0
- package/src/CompletionPopup.tsx +12 -1
- package/src/DiffView.tsx +262 -0
- package/src/EditorContextMenu.tsx +1 -0
- package/src/EditorView.tsx +21 -2
- package/src/FindReplace.tsx +51 -13
- package/src/HoverCard.tsx +23 -21
- package/src/LanguagePopups.tsx +5 -0
- package/src/Markdown.tsx +10 -3
- package/src/PeekPanel.tsx +2 -3
- package/src/PickPopup.tsx +18 -2
- package/src/RenameCard.tsx +18 -0
- package/src/RenderedDocs.tsx +155 -0
- package/src/SplitDiffView.tsx +337 -0
- package/src/SymbolPicker.tsx +4 -3
- package/src/call-hierarchy-model.ts +158 -0
- package/src/call-hierarchy.ts +441 -0
- package/src/code-actions-model.ts +17 -11
- package/src/code-actions.ts +3 -1
- package/src/completion-model.ts +61 -35
- package/src/completion.ts +302 -42
- package/src/diagnostics.ts +8 -6
- package/src/diff-expansion.ts +49 -0
- package/src/diff-overlays.tsx +69 -0
- package/src/diff-syntax.ts +67 -0
- package/src/diff-wordmarks.ts +31 -0
- package/src/doc-comment-model.ts +336 -0
- package/src/editor-language.ts +21 -5
- package/src/editor-react.css +79 -0
- package/src/format-on-type.ts +111 -0
- package/src/host-types.ts +19 -0
- package/src/hover.ts +18 -3
- package/src/index.ts +27 -4
- package/src/inlay-hints.ts +394 -18
- package/src/language-ids.ts +4 -2
- package/src/locales/en.json +114 -3
- package/src/locales/nl.json +114 -3
- package/src/models.ts +40 -1
- package/src/patch-decorations.ts +224 -0
- package/src/patch-layout.ts +388 -0
- package/src/peek.ts +1 -0
- package/src/pick.ts +12 -1
- package/src/popups.ts +24 -0
- package/src/project-language.ts +43 -2
- package/src/refresher.ts +6 -0
- package/src/rename-model.ts +4 -8
- package/src/rename.ts +5 -8
- package/src/rendered-docs.ts +422 -0
- package/src/row-host.ts +12 -3
- package/src/semantic-delta.ts +29 -0
- package/src/semantic-model.ts +17 -0
- package/src/semantic-tokens.ts +202 -19
- package/src/shortcut-keys.ts +6 -2
- package/src/signature.ts +1 -1
- package/src/snippet-field-transforms.ts +144 -0
- package/src/snippet-indent.ts +53 -0
- package/src/snippet-range-order.ts +19 -0
- package/src/snippet-session.ts +797 -56
- package/src/snippet-transform-client.ts +84 -0
- package/src/snippet-transform-format.ts +325 -0
- package/src/snippet-transform-protocol.ts +24 -0
- package/src/snippet-transform-queue.ts +199 -0
- package/src/snippet-transform-worker.ts +58 -0
- package/src/snippet-variable-names.ts +36 -0
- package/src/snippet-variables.ts +74 -0
- package/src/snippet.ts +452 -89
- package/src/split-alignment.ts +45 -0
- package/src/split-scroll.ts +54 -0
- package/src/symbols.ts +43 -7
- package/src/testing.ts +25 -0
- package/src/workspace-edit.ts +347 -169
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { findDocComments, type DocComment, type EditorPosition, type EditorRange, type EditorReplacement, type EditorTrackedRange } from '@adecore/editor';
|
|
2
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
3
|
+
import { parseDocComment, type ParsedDocComment } from './doc-comment-model.ts';
|
|
4
|
+
import { isShortcut } from './shortcut-keys.ts';
|
|
5
|
+
import { realTimers, type Timers } from './timers.ts';
|
|
6
|
+
|
|
7
|
+
const OWNER = 'rendered-docs';
|
|
8
|
+
const DOCS_PREFIX = 'docs-';
|
|
9
|
+
const SUMMARY_WORDS = 6;
|
|
10
|
+
const SETTLE_MS = 100;
|
|
11
|
+
/* What a row is taken to be tall until its content is in and measured. */
|
|
12
|
+
const ESTIMATED_LINE_HEIGHT = 20;
|
|
13
|
+
const MAX_ESTIMATED_LINES = 8;
|
|
14
|
+
const MAX_PARSED = 200;
|
|
15
|
+
|
|
16
|
+
export interface RenderedDocsSettings {
|
|
17
|
+
/* Draw every documentation comment as formatted text, as the Reader mode of the documentation does. Off by default. */
|
|
18
|
+
readonly renderDocComments: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* A documentation comment the editor drew a row for, and the element of that row. */
|
|
22
|
+
export interface RenderedDocEntry {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly comment: DocComment;
|
|
25
|
+
readonly parsed: ParsedDocComment;
|
|
26
|
+
readonly container: HTMLElement;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface Override {
|
|
30
|
+
readonly tracked: EditorTrackedRange;
|
|
31
|
+
readonly rendered: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function startOf(comment: DocComment): EditorPosition {
|
|
35
|
+
return { line: comment.startLine, character: comment.indent.length };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* A selection touches a comment unless it merely ends where the comment begins or swallows the comment whole. */
|
|
39
|
+
function touches(range: EditorRange, comment: DocComment): boolean {
|
|
40
|
+
if (range.end.line < comment.startLine || range.start.line > comment.endLine) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const empty = range.start.line === range.end.line && range.start.character === range.end.character;
|
|
44
|
+
if (!empty && range.end.line === comment.startLine && range.end.character === 0) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return empty || range.start.line >= comment.startLine || range.end.line <= comment.endLine;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* The first words of what a comment says, to tell its row from the others for a person who hears it. */
|
|
51
|
+
function summaryOf(body: string): string {
|
|
52
|
+
const lead = body.split('\n').findIndex((line) => /^\s*@/.test(line));
|
|
53
|
+
const text = (lead === -1 ? body : body.split('\n').slice(0, lead).join('\n')).replace(/[`*_#>[\]()]/g, ' ');
|
|
54
|
+
const words = text.split(/\s+/).filter((word) => word !== '');
|
|
55
|
+
return words.slice(0, SUMMARY_WORDS).join(' ');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function samePlace(range: EditorRange | null, position: EditorPosition): boolean {
|
|
59
|
+
return range !== null && range.start.line === position.line && range.start.character === position.character;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/*
|
|
63
|
+
* Documentation comments drawn as formatted text in place of their lines, the way the Reader mode of the
|
|
64
|
+
* documentation does, and the button in the gutter that switches one between that and its source. The
|
|
65
|
+
* text of the document never changes. A comment the caret or a selection touches is shown as source so
|
|
66
|
+
* it can be edited, and shown formatted again when the caret leaves it.
|
|
67
|
+
*/
|
|
68
|
+
export class RenderedDocsFeature {
|
|
69
|
+
private readonly language: EditorLanguage;
|
|
70
|
+
private readonly timers: Timers;
|
|
71
|
+
private settings: RenderedDocsSettings = { renderDocComments: false };
|
|
72
|
+
private comments: DocComment[] = [];
|
|
73
|
+
private overrides: Override[] = [];
|
|
74
|
+
private readonly containers = new Map<string, HTMLElement>();
|
|
75
|
+
private readonly markerLines = new Map<string, number>();
|
|
76
|
+
private readonly parsed = new Map<string, ParsedDocComment>();
|
|
77
|
+
private indexes = new Map<DocComment, number>();
|
|
78
|
+
/* Comments a reveal showed as source until the caret has settled where it was going, or the comment has scrolled out of view if the caret never moves. */
|
|
79
|
+
private readonly revealed = new Set<DocComment>();
|
|
80
|
+
private readonly listeners = new Set<() => void>();
|
|
81
|
+
private version = 0;
|
|
82
|
+
private settleTimer: unknown;
|
|
83
|
+
private caretTimer: unknown;
|
|
84
|
+
private drawn = '';
|
|
85
|
+
private marked = '';
|
|
86
|
+
/* The comments are from before the last edit, so nothing is decided from them until they are read again. */
|
|
87
|
+
private stale = false;
|
|
88
|
+
private disposed = false;
|
|
89
|
+
|
|
90
|
+
constructor(language: EditorLanguage, timers: Timers = realTimers) {
|
|
91
|
+
this.language = language;
|
|
92
|
+
this.timers = timers;
|
|
93
|
+
const { editor } = language;
|
|
94
|
+
const offs = [
|
|
95
|
+
editor.onTextChange(() => {
|
|
96
|
+
this.stale = true;
|
|
97
|
+
this.timers.clear(this.settleTimer);
|
|
98
|
+
this.settleTimer = this.timers.set(() => this.read(), SETTLE_MS);
|
|
99
|
+
}),
|
|
100
|
+
// The caret is told before the text change that moved it, so what the lines are is decided a moment later.
|
|
101
|
+
editor.onCaret(() => {
|
|
102
|
+
this.timers.clear(this.caretTimer);
|
|
103
|
+
this.caretTimer = this.timers.set(() => {
|
|
104
|
+
this.revealed.clear();
|
|
105
|
+
this.publish();
|
|
106
|
+
}, 0);
|
|
107
|
+
}),
|
|
108
|
+
editor.onGutterMarker((id) => {
|
|
109
|
+
const line = this.markerLines.get(id);
|
|
110
|
+
if (line !== undefined) {
|
|
111
|
+
this.toggleAt(line);
|
|
112
|
+
}
|
|
113
|
+
}),
|
|
114
|
+
editor.onKeyDown((event) => {
|
|
115
|
+
return isShortcut(this.language.shortcuts.toggleRenderedDocs, event) && this.toggle();
|
|
116
|
+
}),
|
|
117
|
+
editor.onViewChange(() => {
|
|
118
|
+
this.sweep();
|
|
119
|
+
this.releaseRevealed();
|
|
120
|
+
})
|
|
121
|
+
];
|
|
122
|
+
language.onDispose(() => {
|
|
123
|
+
this.disposed = true;
|
|
124
|
+
for (const off of offs) {
|
|
125
|
+
off();
|
|
126
|
+
}
|
|
127
|
+
this.timers.clear(this.settleTimer);
|
|
128
|
+
this.timers.clear(this.caretTimer);
|
|
129
|
+
for (const override of this.overrides) {
|
|
130
|
+
override.tracked.dispose();
|
|
131
|
+
}
|
|
132
|
+
this.overrides = [];
|
|
133
|
+
editor.setReplacements?.([], OWNER);
|
|
134
|
+
editor.setGutterMarkers([], OWNER);
|
|
135
|
+
this.containers.clear();
|
|
136
|
+
this.bump();
|
|
137
|
+
});
|
|
138
|
+
this.read();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Whether every documentation comment is drawn as text until a person switches it. Changing it puts every comment back under it. */
|
|
142
|
+
configure(settings: RenderedDocsSettings): void {
|
|
143
|
+
if (settings.renderDocComments === this.settings.renderDocComments) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.settings = settings;
|
|
147
|
+
this.clearOverrides();
|
|
148
|
+
this.publish();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* The documentation comments of the document, from the last time it settled. */
|
|
152
|
+
get all(): readonly DocComment[] {
|
|
153
|
+
return this.comments;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Whether a comment is drawn as text now: its own switch, else the setting. A comment the caret is in is not, whatever its switch says. */
|
|
157
|
+
isRendered(comment: DocComment): boolean {
|
|
158
|
+
return this.wantsRendered(comment) && !this.touched(comment) && !this.revealed.has(comment);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Switches the comment the caret is in between formatted and source. False when the caret is in none. */
|
|
162
|
+
toggle(): boolean {
|
|
163
|
+
this.settle();
|
|
164
|
+
const comment = this.at(this.language.editor.getCaret().line);
|
|
165
|
+
if (comment === undefined || !this.canRender(comment)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
this.setRendered(comment, !this.wantsRendered(comment));
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Switches the comment that starts on a line, which is what its gutter button does. */
|
|
173
|
+
toggleAt(startLine: number): boolean {
|
|
174
|
+
this.settle();
|
|
175
|
+
const comment = this.comments.find((candidate) => candidate.startLine === startLine);
|
|
176
|
+
if (comment === undefined || !this.canRender(comment)) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
this.setRendered(comment, !this.wantsRendered(comment));
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* The elements of the rows now in view, for a host that draws them itself; `RenderedDocs` does. */
|
|
184
|
+
entries(): readonly RenderedDocEntry[] {
|
|
185
|
+
const result: RenderedDocEntry[] = [];
|
|
186
|
+
for (const [id, container] of this.containers) {
|
|
187
|
+
const comment = this.comments[Number(id.slice(DOCS_PREFIX.length))];
|
|
188
|
+
if (comment !== undefined) {
|
|
189
|
+
result.push({ id, comment, parsed: this.parse(comment), container });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
getVersion = (): number => this.version;
|
|
196
|
+
|
|
197
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
198
|
+
this.listeners.add(listener);
|
|
199
|
+
return () => {
|
|
200
|
+
this.listeners.delete(listener);
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/* Whether names in `{@link}` and `@see` can be followed, which takes a server that finds symbols by name. */
|
|
205
|
+
get canFollow(): boolean {
|
|
206
|
+
const { project, uri } = this.language;
|
|
207
|
+
return project.service.supports('workspace/symbol', uri);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Goes to what a link in a comment names. */
|
|
211
|
+
follow(name: string, comment: DocComment): void {
|
|
212
|
+
const member = name.split(/::|[#.\\]/).filter((part) => part !== '');
|
|
213
|
+
const target = (member.at(-1) ?? name).replace(/\(\)$/, '');
|
|
214
|
+
void this.language.navigation.goToName(target, startOf(comment), null);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* By place in the document, so a row keeps its element when an edit above it moves its lines. */
|
|
218
|
+
private idOf(comment: DocComment): string {
|
|
219
|
+
return `${DOCS_PREFIX}${this.indexes.get(comment) ?? -1}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* A comment that is all the document has has no line to leave the caret on, so it stays source. */
|
|
223
|
+
private canRender(comment: DocComment): boolean {
|
|
224
|
+
return comment.startLine > 0 || comment.endLine < this.language.editor.positionAt(Number.MAX_SAFE_INTEGER).line;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private wantsRendered(comment: DocComment): boolean {
|
|
228
|
+
if (!this.canRender(comment)) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
const start = startOf(comment);
|
|
232
|
+
const override = this.overrides.find((candidate) => samePlace(candidate.tracked.get(), start));
|
|
233
|
+
return override?.rendered ?? this.settings.renderDocComments;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private at(line: number): DocComment | undefined {
|
|
237
|
+
return this.comments.find((comment) => line >= comment.startLine && line <= comment.endLine);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private touched(comment: DocComment): boolean {
|
|
241
|
+
return this.language.editor.getSelections().some((range) => touches(range, comment));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private setRendered(comment: DocComment, rendered: boolean): void {
|
|
245
|
+
const start = startOf(comment);
|
|
246
|
+
const existing = this.overrides.find((candidate) => samePlace(candidate.tracked.get(), start));
|
|
247
|
+
if (existing !== undefined) {
|
|
248
|
+
existing.tracked.dispose();
|
|
249
|
+
this.overrides = this.overrides.filter((candidate) => candidate !== existing);
|
|
250
|
+
}
|
|
251
|
+
if (rendered !== this.settings.renderDocComments) {
|
|
252
|
+
const { editor } = this.language;
|
|
253
|
+
this.overrides.push({ tracked: editor.trackRange({ start, end: editor.positionAt(comment.to) }), rendered });
|
|
254
|
+
}
|
|
255
|
+
if (rendered) {
|
|
256
|
+
this.leave(comment);
|
|
257
|
+
}
|
|
258
|
+
this.publish();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* Takes the carets and selections that touch a comment about to be drawn as text to the line under it, else the one above it. The others stay. */
|
|
262
|
+
private leave(comment: DocComment): void {
|
|
263
|
+
const { editor } = this.language;
|
|
264
|
+
const selections = editor.getSelections();
|
|
265
|
+
if (!selections.some((range) => touches(range, comment))) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const count = editor.positionAt(Number.MAX_SAFE_INTEGER).line + 1;
|
|
269
|
+
const line = comment.endLine + 1 < count ? comment.endLine + 1 : comment.startLine - 1;
|
|
270
|
+
if (line < 0) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const text = editor.textInRange({ start: { line, character: 0 }, end: { line, character: Number.MAX_SAFE_INTEGER } });
|
|
274
|
+
const character = line > comment.endLine ? (/^[ \t]*/.exec(text)?.[0].length ?? 0) : text.length;
|
|
275
|
+
const place = { line, character };
|
|
276
|
+
editor.setSelections(selections.map((range) => (touches(range, comment) ? { start: place, end: place } : range)));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* Shows a comment as source for the reveal that is about to scroll into it, since its row would only be in the way. */
|
|
280
|
+
private reveal(comment: DocComment): void {
|
|
281
|
+
this.revealed.add(comment);
|
|
282
|
+
this.publish();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private releaseRevealed(): void {
|
|
286
|
+
if (this.revealed.size === 0) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const { start, end } = this.language.editor.getVisibleRange();
|
|
290
|
+
const away = [...this.revealed].filter((comment) => comment.endLine < start.line || comment.startLine > end.line);
|
|
291
|
+
if (away.length > 0) {
|
|
292
|
+
away.forEach((comment) => this.revealed.delete(comment));
|
|
293
|
+
this.publish();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private clearOverrides(): void {
|
|
298
|
+
for (const override of this.overrides) {
|
|
299
|
+
override.tracked.dispose();
|
|
300
|
+
}
|
|
301
|
+
this.overrides = [];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/* Reads the comments now when an edit is waiting to be read. */
|
|
305
|
+
private settle(): void {
|
|
306
|
+
if (this.stale) {
|
|
307
|
+
this.timers.clear(this.settleTimer);
|
|
308
|
+
this.read();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private read(): void {
|
|
313
|
+
if (this.disposed) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
this.stale = false;
|
|
317
|
+
const { editor, languageId } = this.language;
|
|
318
|
+
this.comments = findDocComments(editor.getText(), languageId);
|
|
319
|
+
this.indexes = new Map(this.comments.map((comment, index) => [comment, index]));
|
|
320
|
+
this.revealed.clear();
|
|
321
|
+
const starts = this.comments.map(startOf);
|
|
322
|
+
this.overrides = this.overrides.filter((override) => {
|
|
323
|
+
const kept = starts.some((start) => samePlace(override.tracked.get(), start));
|
|
324
|
+
if (!kept) {
|
|
325
|
+
override.tracked.dispose();
|
|
326
|
+
}
|
|
327
|
+
return kept;
|
|
328
|
+
});
|
|
329
|
+
// An edit that replaced everything leaves the rows on offsets at the end, which only a new set of rows repairs.
|
|
330
|
+
this.publish(true);
|
|
331
|
+
this.bump();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private parse(comment: DocComment): ParsedDocComment {
|
|
335
|
+
const key = `${comment.body}`;
|
|
336
|
+
let result = this.parsed.get(key);
|
|
337
|
+
if (result === undefined) {
|
|
338
|
+
if (this.parsed.size >= MAX_PARSED) {
|
|
339
|
+
this.parsed.delete(this.parsed.keys().next().value!);
|
|
340
|
+
}
|
|
341
|
+
result = parseDocComment(comment.body, { php: /^php$/i.test(this.language.languageId) });
|
|
342
|
+
this.parsed.set(key, result);
|
|
343
|
+
}
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* Hands the editor the rows and the buttons, unless they are what it has already or `force` says to. */
|
|
348
|
+
private publish(force = false): void {
|
|
349
|
+
if (this.disposed || this.stale) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const { editor } = this.language;
|
|
353
|
+
const t = (key: string, options?: Record<string, string>): string => this.language.project.i18n.t(`editor:language.docs.${key}`, options);
|
|
354
|
+
const rendered = this.comments.filter((comment) => this.isRendered(comment));
|
|
355
|
+
const signature = rendered.map((comment) => `${this.idOf(comment)}@${comment.startLine}-${comment.endLine}`).join('|');
|
|
356
|
+
if (force || signature !== this.drawn) {
|
|
357
|
+
this.drawn = signature;
|
|
358
|
+
const lineHeight = Math.max(editor.getBaseLineHeight?.() ?? 0, ESTIMATED_LINE_HEIGHT);
|
|
359
|
+
const rows = rendered.map((comment): EditorReplacement => ({
|
|
360
|
+
id: this.idOf(comment),
|
|
361
|
+
startLine: comment.startLine,
|
|
362
|
+
endLine: comment.endLine,
|
|
363
|
+
height: Math.min(comment.endLine - comment.startLine + 1, MAX_ESTIMATED_LINES) * lineHeight,
|
|
364
|
+
label: this.labelOf(comment, t),
|
|
365
|
+
onReveal: () => this.reveal(comment),
|
|
366
|
+
render: (container) => this.mounted(this.idOf(comment), container)
|
|
367
|
+
}));
|
|
368
|
+
const kept = new Set(rows.map((row) => row.id));
|
|
369
|
+
for (const id of [...this.containers.keys()]) {
|
|
370
|
+
if (!kept.has(id)) {
|
|
371
|
+
this.containers.delete(id);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
editor.setReplacements?.(rows, OWNER);
|
|
375
|
+
this.bump();
|
|
376
|
+
}
|
|
377
|
+
this.markerLines.clear();
|
|
378
|
+
const markers = this.comments.map((comment) => {
|
|
379
|
+
const id = `docs:${comment.startLine}`;
|
|
380
|
+
this.markerLines.set(id, comment.startLine);
|
|
381
|
+
const on = this.wantsRendered(comment);
|
|
382
|
+
return { id, line: comment.startLine, label: t(on ? 'showSource' : 'render'), icon: 'book-open' as const, pressed: on };
|
|
383
|
+
});
|
|
384
|
+
const marking = markers.map((marker) => `${marker.line}${marker.pressed}`).join('|');
|
|
385
|
+
if (marking !== this.marked) {
|
|
386
|
+
this.marked = marking;
|
|
387
|
+
editor.setGutterMarkers(markers, OWNER);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private labelOf(comment: DocComment, t: (key: string, options?: Record<string, string>) => string): string {
|
|
392
|
+
const summary = summaryOf(comment.body);
|
|
393
|
+
return summary === '' ? t('label') : t('labelNamed', { summary });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private mounted(id: string, container: HTMLElement): void {
|
|
397
|
+
this.containers.set(id, container);
|
|
398
|
+
this.sweep(container);
|
|
399
|
+
this.bump();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* The editor drops the element of a row that scrolled out of view, and what React drew into it goes with it. The element just made is not in the page yet. */
|
|
403
|
+
private sweep(fresh?: HTMLElement): void {
|
|
404
|
+
let removed = false;
|
|
405
|
+
for (const [id, container] of this.containers) {
|
|
406
|
+
if (container !== fresh && !container.isConnected) {
|
|
407
|
+
this.containers.delete(id);
|
|
408
|
+
removed = true;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (removed && fresh === undefined) {
|
|
412
|
+
this.bump();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private bump(): void {
|
|
417
|
+
this.version++;
|
|
418
|
+
for (const listener of [...this.listeners]) {
|
|
419
|
+
listener();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
package/src/row-host.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface HostedRow {
|
|
|
6
6
|
line: number;
|
|
7
7
|
placement: 'above' | 'below';
|
|
8
8
|
height: number;
|
|
9
|
+
fixed?: boolean;
|
|
10
|
+
filler?: boolean;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export interface HostedAction {
|
|
@@ -76,7 +78,7 @@ abstract class ContainerHost {
|
|
|
76
78
|
*/
|
|
77
79
|
export class RowHost extends ContainerHost {
|
|
78
80
|
set(rows: readonly HostedRow[]): void {
|
|
79
|
-
const signature = rows.map((row) => `${row.id}@${row.line}${row.placement}`).join('|');
|
|
81
|
+
const signature = rows.map((row) => `${row.id}@${row.line}${row.placement}:${row.height}:${row.fixed}:${row.filler}`).join('|');
|
|
80
82
|
if (
|
|
81
83
|
!this.replace(
|
|
82
84
|
signature,
|
|
@@ -87,11 +89,18 @@ export class RowHost extends ContainerHost {
|
|
|
87
89
|
}
|
|
88
90
|
this.editor.setWidgets(
|
|
89
91
|
rows.map((row) => ({
|
|
90
|
-
id: row.id,
|
|
92
|
+
id: row.filler ? `${row.id}-filler` : row.id,
|
|
91
93
|
line: row.line,
|
|
92
94
|
placement: row.placement,
|
|
93
95
|
height: row.height,
|
|
94
|
-
|
|
96
|
+
fixed: row.fixed,
|
|
97
|
+
render: (container: HTMLElement) => {
|
|
98
|
+
if (row.filler) {
|
|
99
|
+
container.classList.add('adecore-editor-diff-filler');
|
|
100
|
+
} else {
|
|
101
|
+
this.mounted(row.id, container);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
95
104
|
})),
|
|
96
105
|
this.owner
|
|
97
106
|
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SemanticTokensDelta } from '@adecore/lsp';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* The data of a `semanticTokens/full/delta` answer applied to the data it was computed against, or null when
|
|
5
|
+
* the edits cannot be trusted: out of order, overlapping, outside the data, or leaving a length that is no
|
|
6
|
+
* whole number of tokens. The caller then asks for the full tokens instead. One pass over data and edits.
|
|
7
|
+
*/
|
|
8
|
+
export function applySemanticEdits(data: readonly number[], edits: SemanticTokensDelta['edits']): number[] | null {
|
|
9
|
+
const result: number[] = [];
|
|
10
|
+
let cursor = 0;
|
|
11
|
+
for (const edit of edits) {
|
|
12
|
+
const { start, deleteCount } = edit;
|
|
13
|
+
const inserted = edit.data ?? [];
|
|
14
|
+
if (!Number.isInteger(start) || !Number.isInteger(deleteCount) || start < cursor || deleteCount < 0 || start + deleteCount > data.length) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
for (let at = cursor; at < start; at++) {
|
|
18
|
+
result.push(data[at]!);
|
|
19
|
+
}
|
|
20
|
+
for (const value of inserted) {
|
|
21
|
+
result.push(value);
|
|
22
|
+
}
|
|
23
|
+
cursor = start + deleteCount;
|
|
24
|
+
}
|
|
25
|
+
for (let at = cursor; at < data.length; at++) {
|
|
26
|
+
result.push(data[at]!);
|
|
27
|
+
}
|
|
28
|
+
return result.length % 5 === 0 ? result : null;
|
|
29
|
+
}
|
package/src/semantic-model.ts
CHANGED
|
@@ -52,6 +52,18 @@ export function scopesOf(type: string, modifiers: ReadonlySet<string>, scoped =
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/*
|
|
56
|
+
* What a server sends for the language it reads inside a string (SQL in a PHP string). Outside a string the grammar
|
|
57
|
+
* colors these better, so they only apply where the grammar says it is a string.
|
|
58
|
+
*/
|
|
59
|
+
const EMBEDDED_SCOPES: Record<string, string[] | undefined> = {
|
|
60
|
+
keyword: ['keyword'],
|
|
61
|
+
string: ['string'],
|
|
62
|
+
number: ['constant.numeric'],
|
|
63
|
+
comment: ['comment'],
|
|
64
|
+
operator: ['keyword.operator']
|
|
65
|
+
};
|
|
66
|
+
|
|
55
67
|
/* The encoded data of `textDocument/semanticTokens` as one token per classified range, the ones the theme has nothing to say about left out. */
|
|
56
68
|
export function decodeSemanticTokens(tokens: SemanticTokens, legend: SemanticTokensLegend): EditorSemanticToken[] {
|
|
57
69
|
const result: EditorSemanticToken[] = [];
|
|
@@ -72,6 +84,11 @@ export function decodeSemanticTokens(tokens: SemanticTokens, legend: SemanticTok
|
|
|
72
84
|
const scopes = scopesOf(type, modifiers, scoped);
|
|
73
85
|
if (scopes !== null) {
|
|
74
86
|
result.push({ line, character, length: data[at + 2]!, scopes });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const embedded = EMBEDDED_SCOPES[type];
|
|
90
|
+
if (embedded !== undefined) {
|
|
91
|
+
result.push({ line, character, length: data[at + 2]!, scopes: embedded, within: 'string' });
|
|
75
92
|
}
|
|
76
93
|
}
|
|
77
94
|
return result;
|