Skip to content

Commit 483756d

Browse files
committed
Add fingerprint to punctuation checker diagnostics
1 parent ff130aa commit 483756d

33 files changed

Lines changed: 1715 additions & 658 deletions

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"pattern": "./packages/*/"
88
}
99
],
10-
"cSpell.words": ["clientrc", "sillsdev", "Usfm"]
10+
"cSpell.words": ["checkables", "clientrc", "sillsdev", "Usfm"]
1111
}

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
1-
import { concat, defer, from, map, merge, mergeMap, Observable, Subject, switchMap } from 'rxjs';
1+
import {
2+
concat,
3+
concatMap,
4+
connectable,
5+
defer,
6+
finalize,
7+
from,
8+
map,
9+
merge,
10+
mergeMap,
11+
Observable,
12+
ReplaySubject,
13+
Subject,
14+
switchMap,
15+
} from 'rxjs';
216

317
import { TextEdit } from '../common/text-edit';
418
import { Document } from '../document/document';
@@ -23,26 +37,26 @@ export interface DiagnosticProvider<T = TextEdit> {
2337

2438
export function activeDiagnosticsChanged$<T extends Document>(
2539
documents: DocumentAccessor<T>,
26-
validateDocument: (doc: T) => Diagnostic[],
40+
validateDocument: (doc: T) => Promise<Diagnostic[]>,
2741
refreshSubject?: Subject<string>,
2842
): Observable<DiagnosticsChanged> {
2943
const streams: Observable<DiagnosticsChanged>[] = [
3044
documents.opened$.pipe(
31-
map((e) => ({
45+
concatMap(async (e) => ({
3246
uri: e.document.uri,
3347
version: e.document.version,
34-
diagnostics: validateDocument(e.document),
48+
diagnostics: await validateDocument(e.document),
3549
})),
3650
),
3751
documents.changed$.pipe(
38-
map((e) => ({
52+
concatMap(async (e) => ({
3953
uri: e.document.uri,
4054
version: e.document.version,
41-
diagnostics: validateDocument(e.document),
55+
diagnostics: await validateDocument(e.document),
4256
})),
4357
),
4458
documents.closed$.pipe(
45-
switchMap(async (e) => {
59+
concatMap(async (e) => {
4660
const doc = await documents.get(e.uri);
4761
return { uri: e.uri, version: doc?.version, diagnostics: [] };
4862
}),
@@ -51,9 +65,9 @@ export function activeDiagnosticsChanged$<T extends Document>(
5165
if (refreshSubject != null) {
5266
streams.push(
5367
refreshSubject.pipe(
54-
switchMap(async (uri): Promise<DiagnosticsChanged> => {
68+
concatMap(async (uri): Promise<DiagnosticsChanged> => {
5569
const doc = await documents.get(uri);
56-
return { uri, version: doc?.version, diagnostics: doc != null ? validateDocument(doc) : [] };
70+
return { uri, version: doc?.version, diagnostics: doc != null ? await validateDocument(doc) : [] };
5771
}),
5872
),
5973
);
@@ -63,53 +77,76 @@ export function activeDiagnosticsChanged$<T extends Document>(
6377

6478
export function allDiagnosticsChanged$<T extends Document>(
6579
documents: DocumentAccessor<T>,
66-
validateDocument: (doc: T) => Diagnostic[],
80+
validateDocument: (doc: T) => Promise<Diagnostic[]>,
6781
refreshSubject?: Subject<string>,
6882
): Observable<DiagnosticsChanged> {
83+
// Live document lifecycle streams that may emit while initial full validation is running.
6984
const streams: Observable<DiagnosticsChanged>[] = [
7085
documents.opened$.pipe(
71-
map((e) => ({
86+
concatMap(async (e) => ({
7287
uri: e.document.uri,
7388
version: e.document.version,
74-
diagnostics: validateDocument(e.document),
89+
diagnostics: await validateDocument(e.document),
7590
})),
7691
),
7792
documents.changed$.pipe(
78-
map((e) => ({
93+
concatMap(async (e) => ({
7994
uri: e.document.uri,
8095
version: e.document.version,
81-
diagnostics: validateDocument(e.document),
96+
diagnostics: await validateDocument(e.document),
8297
})),
8398
),
8499
documents.created$.pipe(
85-
map((e) => ({
100+
concatMap(async (e) => ({
86101
uri: e.document.uri,
87102
version: e.document.version,
88-
diagnostics: validateDocument(e.document),
103+
diagnostics: await validateDocument(e.document),
89104
})),
90105
),
91106
documents.deleted$.pipe(map((e) => ({ uri: e.uri, diagnostics: [] }))),
92107
documents.reset$.pipe(
93108
switchMap((_) => documents.all()),
94109
mergeMap((docs) => docs),
95-
map((doc) => ({ uri: doc.uri, version: doc.version, diagnostics: validateDocument(doc) })),
110+
mergeMap(async (doc) => ({ uri: doc.uri, version: doc.version, diagnostics: await validateDocument(doc) })),
96111
),
97112
];
98113
if (refreshSubject != null) {
99114
streams.push(
100115
refreshSubject.pipe(
101-
switchMap(async (uri): Promise<DiagnosticsChanged> => {
116+
switchMap(async (uri) => {
102117
const doc = await documents.get(uri);
103-
return { uri, version: doc?.version, diagnostics: doc != null ? validateDocument(doc) : [] };
118+
return { uri, version: doc?.version, diagnostics: doc != null ? await validateDocument(doc) : [] };
104119
}),
105120
),
106121
);
107122
}
108-
return concat(
109-
defer(() => from(documents.all())).pipe(
110-
mergeMap((docs) => docs),
111-
map((doc) => ({ uri: doc.uri, version: doc.version, diagnostics: validateDocument(doc) })),
112-
),
113-
merge(...streams),
114-
);
123+
124+
// Two-phase emission strategy:
125+
// 1) Emit diagnostics for all currently known documents (initial snapshot pass).
126+
// 2) Then continue with live document events.
127+
//
128+
// To avoid missing live events that occur during phase (1), we connect and buffer
129+
// live streams immediately using a ReplaySubject-backed connectable observable.
130+
return defer(() => {
131+
const liveStreams$ = connectable(merge(...streams), {
132+
connector: () => new ReplaySubject<DiagnosticsChanged>(),
133+
});
134+
// Start capturing live events now, before initial documents are validated.
135+
const connection = liveStreams$.connect();
136+
137+
return concat(
138+
// Initial full validation pass over all known documents.
139+
defer(() => from(documents.all())).pipe(
140+
mergeMap((docs) => docs),
141+
mergeMap(async (doc) => ({ uri: doc.uri, version: doc.version, diagnostics: await validateDocument(doc) })),
142+
),
143+
// Replay any buffered live events, then continue streaming new live events.
144+
liveStreams$,
145+
).pipe(
146+
finalize(() => {
147+
// Ensure we tear down the live connection when downstream unsubscribes/completes.
148+
connection.unsubscribe();
149+
}),
150+
);
151+
});
115152
}

packages/examples/src/verse-order-diagnostic-provider.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ export class VerseOrderDiagnosticProvider<T = TextEdit> implements DiagnosticPro
3131
if (validateAllDocuments) {
3232
this.diagnosticsChanged$ = allDiagnosticsChanged$(
3333
documents,
34-
(doc) => this.validateDocument(doc),
34+
(doc) => Promise.resolve(this.validateDocument(doc)),
3535
this.refreshSubject,
3636
);
3737
} else {
3838
this.diagnosticsChanged$ = activeDiagnosticsChanged$(
3939
documents,
40-
(doc) => this.validateDocument(doc),
40+
(doc) => Promise.resolve(this.validateDocument(doc)),
4141
this.refreshSubject,
4242
);
4343
}
@@ -112,7 +112,7 @@ export class VerseOrderDiagnosticProvider<T = TextEdit> implements DiagnosticPro
112112
}),
113113
moreInfo: this.localizer.t('verseOutOfOrder.moreInfo', { ns: 'verseOrder' }),
114114
source: this.id,
115-
fingerprint: `1|${chapterNumber}|${prevVerseNumber.toString()}`,
115+
fingerprint: `1|${chapterNumber}:${prevVerseNumber.toString()}`,
116116
});
117117
}
118118
}
@@ -143,7 +143,7 @@ export class VerseOrderDiagnosticProvider<T = TextEdit> implements DiagnosticPro
143143
moreInfo: this.localizer.t('missingVerse.moreInfo', { ns: 'verseOrder' }),
144144
source: this.id,
145145
data: missingVerse,
146-
fingerprint: `2|${chapterNumber}|${missingVerse.toString()}`,
146+
fingerprint: `2|${chapterNumber}:${missingVerse.toString()}`,
147147
});
148148
}
149149
}

packages/punctuation-checker/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"directory": "packages/punctuation-checker"
3535
},
3636
"dependencies": {
37+
"@aws-crypto/sha256-universal": "^5.2.0",
3738
"@sillsdev/lynx": "^0.3.5",
3839
"rxjs": "^7.8.1"
3940
},

packages/punctuation-checker/src/abstract-checker.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export abstract class AbstractChecker<TDoc extends TextDocument | ScriptureDocum
5454
if (doc == null) {
5555
return [];
5656
}
57-
return this.validateDocument(doc);
57+
return await this.validateDocument(doc);
5858
}
5959

6060
async getDiagnosticFixes(uri: string, diagnostic: Diagnostic): Promise<DiagnosticFix<TEdit>[]> {
@@ -69,29 +69,31 @@ export abstract class AbstractChecker<TDoc extends TextDocument | ScriptureDocum
6969
this.refreshSubject.next(uri);
7070
}
7171

72-
private validateDocument(document: TDoc): Diagnostic[] {
72+
private async validateDocument(document: TDoc): Promise<Diagnostic[]> {
7373
if (isScriptureDocument(document)) {
74-
return this.validateScriptureDocument(document);
74+
return await this.validateScriptureDocument(document);
7575
}
76-
return this.validateTextDocument(document);
76+
return await this.validateTextDocument(document);
7777
}
7878

79-
protected validateTextDocument(textDocument: TextDocument): Diagnostic[] {
79+
protected async validateTextDocument(textDocument: TextDocument): Promise<Diagnostic[]> {
8080
const diagnosticFactory: DiagnosticFactory = new DiagnosticFactory(this.id, textDocument);
8181

8282
const issueFinder: IssueFinder = this.issueFinderFactory.createIssueFinder(diagnosticFactory);
83-
return issueFinder.produceDiagnostics(new CheckableGroup([new TextDocumentCheckable(textDocument.getText())]));
83+
return await issueFinder.produceDiagnostics(
84+
new CheckableGroup([new TextDocumentCheckable(textDocument.getText())]),
85+
);
8486
}
8587

86-
protected validateScriptureDocument(scriptureDocument: ScriptureDocument): Diagnostic[] {
88+
protected async validateScriptureDocument(scriptureDocument: ScriptureDocument): Promise<Diagnostic[]> {
8789
let diagnostics: Diagnostic[] = [];
8890
const diagnosticFactory: DiagnosticFactory = new DiagnosticFactory(this.id, scriptureDocument);
8991

9092
const issueFinder: IssueFinder = this.issueFinderFactory.createIssueFinder(diagnosticFactory);
9193

9294
const scriptureNodeGrouper: ScriptureTextNodeGrouper = new ScriptureTextNodeGrouper(scriptureDocument);
9395
for (const checkableGroup of scriptureNodeGrouper.getCheckableGroups()) {
94-
diagnostics = diagnostics.concat(issueFinder.produceDiagnostics(checkableGroup));
96+
diagnostics = diagnostics.concat(await issueFinder.produceDiagnostics(checkableGroup));
9597
}
9698
return diagnostics;
9799
}

packages/punctuation-checker/src/allowed-character/allowed-character-issue-finder.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,40 +31,63 @@ export class AllowedCharacterIssueFinder implements IssueFinder {
3131
this.diagnosticList = new DiagnosticList();
3232
}
3333

34-
public produceDiagnostics(checkableGroup: CheckableGroup): Diagnostic[] {
34+
public async produceDiagnostics(checkableGroup: CheckableGroup): Promise<Diagnostic[]> {
3535
this.diagnosticList = new DiagnosticList();
3636

3737
for (const checkable of checkableGroup) {
3838
let match: RegExpExecArray | null;
39-
while ((match = this.characterRegex.exec(checkable.getText()))) {
39+
const text = checkable.getText();
40+
while ((match = this.characterRegex.exec(text))) {
4041
const character = match[0];
4142

42-
this.checkCharacter(character, match.index, match.index + match[0].length, checkable.getEnclosingRange());
43+
await this.checkCharacter(
44+
character,
45+
match.index,
46+
match.index + match[0].length,
47+
checkable.getEnclosingRange(),
48+
checkable.getVerseRef(),
49+
checkable.getLeftContext(match.index, 5),
50+
checkable.getRightContext(match.index + match[0].length, 5),
51+
);
4352
}
4453
}
4554

4655
return this.diagnosticList.toArray();
4756
}
4857

49-
private checkCharacter(
58+
private async checkCharacter(
5059
character: string,
5160
characterStartIndex: number,
5261
characterEndIndex: number,
53-
enclosingRange?: Range,
54-
): void {
62+
enclosingRange: Range | undefined,
63+
verseRef: string | undefined,
64+
leftContext: string | undefined,
65+
rightContext: string | undefined,
66+
): Promise<void> {
5567
if (!this.allowedCharacterSet.isCharacterAllowed(character)) {
56-
this.addDisallowedCharacterWarning(character, characterStartIndex, characterEndIndex, enclosingRange);
68+
await this.addDisallowedCharacterWarning(
69+
character,
70+
characterStartIndex,
71+
characterEndIndex,
72+
enclosingRange,
73+
verseRef,
74+
leftContext,
75+
rightContext,
76+
);
5777
}
5878
}
5979

60-
private addDisallowedCharacterWarning(
80+
private async addDisallowedCharacterWarning(
6181
character: string,
6282
characterStartIndex: number,
6383
characterEndIndex: number,
64-
enclosingRange?: Range,
84+
enclosingRange: Range | undefined,
85+
verseRef: string | undefined,
86+
leftContext: string | undefined,
87+
rightContext: string | undefined,
6588
) {
6689
const code: string = AllowedCharacterIssueFinder.DIAGNOSTIC_CODE;
67-
const diagnostic: Diagnostic = this.diagnosticFactory
90+
const diagnostic: Diagnostic = await this.diagnosticFactory
6891
.newBuilder()
6992
.setCode(code)
7093
.setSeverity(DiagnosticSeverity.Warning)
@@ -75,6 +98,10 @@ export class AllowedCharacterIssueFinder implements IssueFinder {
7598
character: character,
7699
}),
77100
)
101+
.setVerseRef(verseRef)
102+
.setContent(character)
103+
.setLeftContext(leftContext)
104+
.setRightContext(rightContext)
78105
.build();
79106

80107
this.diagnosticList.addDiagnostic(diagnostic);

0 commit comments

Comments
 (0)