@adecore/editor-core 0.22.1 → 0.23.1
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/comments.js +67 -14
- package/dist/doc-comments.d.ts +10 -0
- package/dist/doc-comments.js +85 -0
- package/dist/document.d.ts +8 -0
- package/dist/document.js +111 -29
- package/dist/edit-regions.d.ts +6 -0
- package/dist/edit-regions.js +87 -0
- package/dist/enter.js +24 -12
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/languages.d.ts +2 -1
- package/dist/languages.js +7 -1
- package/dist/markup-regions.d.ts +1 -0
- package/dist/markup-regions.js +53 -1
- package/dist/offsets.d.ts +1 -0
- package/dist/offsets.js +23 -0
- package/dist/php-markup.d.ts +7 -0
- package/dist/php-markup.js +99 -0
- package/dist/rope.d.ts +6 -0
- package/dist/rope.js +33 -4
- package/dist/script-source.d.ts +2 -0
- package/dist/script-source.js +44 -0
- package/dist/search.d.ts +3 -1
- package/dist/search.js +67 -12
- package/dist/types.d.ts +3 -0
- package/dist/typing-context.d.ts +8 -0
- package/dist/typing-context.js +70 -22
- package/package.json +1 -1
- package/src/comments.ts +70 -15
- package/src/doc-comments.ts +104 -0
- package/src/document.ts +131 -31
- package/src/edit-regions.ts +96 -0
- package/src/enter.ts +25 -13
- package/src/index.ts +2 -1
- package/src/languages.ts +9 -2
- package/src/markup-regions.ts +54 -1
- package/src/offsets.ts +24 -0
- package/src/php-markup.ts +104 -0
- package/src/rope.ts +35 -5
- package/src/script-source.ts +43 -0
- package/src/search.ts +69 -12
- package/src/types.ts +6 -0
- package/src/typing-context.ts +85 -22
package/dist/types.d.ts
CHANGED
|
@@ -34,11 +34,14 @@ export interface EditorSnapshot {
|
|
|
34
34
|
changes?: readonly (readonly DocumentChange[])[];
|
|
35
35
|
contentEdits?: readonly ContentEdit[];
|
|
36
36
|
source?: ChangeSource;
|
|
37
|
+
typed?: boolean;
|
|
37
38
|
}
|
|
38
39
|
export interface EditOptions {
|
|
39
40
|
selections?: readonly Selection[];
|
|
40
41
|
source?: ChangeSource;
|
|
41
42
|
historyGroup?: string;
|
|
43
|
+
joinPrevious?: boolean;
|
|
44
|
+
typed?: boolean;
|
|
42
45
|
}
|
|
43
46
|
export type EditorCommand = 'undo' | 'redo' | 'selectAll' | 'smartHome' | 'smartEnd' | 'selectSmartHome' | 'selectSmartEnd' | 'wordLeft' | 'wordRight' | 'selectWordLeft' | 'selectWordRight' | 'deleteWordLeft' | 'deleteWordRight' | 'camelLeft' | 'camelRight' | 'selectCamelLeft' | 'selectCamelRight' | 'deleteCamelLeft' | 'deleteCamelRight' | 'expandSelection' | 'shrinkSelection' | 'smartBackspace' | 'deleteForward' | 'duplicateLine' | 'deleteLine' | 'moveLineUp' | 'moveLineDown' | 'toggleLineComment' | 'toggleBlockComment' | 'insertTab' | 'indent' | 'outdent' | 'insertNewline' | 'startNewLine' | 'startNewLineBefore' | 'splitLine' | 'joinLines' | 'toggleCase' | 'autoIndentLines' | 'addCaretAbove' | 'addCaretBelow' | 'selectNextOccurrence' | 'unselectOccurrence' | 'selectAllOccurrences' | 'addCaretPerSelectedLine';
|
|
44
47
|
export interface CommandOptions {
|
package/dist/typing-context.d.ts
CHANGED
|
@@ -17,11 +17,19 @@ export interface TypingContext {
|
|
|
17
17
|
expression: boolean;
|
|
18
18
|
word: string;
|
|
19
19
|
commentStart?: number;
|
|
20
|
+
markup?: 'tag' | 'equals' | 'value' | 'comment' | 'raw';
|
|
21
|
+
markupQuote?: '"' | "'";
|
|
22
|
+
element?: 'script' | 'style' | 'textarea' | 'title';
|
|
23
|
+
embed?: TypingContext;
|
|
24
|
+
embedded?: 'javascript';
|
|
25
|
+
embedStart?: number;
|
|
26
|
+
phpClose?: number;
|
|
20
27
|
}
|
|
21
28
|
export declare class TypingContexts {
|
|
22
29
|
private readonly line;
|
|
23
30
|
private states;
|
|
24
31
|
private language;
|
|
32
|
+
private decidingLine;
|
|
25
33
|
constructor(line: (index: number) => DocumentLine);
|
|
26
34
|
invalidate(line?: number): void;
|
|
27
35
|
private initialState;
|
package/dist/typing-context.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { hasBlockComments, hasDashComments, hasHashComments, hashNeedsWordStart, hasMarkupComments, hasSlashComments, isPhp, isScript, isWordCharacter, openers, startsExpression, startsExpressionAfter, startsWord } from './lexical.js';
|
|
2
|
+
import { scanPhpMarkup } from './php-markup.js';
|
|
2
3
|
const PHP_HEREDOC = /^<<<[ \t]*(["']?)([A-Za-z_]\w*)\1[ \t]*$/;
|
|
3
4
|
const PHP_OPEN = /<\?(?:php\b|=)/;
|
|
5
|
+
const PHP_INITIAL_LINES = 64;
|
|
6
|
+
const PHP_INITIAL_CHARACTERS = 64 * 1024;
|
|
7
|
+
// Quoted PHP tags within a bare code fragment must not turn its prelude into output.
|
|
8
|
+
const PHP_FRAGMENT_START = /^(?:[$'"#]|\/[/*]|(?:abstract|class|const|declare|echo|enum|final|for|foreach|function|if|include(?:_once)?|interface|namespace|print|readonly|require(?:_once)?|return|switch|throw|trait|try|use|while)\b|[A-Za-z_]\w*\s*(?:\(|::))/;
|
|
4
9
|
const controlWords = /^(for|for-await|if|while|switch|catch|with)$/;
|
|
5
10
|
const blockWords = /^(else|try|finally|do)$/;
|
|
6
11
|
function initial() {
|
|
@@ -93,6 +98,7 @@ function scanCode(state, rules, text, at, end, offset) {
|
|
|
93
98
|
}
|
|
94
99
|
}
|
|
95
100
|
if (rules.php && char === '?' && next === '>') {
|
|
101
|
+
state.phpClose = offset + at + 2;
|
|
96
102
|
state.mode = 'html';
|
|
97
103
|
return at + 1;
|
|
98
104
|
}
|
|
@@ -157,9 +163,30 @@ function closeBracket(state, char) {
|
|
|
157
163
|
state.expression = Boolean(bracket.control || bracket.block);
|
|
158
164
|
state.word = bracket.control ? 'control-close' : '';
|
|
159
165
|
}
|
|
166
|
+
let scriptRules;
|
|
167
|
+
function stepScript(state, text, at, end, offset) {
|
|
168
|
+
if (state.mode === 'line-comment') {
|
|
169
|
+
return at;
|
|
170
|
+
}
|
|
171
|
+
if (state.mode === 'block-comment') {
|
|
172
|
+
return scanComment(state, text, at, end);
|
|
173
|
+
}
|
|
174
|
+
return state.mode === 'code' ? scanCode(state, (scriptRules ??= rulesOf('javascript')), text, at, end, offset) : scanLiteral(state, text, at, end, offset);
|
|
175
|
+
}
|
|
176
|
+
const scriptLexer = { initial, step: stepScript };
|
|
177
|
+
function normalizeLine(state, multilineQuotes) {
|
|
178
|
+
if (state.mode === 'line-comment' || state.mode === 'regex' || (state.mode === 'quote' && !multilineQuotes && !state.escaped)) {
|
|
179
|
+
state.mode = 'code';
|
|
180
|
+
state.quote = undefined;
|
|
181
|
+
state.commentStart = undefined;
|
|
182
|
+
state.expression = true;
|
|
183
|
+
}
|
|
184
|
+
state.escaped = false;
|
|
185
|
+
}
|
|
160
186
|
/* Lexes `text` up to `end`, from the state the line started in. `offset` is where the line starts in the document. */
|
|
161
187
|
function scan(text, end, start, rules, offset) {
|
|
162
|
-
|
|
188
|
+
// An embedded lexer must not mutate the script state cached at the line start.
|
|
189
|
+
const state = { ...start, embed: start.embed === undefined ? undefined : { ...start.embed } };
|
|
163
190
|
for (let at = 0; at < end; at++) {
|
|
164
191
|
if (state.mode === 'line-comment') {
|
|
165
192
|
// A PHP line comment ends at its line break and at a closing tag.
|
|
@@ -167,19 +194,13 @@ function scan(text, end, start, rules, offset) {
|
|
|
167
194
|
if (tag < 0 || tag >= end) {
|
|
168
195
|
break;
|
|
169
196
|
}
|
|
197
|
+
state.phpClose = offset + tag + 2;
|
|
170
198
|
state.mode = 'html';
|
|
171
199
|
state.commentStart = undefined;
|
|
172
200
|
at = tag + 1;
|
|
173
201
|
}
|
|
174
202
|
else if (state.mode === 'html') {
|
|
175
|
-
|
|
176
|
-
if (open === null || at + open.index >= end) {
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
state.mode = 'code';
|
|
180
|
-
state.expression = true;
|
|
181
|
-
state.word = '';
|
|
182
|
-
at += open.index + open[0].length - 1;
|
|
203
|
+
at = scanPhpMarkup(state, text, at, end, offset, scriptLexer);
|
|
183
204
|
}
|
|
184
205
|
else if (state.mode === 'heredoc') {
|
|
185
206
|
const closing = new RegExp(`^[\\t ]*${state.quote}(?![\\w])`).exec(text);
|
|
@@ -211,27 +232,52 @@ function sameState(left, right) {
|
|
|
211
232
|
left.bracket === right.bracket &&
|
|
212
233
|
left.expression === right.expression &&
|
|
213
234
|
left.word === right.word &&
|
|
214
|
-
left.commentStart === right.commentStart
|
|
235
|
+
left.commentStart === right.commentStart &&
|
|
236
|
+
left.markup === right.markup &&
|
|
237
|
+
left.markupQuote === right.markupQuote &&
|
|
238
|
+
left.element === right.element &&
|
|
239
|
+
left.phpClose === right.phpClose &&
|
|
240
|
+
left.embedStart === right.embedStart &&
|
|
241
|
+
(left.embed === right.embed || (left.embed !== undefined && right.embed !== undefined && sameState(left.embed, right.embed))));
|
|
215
242
|
}
|
|
216
243
|
/* The state of the lexer at the start of each line, computed on demand. An edit drops the lines from its own on. */
|
|
217
244
|
export class TypingContexts {
|
|
218
245
|
line;
|
|
219
246
|
states = [initial()];
|
|
220
247
|
language = '';
|
|
248
|
+
decidingLine = 0;
|
|
221
249
|
constructor(line) {
|
|
222
250
|
this.line = line;
|
|
223
251
|
}
|
|
224
252
|
invalidate(line = 0) {
|
|
225
253
|
this.states.length = Math.min(this.states.length, Math.max(1, line + 1));
|
|
226
|
-
if (line
|
|
254
|
+
if (line <= this.decidingLine) {
|
|
227
255
|
this.language = '';
|
|
228
256
|
}
|
|
229
257
|
}
|
|
230
|
-
/* A PHP file that opens with markup, and not with a tag, starts outside the PHP. */
|
|
231
258
|
initialState(language) {
|
|
232
259
|
const state = initial();
|
|
233
|
-
|
|
234
|
-
|
|
260
|
+
this.decidingLine = 0;
|
|
261
|
+
if (language !== 'php') {
|
|
262
|
+
return state;
|
|
263
|
+
}
|
|
264
|
+
let remaining = PHP_INITIAL_CHARACTERS;
|
|
265
|
+
for (let line = 0; line < PHP_INITIAL_LINES && remaining > 0; line++) {
|
|
266
|
+
this.decidingLine = line;
|
|
267
|
+
const source = this.line(line);
|
|
268
|
+
const sample = source.text.slice(0, remaining);
|
|
269
|
+
remaining -= sample.length;
|
|
270
|
+
const text = sample.trimStart();
|
|
271
|
+
if (text !== '') {
|
|
272
|
+
const open = PHP_OPEN.exec(text);
|
|
273
|
+
if (/^<(?![?<])/.test(text) || (open !== null && open.index > 0 && !PHP_FRAGMENT_START.test(text))) {
|
|
274
|
+
state.mode = 'html';
|
|
275
|
+
}
|
|
276
|
+
return state;
|
|
277
|
+
}
|
|
278
|
+
if (source.end === source.next) {
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
235
281
|
}
|
|
236
282
|
return state;
|
|
237
283
|
}
|
|
@@ -246,17 +292,19 @@ export class TypingContexts {
|
|
|
246
292
|
const previous = this.states.at(-1);
|
|
247
293
|
const source = this.line(this.states.length - 1);
|
|
248
294
|
const next = scan(source.text, source.text.length, previous, rules, source.start);
|
|
249
|
-
//
|
|
250
|
-
if (next.mode === '
|
|
251
|
-
next.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
295
|
+
// A PHP island's source newlines are not newlines in the emitted script.
|
|
296
|
+
if (next.mode === 'html' && next.embed !== undefined && next.phpClose !== source.end) {
|
|
297
|
+
normalizeLine(next.embed, false);
|
|
298
|
+
}
|
|
299
|
+
next.phpClose = undefined;
|
|
300
|
+
normalizeLine(next, /^(php|rust|rs)$/i.test(id));
|
|
301
|
+
if (next.mode === 'html' && next.markup === 'value' && next.markupQuote === undefined) {
|
|
302
|
+
next.markup = 'tag';
|
|
255
303
|
}
|
|
256
|
-
next.escaped = false;
|
|
257
304
|
this.states.push(sameState(next, previous) ? previous : next);
|
|
258
305
|
}
|
|
259
306
|
const source = this.line(line);
|
|
260
|
-
|
|
307
|
+
const state = scan(source.text, column, this.states[line], rules, source.start);
|
|
308
|
+
return state.mode === 'html' && state.embed !== undefined ? { ...state.embed, markup: 'raw', element: 'script', embedded: 'javascript' } : state;
|
|
261
309
|
}
|
|
262
310
|
}
|
package/package.json
CHANGED
package/src/comments.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { EditPlan, EditSource } from './edit-source.ts';
|
|
2
|
+
import { editRegions, phpLiteralAt, phpTagAt, type EditRegions } from './edit-regions.ts';
|
|
3
|
+
import { hasJsx } from './markup-regions.ts';
|
|
2
4
|
import { type BlockComment, type CommentSyntax, commentSyntax } from './languages.ts';
|
|
3
5
|
import { indentText, isBlank, rangeOf, whitespaceOf } from './line-text.ts';
|
|
4
6
|
import { mapOffset } from './offsets.ts';
|
|
@@ -27,8 +29,8 @@ interface Geometry {
|
|
|
27
29
|
length: number;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
function syntaxOf(
|
|
31
|
-
const syntax = commentSyntax(options.language,
|
|
32
|
+
function syntaxOf(regions: EditRegions, options: CommentOptions, line: number, offset?: number): CommentSyntax {
|
|
33
|
+
const syntax = commentSyntax(options.language, regions(line, offset));
|
|
32
34
|
return options.lineToken === undefined ? syntax : { ...syntax, line: options.lineToken };
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -89,19 +91,42 @@ function geometryOf(source: EditSource, edits: readonly TextEdit[], index: numbe
|
|
|
89
91
|
* The markers go in at the smallest indentation of the lines, and a lone caret moves to the next line.
|
|
90
92
|
*/
|
|
91
93
|
export function planLineComments(source: EditSource, selections: readonly Selection[], options: CommentOptions): EditPlan | null {
|
|
94
|
+
const regions = editRegions(source, options.language);
|
|
92
95
|
const blocks = blocksOf(source, selections, options.lineSpan);
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
const lineBased = new Map<number, CommentSyntax>();
|
|
97
|
+
for (const block of blocks) {
|
|
98
|
+
let marker: CommentSyntax | undefined;
|
|
99
|
+
let mixed = false;
|
|
100
|
+
const firstSyntax = hasJsx(options.language) && block.first !== block.last ? syntaxOf(regions, options, block.first) : undefined;
|
|
101
|
+
for (let line = block.first; line <= block.last; line++) {
|
|
102
|
+
const syntax = firstSyntax ?? syntaxOf(regions, options, line);
|
|
103
|
+
const effective = syntax.line === null ? syntax : { ...syntax, block: null };
|
|
104
|
+
lineBased.set(line, effective);
|
|
105
|
+
if (effective.line !== null || effective.block !== null) {
|
|
106
|
+
mixed ||=
|
|
107
|
+
marker !== undefined &&
|
|
108
|
+
(marker.line !== effective.line || marker.block?.open !== effective.block?.open || marker.block?.close !== effective.block?.close);
|
|
109
|
+
marker ??= effective;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Commenting a region opener would change the markers needed for the next toggle.
|
|
113
|
+
if (mixed && /^(php|vue)$/i.test(options.language)) {
|
|
114
|
+
for (let line = block.first; line <= block.last; line++) {
|
|
115
|
+
lineBased.set(line, { line: null, block: null });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
96
118
|
}
|
|
97
|
-
const lineBased = syntaxes.map((syntax): CommentSyntax => (syntax.line === null ? syntax : { ...syntax, block: null }));
|
|
98
119
|
let allCommented = true;
|
|
99
120
|
let anyCommented = false;
|
|
100
|
-
for (const
|
|
121
|
+
for (const block of blocks) {
|
|
101
122
|
const single = block.first === block.last;
|
|
102
123
|
for (let line = block.first; line <= block.last; line++) {
|
|
103
124
|
const text = source.line(line).text;
|
|
104
|
-
const
|
|
125
|
+
const syntax = lineBased.get(line)!;
|
|
126
|
+
if (syntax.line === null && syntax.block === null) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const commented = isCommented(text, syntax, !single);
|
|
105
130
|
if (commented && !isBlank(text)) {
|
|
106
131
|
anyCommented = true;
|
|
107
132
|
}
|
|
@@ -116,8 +141,8 @@ export function planLineComments(source: EditSource, selections: readonly Select
|
|
|
116
141
|
|
|
117
142
|
const edits: TextEdit[] = [];
|
|
118
143
|
const placed = new Set<number>();
|
|
119
|
-
for (const
|
|
120
|
-
const syntax = lineBased
|
|
144
|
+
for (const block of blocks) {
|
|
145
|
+
const syntax = lineBased.get(block.first)!;
|
|
121
146
|
const multiline = block.first !== block.last;
|
|
122
147
|
const startingNewLine =
|
|
123
148
|
!multiline &&
|
|
@@ -127,6 +152,10 @@ export function planLineComments(source: EditSource, selections: readonly Select
|
|
|
127
152
|
const minimum = allCommented ? 0 : minimumIndent(source, block, syntax, options);
|
|
128
153
|
for (let line = block.first; line <= block.last; line++) {
|
|
129
154
|
const bounds = source.line(line);
|
|
155
|
+
const syntax = lineBased.get(line)!;
|
|
156
|
+
if (syntax.line === null && syntax.block === null) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
130
159
|
const edit = allCommented
|
|
131
160
|
? uncommentLine(bounds.start, bounds.text, syntax)
|
|
132
161
|
: commentLine(bounds.start, bounds.text, syntax, minimum, multiline, startingNewLine, options);
|
|
@@ -239,7 +268,7 @@ function finalSelections(
|
|
|
239
268
|
edits: readonly TextEdit[],
|
|
240
269
|
allCommented: boolean,
|
|
241
270
|
placed: ReadonlySet<number>,
|
|
242
|
-
syntaxes:
|
|
271
|
+
syntaxes: ReadonlyMap<number, CommentSyntax>,
|
|
243
272
|
span: CommentOptions['lineSpan']
|
|
244
273
|
): Selection[] {
|
|
245
274
|
const sorted = [...edits].sort((left, right) => left.from - right.from);
|
|
@@ -251,13 +280,17 @@ function finalSelections(
|
|
|
251
280
|
if (!moveCarets) {
|
|
252
281
|
return result;
|
|
253
282
|
}
|
|
254
|
-
for (const
|
|
283
|
+
for (const block of blocks) {
|
|
255
284
|
for (const index of block.carets) {
|
|
256
285
|
const selection = selections[index]!;
|
|
257
286
|
const { from, to } = rangeOf(selection);
|
|
258
287
|
const line = source.lineAt(from);
|
|
259
288
|
if (from === to) {
|
|
260
|
-
|
|
289
|
+
const syntax = syntaxes.get(line)!;
|
|
290
|
+
if (syntax.line === null && syntax.block === null) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
result[index] = caretAfter(source, sorted, line, span?.(line).last ?? line, selection.head, allCommented, placed, syntax);
|
|
261
294
|
continue;
|
|
262
295
|
}
|
|
263
296
|
const lastLine = source.lineAt(to - 1);
|
|
@@ -302,7 +335,7 @@ function commentAround(source: EditSource, offset: number, block: BlockComment):
|
|
|
302
335
|
if ((context.mode !== 'block-comment' && context.mode !== 'html-comment') || context.commentStart === undefined) {
|
|
303
336
|
return null;
|
|
304
337
|
}
|
|
305
|
-
const from = context.commentStart;
|
|
338
|
+
const from = block.open === '{/*' && source.charAt(context.commentStart - 1) === '{' ? context.commentStart - 1 : context.commentStart;
|
|
306
339
|
if (source.slice(from, from + block.open.length) !== block.open) {
|
|
307
340
|
return null;
|
|
308
341
|
}
|
|
@@ -327,12 +360,34 @@ function commentOf(source: EditSource, from: number, to: number, block: BlockCom
|
|
|
327
360
|
* With nothing selected it opens an empty comment around the caret.
|
|
328
361
|
*/
|
|
329
362
|
export function planBlockComment(source: EditSource, selections: readonly Selection[], options: CommentOptions): EditPlan | null {
|
|
363
|
+
const regions = editRegions(source, options.language);
|
|
330
364
|
const edits: TextEdit[] = [];
|
|
331
365
|
const result: Selection[] = [];
|
|
332
366
|
let offsetDelta = 0;
|
|
333
367
|
for (const selection of selections) {
|
|
334
368
|
const { from, to } = rangeOf(selection);
|
|
335
|
-
|
|
369
|
+
if (/^php$/i.test(options.language)) {
|
|
370
|
+
const context = source.context(from);
|
|
371
|
+
const { mode } = context;
|
|
372
|
+
const endContext = source.context(to);
|
|
373
|
+
const endMode = endContext.mode;
|
|
374
|
+
const endsLineComment = mode === 'code' && endMode === 'line-comment' && source.slice(to, source.line(source.lineAt(to)).end).trim() === '';
|
|
375
|
+
const text = source.slice(from, to);
|
|
376
|
+
// Wrapping a PHP tag can move a closing marker into a different language.
|
|
377
|
+
if (
|
|
378
|
+
phpLiteralAt(source, from) ||
|
|
379
|
+
phpLiteralAt(source, to) ||
|
|
380
|
+
phpTagAt(source, from) ||
|
|
381
|
+
phpTagAt(source, to) ||
|
|
382
|
+
(mode !== endMode && !endsLineComment) ||
|
|
383
|
+
context.embedStart !== endContext.embedStart ||
|
|
384
|
+
/<\?(?:php\b|=)/.test(text) ||
|
|
385
|
+
(mode !== 'html' && context.embedded === undefined && text.includes('?>'))
|
|
386
|
+
) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const block = syntaxOf(regions, options, source.lineAt(from), from).block;
|
|
336
391
|
if (!block) {
|
|
337
392
|
return null;
|
|
338
393
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { commentRole } from './fold-roles.ts';
|
|
2
|
+
import { commentSyntax, vueRegionAt } from './languages.ts';
|
|
3
|
+
import { scanStructure } from './structure.ts';
|
|
4
|
+
|
|
5
|
+
/* A documentation comment that stands on lines of its own, so a row can take its place. */
|
|
6
|
+
export interface DocComment {
|
|
7
|
+
/* Offsets of its opener and of the end of its closer. */
|
|
8
|
+
from: number;
|
|
9
|
+
to: number;
|
|
10
|
+
/* Zero-based and inclusive. */
|
|
11
|
+
startLine: number;
|
|
12
|
+
endLine: number;
|
|
13
|
+
/* The whitespace in front of the comment on its first line. */
|
|
14
|
+
indent: string;
|
|
15
|
+
/* The text between the delimiters with the margin stars taken off. */
|
|
16
|
+
body: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Past this many comments in one document the rest are left as text. */
|
|
20
|
+
export const MAX_DOC_COMMENTS = 500;
|
|
21
|
+
|
|
22
|
+
function bodyOf(inner: string): string {
|
|
23
|
+
const lines = inner.split(/\r\n|\r|\n/).map((line, index) => (index === 0 ? line.replace(/^[ \t]+/, '') : line.replace(/^[ \t]*\*?[ \t]?/, '')));
|
|
24
|
+
const last = lines.length - 1;
|
|
25
|
+
lines[last] = lines[last]!.replace(/[ \t*]+$/, '');
|
|
26
|
+
while (lines.length > 0 && lines[0]!.trim() === '') {
|
|
27
|
+
lines.shift();
|
|
28
|
+
}
|
|
29
|
+
while (lines.length > 0 && lines.at(-1)!.trim() === '') {
|
|
30
|
+
lines.pop();
|
|
31
|
+
}
|
|
32
|
+
return lines.map((line) => line.replace(/[ \t]+$/, '')).join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const HEREDOC_OPENER = /<<<[ \t]*(?:'(\w+)'|"(\w+)"|(\w+))\r?\n/g;
|
|
36
|
+
|
|
37
|
+
/* The bodies of PHP heredocs and nowdocs, which the structure scan reads as code. Offsets of the first character after the opener and of the closing label. */
|
|
38
|
+
function heredocBodies(text: string): [number, number][] {
|
|
39
|
+
const result: [number, number][] = [];
|
|
40
|
+
for (const opener of text.matchAll(HEREDOC_OPENER)) {
|
|
41
|
+
const label = opener[1] ?? opener[2] ?? opener[3]!;
|
|
42
|
+
const from = opener.index + opener[0].length;
|
|
43
|
+
const closer = new RegExp(`^[ \\t]*${label}\\b`, 'm').exec(text.slice(from));
|
|
44
|
+
result.push([from, closer === null ? text.length : from + closer.index]);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
* The block comments that open with two stars and have their lines to themselves: nothing but
|
|
51
|
+
* whitespace before the opener and after the closer. Only languages that write block comments as
|
|
52
|
+
* a slash and a star have any, and a Vue file has them in its script. A comment that shares a line
|
|
53
|
+
* with code is left out, since a row could not take its lines without hiding the code. A banner of
|
|
54
|
+
* three stars or more is no documentation, and nor is the text of a PHP heredoc or nowdoc. A heredoc
|
|
55
|
+
* body is still read as code by the scan, so a quote in it can hide the comments that follow.
|
|
56
|
+
*/
|
|
57
|
+
export function findDocComments(text: string, language: string, limit = MAX_DOC_COMMENTS): DocComment[] {
|
|
58
|
+
const vue = language.toLowerCase() === 'vue';
|
|
59
|
+
const syntax = commentSyntax(language, vue ? 'script' : null);
|
|
60
|
+
if (syntax.block?.open !== '/*' || syntax.line !== '//' || !text.includes('/**')) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const result: DocComment[] = [];
|
|
64
|
+
const bodies = language.toLowerCase() === 'php' && text.includes('<<<') ? heredocBodies(text) : [];
|
|
65
|
+
const lines = vue ? text.split(/\r\n|\r|\n/) : null;
|
|
66
|
+
let counted = 0;
|
|
67
|
+
let line = 0;
|
|
68
|
+
for (const range of scanStructure(text)) {
|
|
69
|
+
if (result.length >= limit) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
if (range.kind !== 'comment' || commentRole((from, to) => text.slice(from, to), range.from, range.to) === undefined) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// Three stars or more is a banner, not a comment about the code under it.
|
|
76
|
+
if (text[range.from + 3] === '*' || bodies.some(([from, to]) => range.from >= from && range.from < to)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const lineStart = Math.max(text.lastIndexOf('\n', range.from - 1), text.lastIndexOf('\r', range.from - 1)) + 1;
|
|
80
|
+
const newline = text.slice(range.to).search(/[\r\n]/);
|
|
81
|
+
const lineEnd = newline === -1 ? text.length : range.to + newline;
|
|
82
|
+
const before = text.slice(lineStart, range.from);
|
|
83
|
+
if (!/^[ \t]*$/.test(before) || !/^[ \t]*$/.test(text.slice(range.to, lineEnd))) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
for (; counted < range.from; counted++) {
|
|
87
|
+
if (text[counted] === '\n' || (text[counted] === '\r' && text[counted + 1] !== '\n')) {
|
|
88
|
+
line++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const startLine = line;
|
|
92
|
+
if (lines !== null && vueRegionAt((index) => lines[index] ?? '', startLine) !== 'script') {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
let endLine = startLine;
|
|
96
|
+
for (let at = range.from; at < range.to; at++) {
|
|
97
|
+
if (text[at] === '\n' || (text[at] === '\r' && text[at + 1] !== '\n')) {
|
|
98
|
+
endLine++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
result.push({ from: range.from, to: range.to, startLine, endLine, indent: before, body: bodyOf(text.slice(range.innerFrom + 1, range.innerTo)) });
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|