@tsuzuri-lab/sed-language-server 0.1.1 → 0.2.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.
@@ -1,774 +0,0 @@
1
- import {
2
- characterAt,
3
- commandSpecificationsFor,
4
- createSubstituteFlagState,
5
- findLineEnd,
6
- findReplacementDelimiter,
7
- findTransliterateDelimiter,
8
- hasUnescapedTrailingBackslash,
9
- scanAddressSyntax,
10
- scanCommandEndSyntax,
11
- scanCommandRecoverySyntax,
12
- scanLabelArgumentSyntax,
13
- scanOptionalNumericArgumentSyntax,
14
- scanRegexDelimiter,
15
- scanShellCommandSyntax,
16
- scanSubstituteFlagTokenSyntax,
17
- scanTextCommandSyntax,
18
- scanVersionArgumentSyntax,
19
- skipBlanks,
20
- syntaxPolicyFor,
21
- } from "./sed-syntax.js";
22
- import {
23
- defaultSyntaxProfile,
24
- requireSyntaxProfile,
25
- } from "./syntax-profile.js";
26
-
27
- const commandKindsByProfile = new Map();
28
-
29
- function commandKindsFor(syntaxProfile) {
30
- let commandKinds = commandKindsByProfile.get(syntaxProfile);
31
- if (commandKinds !== undefined) {
32
- return commandKinds;
33
- }
34
-
35
- commandKinds = new Map(
36
- commandSpecificationsFor(syntaxProfile).map(({ command, kind }) => [
37
- command,
38
- kind,
39
- ]),
40
- );
41
- commandKindsByProfile.set(syntaxProfile, commandKinds);
42
- return commandKinds;
43
- }
44
-
45
- function scanCommandHeader(text, offset, lineEnd, syntaxProfile) {
46
- const policy = syntaxPolicyFor(syntaxProfile);
47
- const addressScanEnd = policy.regexp.escapedPhysicalNewlines
48
- ? text.length
49
- : lineEnd;
50
- let cursor = skipBlanks(text, offset, lineEnd);
51
- let commandContextStart = offset;
52
- const firstAddress = scanAddressSyntax(
53
- text,
54
- cursor,
55
- addressScanEnd,
56
- syntaxProfile,
57
- );
58
-
59
- if (
60
- firstAddress.kind === "invalid" ||
61
- firstAddress.kind === "indeterminate"
62
- ) {
63
- if (firstAddress.endOffset > lineEnd) {
64
- lineEnd = findLineEnd(text, firstAddress.endOffset);
65
- }
66
- return {
67
- commandContextStart: null,
68
- commandOffset: null,
69
- lineEnd,
70
- resumeOffset: null,
71
- };
72
- }
73
-
74
- if (firstAddress.kind === "valid") {
75
- commandContextStart = firstAddress.endOffset;
76
- if (firstAddress.endOffset > lineEnd) {
77
- lineEnd = findLineEnd(text, firstAddress.endOffset);
78
- }
79
- cursor = skipBlanks(text, firstAddress.endOffset, lineEnd);
80
-
81
- while (text[cursor] === ",") {
82
- cursor = skipBlanks(text, cursor + 1, lineEnd);
83
- const nextAddress = scanAddressSyntax(
84
- text,
85
- cursor,
86
- addressScanEnd,
87
- syntaxProfile,
88
- );
89
-
90
- if (
91
- nextAddress.kind === "invalid" ||
92
- nextAddress.kind === "indeterminate"
93
- ) {
94
- if (nextAddress.endOffset > lineEnd) {
95
- lineEnd = findLineEnd(text, nextAddress.endOffset);
96
- }
97
- return {
98
- commandContextStart: null,
99
- commandOffset: null,
100
- lineEnd,
101
- resumeOffset: null,
102
- };
103
- }
104
-
105
- if (nextAddress.kind === "none") {
106
- if (text[cursor] === ";") {
107
- return {
108
- commandContextStart: null,
109
- commandOffset: null,
110
- lineEnd,
111
- resumeOffset: cursor + 1,
112
- };
113
- }
114
-
115
- if (cursor >= lineEnd) {
116
- return {
117
- commandContextStart: null,
118
- commandOffset: null,
119
- lineEnd,
120
- resumeOffset: null,
121
- };
122
- }
123
- break;
124
- }
125
-
126
- commandContextStart = nextAddress.endOffset;
127
- if (nextAddress.endOffset > lineEnd) {
128
- lineEnd = findLineEnd(text, nextAddress.endOffset);
129
- }
130
- cursor = skipBlanks(text, nextAddress.endOffset, lineEnd);
131
- }
132
- }
133
-
134
- if (text[cursor] === "!") {
135
- commandContextStart = cursor + 1;
136
- cursor = skipBlanks(text, cursor + 1, lineEnd);
137
- }
138
-
139
- return {
140
- commandContextStart,
141
- commandOffset: cursor,
142
- lineEnd,
143
- resumeOffset: null,
144
- };
145
- }
146
-
147
- function scanCommandEnd(text, commandEnd, lineEnd, syntaxProfile, state = {}) {
148
- return scanCommandEndSyntax(text, commandEnd, lineEnd, syntaxProfile, state)
149
- .nextCommandOffset;
150
- }
151
-
152
- function scanSubstituteTail(
153
- text,
154
- flagsOffset,
155
- lineEnd,
156
- recoverAtClosingBrace,
157
- syntaxProfile,
158
- ) {
159
- const policy = syntaxPolicyFor(syntaxProfile);
160
- const canRecoverAtClosingBrace =
161
- recoverAtClosingBrace || policy.commandEnd.closingBraceTerminates;
162
- let cursor = flagsOffset;
163
- let flagState = createSubstituteFlagState();
164
-
165
- while (cursor < lineEnd) {
166
- const character = characterAt(text, cursor);
167
- if (character === null) {
168
- break;
169
- }
170
-
171
- if (character.value === ";") {
172
- return {
173
- contextEnd: cursor,
174
- nextCommandOffset: cursor + character.width,
175
- };
176
- }
177
-
178
- if (policy.commandEnd.commentTerminates && character.value === "#") {
179
- return {
180
- contextEnd: cursor,
181
- nextCommandOffset: null,
182
- };
183
- }
184
-
185
- if (canRecoverAtClosingBrace && character.value === "}") {
186
- return {
187
- contextEnd: cursor,
188
- nextCommandOffset: cursor,
189
- };
190
- }
191
-
192
- if (
193
- policy.substituteFlags.separator === "optional-blanks" &&
194
- (character.value === " " || character.value === "\t")
195
- ) {
196
- cursor = skipBlanks(text, cursor, lineEnd);
197
- continue;
198
- }
199
-
200
- const token = scanSubstituteFlagTokenSyntax(
201
- text,
202
- cursor,
203
- lineEnd,
204
- flagState,
205
- syntaxProfile,
206
- );
207
- if (token.kind === "flag" || token.kind === "occurrence") {
208
- flagState = token.state;
209
- cursor = token.endOffset;
210
- continue;
211
- }
212
-
213
- if (token.kind === "write") {
214
- return {
215
- contextEnd: cursor,
216
- nextCommandOffset: null,
217
- };
218
- }
219
-
220
- const recovery = scanCommandRecoverySyntax(
221
- text,
222
- token.endOffset,
223
- lineEnd,
224
- syntaxProfile,
225
- { recoverAtClosingBrace: canRecoverAtClosingBrace },
226
- );
227
- if (recovery.recoveryOffset === null) {
228
- return {
229
- contextEnd: null,
230
- nextCommandOffset: null,
231
- };
232
- }
233
-
234
- return {
235
- contextEnd: null,
236
- nextCommandOffset: recovery.nextCommandOffset,
237
- };
238
- }
239
-
240
- return {
241
- contextEnd: lineEnd,
242
- nextCommandOffset: null,
243
- };
244
- }
245
-
246
- function scanSubstitute(
247
- text,
248
- commandOffset,
249
- lineEnd,
250
- recoverAtClosingBrace,
251
- syntaxProfile,
252
- ) {
253
- const policy = syntaxPolicyFor(syntaxProfile);
254
- const delimiterOffset = commandOffset + 1;
255
- if (delimiterOffset >= lineEnd) {
256
- return {
257
- context: null,
258
- consumedUntil: lineEnd,
259
- nextCommandOffset: null,
260
- };
261
- }
262
-
263
- const delimiter = characterAt(text, delimiterOffset);
264
- if (delimiter === null || delimiter.value === "\\") {
265
- return {
266
- context: null,
267
- consumedUntil: lineEnd,
268
- nextCommandOffset: null,
269
- };
270
- }
271
-
272
- const pattern = scanRegexDelimiter(
273
- text,
274
- delimiterOffset + delimiter.width,
275
- policy.regexp.escapedPhysicalNewlines ? text.length : lineEnd,
276
- delimiter,
277
- syntaxProfile,
278
- );
279
- const patternEnd =
280
- pattern.closingOffset ??
281
- (syntaxProfile.dialect === "posix" && pattern.hasUnclosedBracketExpression
282
- ? pattern.possibleClosingOffset
283
- : null);
284
- if (patternEnd === null) {
285
- return {
286
- context: null,
287
- consumedUntil:
288
- pattern.failureOffset ??
289
- (policy.regexp.escapedPhysicalNewlines ? text.length : lineEnd),
290
- nextCommandOffset: null,
291
- };
292
- }
293
-
294
- const replacement = findReplacementDelimiter(
295
- text,
296
- patternEnd + delimiter.width,
297
- delimiter,
298
- syntaxProfile,
299
- );
300
- if (replacement.closingOffset === null) {
301
- return {
302
- context: null,
303
- consumedUntil: replacement.failureOffset,
304
- nextCommandOffset: null,
305
- };
306
- }
307
-
308
- const flagsOffset = replacement.closingOffset + delimiter.width;
309
- const flagsLineEnd =
310
- flagsOffset <= lineEnd ? lineEnd : findLineEnd(text, flagsOffset);
311
- const tail = scanSubstituteTail(
312
- text,
313
- flagsOffset,
314
- flagsLineEnd,
315
- recoverAtClosingBrace,
316
- syntaxProfile,
317
- );
318
-
319
- return {
320
- context:
321
- tail.contextEnd === null
322
- ? null
323
- : {
324
- startOffset: flagsOffset,
325
- endOffset: tail.contextEnd,
326
- value: { kind: "substitute-flag" },
327
- },
328
- consumedUntil: flagsLineEnd,
329
- nextCommandOffset: tail.nextCommandOffset,
330
- };
331
- }
332
-
333
- function scanTransliterate(text, commandOffset, lineEnd, syntaxProfile) {
334
- const delimiterOffset = commandOffset + 1;
335
- if (delimiterOffset >= lineEnd) {
336
- return null;
337
- }
338
-
339
- const delimiter = characterAt(text, delimiterOffset);
340
- if (
341
- delimiter === null ||
342
- delimiter.value === "\\" ||
343
- delimiter.value === "\n"
344
- ) {
345
- return null;
346
- }
347
-
348
- const firstStringEnd = findTransliterateDelimiter(
349
- text,
350
- delimiterOffset + delimiter.width,
351
- lineEnd,
352
- delimiter,
353
- syntaxProfile,
354
- );
355
- if (firstStringEnd === null) {
356
- return null;
357
- }
358
-
359
- const secondStringEnd = findTransliterateDelimiter(
360
- text,
361
- firstStringEnd + delimiter.width,
362
- lineEnd,
363
- delimiter,
364
- syntaxProfile,
365
- );
366
- if (secondStringEnd === null) {
367
- return null;
368
- }
369
-
370
- return secondStringEnd + delimiter.width;
371
- }
372
-
373
- function range(startOffset, endOffset) {
374
- return { startOffset, endOffset };
375
- }
376
-
377
- export function buildDocumentStructure(text, options = defaultSyntaxProfile) {
378
- const syntaxProfile = requireSyntaxProfile(options);
379
- const commandKinds = commandKindsFor(syntaxProfile);
380
- const contexts = [];
381
- const labelDefinitions = [];
382
- const labelReferences = [];
383
- let blockDepth = 0;
384
- let consumedUntil = -1;
385
- let lineStart = 0;
386
- let pendingCommandOffset = null;
387
- let continuedOpaqueArgument = false;
388
-
389
- function addCommandContext(startOffset, endOffset = startOffset) {
390
- contexts.push({
391
- startOffset,
392
- endOffset,
393
- value: { kind: "command" },
394
- });
395
- }
396
-
397
- function addLabel(command, commandOffset, lineEnd) {
398
- const argument = scanLabelArgumentSyntax(
399
- text,
400
- commandOffset,
401
- lineEnd,
402
- syntaxProfile,
403
- );
404
-
405
- if (command === ":") {
406
- if (argument.hasLabel) {
407
- labelDefinitions.push({
408
- name: argument.name,
409
- range: range(argument.labelStartOffset, argument.labelEndOffset),
410
- });
411
- }
412
- return argument;
413
- }
414
-
415
- if (!argument.hasValidSeparator) {
416
- return argument;
417
- }
418
-
419
- contexts.push({
420
- startOffset: argument.branchContextStartOffset,
421
- endOffset: argument.fieldEndOffset,
422
- replacementRange: range(
423
- argument.labelStartOffset,
424
- argument.labelEndOffset,
425
- ),
426
- value: { kind: "branch-label", command },
427
- });
428
-
429
- if (argument.hasLabel) {
430
- labelReferences.push({
431
- command,
432
- name: argument.name,
433
- range: range(argument.labelStartOffset, argument.labelEndOffset),
434
- });
435
- }
436
-
437
- return argument;
438
- }
439
-
440
- function scanLine(initialOffset, lineEnd) {
441
- let commandSearchOffset = initialOffset;
442
-
443
- while (commandSearchOffset <= lineEnd) {
444
- const header = scanCommandHeader(
445
- text,
446
- commandSearchOffset,
447
- lineEnd,
448
- syntaxProfile,
449
- );
450
- if (header.lineEnd > lineEnd) {
451
- lineEnd = header.lineEnd;
452
- consumedUntil = Math.max(consumedUntil, lineEnd);
453
- }
454
- if (header.resumeOffset !== null) {
455
- commandSearchOffset = header.resumeOffset;
456
- continue;
457
- }
458
-
459
- if (header.commandOffset === null) {
460
- break;
461
- }
462
-
463
- const commandOffset = header.commandOffset;
464
- // POSIX forbids trailing blanks after commands inside a command block.
465
- const commandContextStart =
466
- commandOffset >= lineEnd &&
467
- blockDepth === 0 &&
468
- header.commandContextStart !== null
469
- ? header.commandContextStart
470
- : commandOffset;
471
- addCommandContext(commandContextStart, commandOffset);
472
- if (commandOffset >= lineEnd) {
473
- break;
474
- }
475
-
476
- const commandCharacter = characterAt(text, commandOffset);
477
- if (commandCharacter === null) {
478
- break;
479
- }
480
-
481
- if (commandCharacter.value === ";") {
482
- commandSearchOffset = commandOffset + commandCharacter.width;
483
- continue;
484
- }
485
-
486
- const kind = commandKinds.get(commandCharacter.value);
487
- if (kind === undefined) {
488
- const nextCommandOffset = scanCommandEnd(
489
- text,
490
- commandOffset + commandCharacter.width,
491
- lineEnd,
492
- syntaxProfile,
493
- { insideBlock: blockDepth > 0 },
494
- );
495
- if (nextCommandOffset === null) {
496
- break;
497
- }
498
- commandSearchOffset = nextCommandOffset;
499
- continue;
500
- }
501
-
502
- if (kind === "line" || kind === "file") {
503
- break;
504
- }
505
-
506
- if (kind === "text") {
507
- const hasPhysicalNewline =
508
- text[lineEnd] === "\n" ||
509
- (text[lineEnd] === "\r" && text[lineEnd + 1] === "\n");
510
- const argument = scanTextCommandSyntax(
511
- text,
512
- commandOffset,
513
- lineEnd,
514
- hasPhysicalNewline,
515
- syntaxProfile,
516
- );
517
- continuedOpaqueArgument = argument.consumesFollowingTextLine ?? false;
518
- break;
519
- }
520
-
521
- if (kind === "shell") {
522
- const hasPhysicalNewline =
523
- text[lineEnd] === "\n" ||
524
- (text[lineEnd] === "\r" && text[lineEnd + 1] === "\n");
525
- const argument = scanShellCommandSyntax(
526
- text,
527
- commandOffset,
528
- lineEnd,
529
- hasPhysicalNewline,
530
- syntaxProfile,
531
- );
532
- continuedOpaqueArgument = argument.consumesFollowingTextLine;
533
- break;
534
- }
535
-
536
- if (kind === "label") {
537
- const argument = addLabel(
538
- commandCharacter.value,
539
- commandOffset,
540
- lineEnd,
541
- );
542
- if (argument.nextCommandOffset === null) {
543
- break;
544
- }
545
- commandSearchOffset = argument.nextCommandOffset;
546
- continue;
547
- }
548
-
549
- if (kind === "version") {
550
- const argument = scanVersionArgumentSyntax(
551
- text,
552
- commandOffset,
553
- lineEnd,
554
- syntaxProfile,
555
- );
556
- if (argument.nextCommandOffset === null) {
557
- break;
558
- }
559
- commandSearchOffset = argument.nextCommandOffset;
560
- continue;
561
- }
562
-
563
- if (kind === "substitute") {
564
- const result = scanSubstitute(
565
- text,
566
- commandOffset,
567
- lineEnd,
568
- blockDepth > 0,
569
- syntaxProfile,
570
- );
571
- consumedUntil = Math.max(consumedUntil, result.consumedUntil);
572
- if (result.context !== null) {
573
- contexts.push(result.context);
574
- }
575
-
576
- if (result.nextCommandOffset === null) {
577
- break;
578
- }
579
-
580
- if (result.nextCommandOffset > lineEnd) {
581
- pendingCommandOffset = result.nextCommandOffset;
582
- break;
583
- }
584
-
585
- commandSearchOffset = result.nextCommandOffset;
586
- continue;
587
- }
588
-
589
- if (kind === "transliterate") {
590
- const commandEnd = scanTransliterate(
591
- text,
592
- commandOffset,
593
- lineEnd,
594
- syntaxProfile,
595
- );
596
- if (commandEnd === null) {
597
- break;
598
- }
599
-
600
- const nextCommandOffset = scanCommandEnd(
601
- text,
602
- commandEnd,
603
- lineEnd,
604
- syntaxProfile,
605
- { insideBlock: blockDepth > 0 },
606
- );
607
- if (nextCommandOffset === null) {
608
- break;
609
- }
610
- commandSearchOffset = nextCommandOffset;
611
- continue;
612
- }
613
-
614
- if (kind === "block-open") {
615
- blockDepth += 1;
616
- commandSearchOffset = commandOffset + commandCharacter.width;
617
- continue;
618
- }
619
-
620
- if (kind === "block-close") {
621
- if (blockDepth > 0) {
622
- blockDepth -= 1;
623
- }
624
- }
625
-
626
- const commandEnd =
627
- kind === "numeric"
628
- ? scanOptionalNumericArgumentSyntax(
629
- text,
630
- commandOffset,
631
- lineEnd,
632
- syntaxProfile,
633
- ).commandEndOffset
634
- : commandOffset + commandCharacter.width;
635
- const nextCommandOffset = scanCommandEnd(
636
- text,
637
- commandEnd,
638
- lineEnd,
639
- syntaxProfile,
640
- {
641
- insideBlock: blockDepth > 0,
642
- recoverAtClosingBrace: kind === "block-close" || blockDepth > 0,
643
- },
644
- );
645
- if (nextCommandOffset === null) {
646
- break;
647
- }
648
- commandSearchOffset = nextCommandOffset;
649
- }
650
- }
651
-
652
- while (lineStart <= text.length) {
653
- const newlineOffset = text.indexOf("\n", lineStart);
654
- const nextLineStart =
655
- newlineOffset === -1 ? text.length + 1 : newlineOffset + 1;
656
- let lineEnd = newlineOffset === -1 ? text.length : newlineOffset;
657
- if (lineEnd > lineStart && text[lineEnd - 1] === "\r") {
658
- lineEnd -= 1;
659
- }
660
-
661
- const resumesOnThisLine =
662
- pendingCommandOffset !== null &&
663
- pendingCommandOffset >= lineStart &&
664
- pendingCommandOffset <= lineEnd;
665
-
666
- if (resumesOnThisLine) {
667
- const commandOffset = pendingCommandOffset;
668
- pendingCommandOffset = null;
669
- scanLine(commandOffset, lineEnd);
670
- } else if (lineStart <= consumedUntil) {
671
- // A multiline regexp may already have scanned a command on a later
672
- // physical line. Do not let its intermediate lines alter argument state.
673
- } else if (continuedOpaqueArgument) {
674
- const hasPhysicalTextLine =
675
- newlineOffset !== -1 || lineStart < text.length;
676
- continuedOpaqueArgument =
677
- hasPhysicalTextLine &&
678
- newlineOffset !== -1 &&
679
- hasUnescapedTrailingBackslash(text, lineStart, lineEnd);
680
- } else {
681
- scanLine(lineStart, lineEnd);
682
- }
683
-
684
- if (newlineOffset === -1) {
685
- break;
686
- }
687
- lineStart = nextLineStart;
688
- }
689
-
690
- return {
691
- labelDefinitions,
692
- labelReferences,
693
- contextAt(offset) {
694
- if (!Number.isInteger(offset) || offset < 0 || offset > text.length) {
695
- return null;
696
- }
697
-
698
- for (const context of contexts) {
699
- if (
700
- context.value.kind === "command" &&
701
- context.startOffset === offset
702
- ) {
703
- return context.value;
704
- }
705
- }
706
-
707
- for (const context of contexts) {
708
- if (context.startOffset <= offset && offset <= context.endOffset) {
709
- return context.value;
710
- }
711
- }
712
-
713
- return null;
714
- },
715
- contextDetailsAt(offset) {
716
- if (!Number.isInteger(offset) || offset < 0 || offset > text.length) {
717
- return null;
718
- }
719
-
720
- const exactCommand = contexts.find(
721
- (context) =>
722
- context.value.kind === "command" &&
723
- context.startOffset === offset &&
724
- context.endOffset === offset,
725
- );
726
- const context =
727
- exactCommand ??
728
- contexts.find(
729
- (candidate) =>
730
- candidate.startOffset <= offset && offset <= candidate.endOffset,
731
- );
732
- if (context === undefined) {
733
- return null;
734
- }
735
-
736
- return {
737
- ...context.value,
738
- range: range(context.startOffset, context.endOffset),
739
- replacementRange: context.replacementRange ?? null,
740
- };
741
- },
742
- };
743
- }
744
-
745
- let documentStructureCache = new WeakMap();
746
-
747
- export function invalidateDocumentStructureCache(document) {
748
- if (document === undefined) {
749
- documentStructureCache = new WeakMap();
750
- return;
751
- }
752
-
753
- documentStructureCache.delete(document);
754
- }
755
-
756
- export function getDocumentStructure(document, options = defaultSyntaxProfile) {
757
- const syntaxProfile = requireSyntaxProfile(options);
758
- let cachedProfiles = documentStructureCache.get(document);
759
- const cached = cachedProfiles?.get(syntaxProfile);
760
- if (cached?.version === document.version) {
761
- return cached.structure;
762
- }
763
-
764
- const structure = buildDocumentStructure(document.getText(), syntaxProfile);
765
- if (cachedProfiles === undefined) {
766
- cachedProfiles = new Map();
767
- documentStructureCache.set(document, cachedProfiles);
768
- }
769
- cachedProfiles.set(syntaxProfile, {
770
- structure,
771
- version: document.version,
772
- });
773
- return structure;
774
- }