Skip to content

Commit 1f84472

Browse files
committed
Extract content validation + extend JSON record validation & detection
This now uses parseTree with model.getPosition to get the exact correct line & column for errors, and to handle multiple errors in the same line or elsewhere, and accepts JSON lines & NDJSON in addition to 0x1E separated records like SignalR. Detection is extended to match, and uses some slightly broader heuristics to catch a few more cases.
1 parent f982a19 commit 1f84472

File tree

9 files changed

+356
-208
lines changed

9 files changed

+356
-208
lines changed

src/components/editor/json-records-validation.ts

Lines changed: 0 additions & 117 deletions
This file was deleted.

src/components/editor/monaco.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type * as MonacoTypes from 'monaco-editor';
22
import type { default as _MonacoEditor, MonacoEditorProps } from 'react-monaco-editor';
3+
import { observable, runInAction } from 'mobx';
34

45
import { defineMonacoThemes } from '../../styles';
56

67
import { delay } from '../../util/promise';
78
import { asError } from '../../util/error';
8-
import { observable, runInAction } from 'mobx';
9-
import { setupXMLValidation } from './xml-validation';
10-
import { setupJsonRecordsValidation } from './json-records-validation';
9+
10+
import { ContentValidator, validateJsonRecords, validateXml } from '../../model/events/content-validation';
1111

1212
export type {
1313
MonacoTypes,
@@ -80,8 +80,8 @@ async function loadMonacoEditor(retries = 5): Promise<void> {
8080
id: 'json-records'
8181
});
8282

83-
setupXMLValidation(monaco);
84-
setupJsonRecordsValidation(monaco);
83+
addValidator(monaco, 'xml', validateXml);
84+
addValidator(monaco, 'json-records', validateJsonRecords);
8585

8686
MonacoEditor = rmeModule.default;
8787
} catch (err) {
@@ -95,6 +95,43 @@ async function loadMonacoEditor(retries = 5): Promise<void> {
9595
}
9696
}
9797

98+
function addValidator(monaco: typeof MonacoTypes, modeId: string, validator: ContentValidator) {
99+
function validate(model: MonacoTypes.editor.ITextModel) {
100+
const text = model.getValue();
101+
const markers = validator(text, model);
102+
monaco.editor.setModelMarkers(model, modeId, markers);
103+
}
104+
105+
const contentChangeListeners = new Map<MonacoTypes.editor.ITextModel, MonacoTypes.IDisposable>();
106+
107+
function manageContentChangeListener(model: MonacoTypes.editor.ITextModel) {
108+
const isActiveMode = model.getModeId() === modeId;
109+
const listener = contentChangeListeners.get(model);
110+
111+
if (isActiveMode && !listener) {
112+
contentChangeListeners.set(model, model.onDidChangeContent(() =>
113+
validate(model)
114+
));
115+
validate(model);
116+
} else if (!isActiveMode && listener) {
117+
listener.dispose();
118+
contentChangeListeners.delete(model);
119+
monaco.editor.setModelMarkers(model, modeId, []);
120+
}
121+
}
122+
123+
monaco.editor.onWillDisposeModel(model => {
124+
contentChangeListeners.delete(model);
125+
});
126+
monaco.editor.onDidChangeModelLanguage(({ model }) => {
127+
manageContentChangeListener(model);
128+
});
129+
monaco.editor.onDidCreateModel(model => {
130+
manageContentChangeListener(model);
131+
});
132+
133+
}
134+
98135
export function reloadMonacoEditor() {
99136
return monacoLoadingPromise = loadMonacoEditor(0);
100137
}

src/components/editor/xml-validation.ts

Lines changed: 0 additions & 58 deletions
This file was deleted.

src/model/events/content-types.ts

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
isProbablyGrpcProto,
88
isValidGrpcProto,
99
} from '../../util/protobuf';
10+
import { isProbablyJson, isProbablyJsonRecords } from '../../util/json';
1011

1112
// Simplify a mime type as much as we can, without throwing any errors
1213
export const getBaseContentType = (mimeType: string | undefined) => {
@@ -121,13 +122,17 @@ const mimeTypeToContentTypeMap: { [mimeType: string]: ViewableContentType } = {
121122
'application/grpc-proto': 'grpc-proto',
122123
'application/grpc-protobuf': 'grpc-proto',
123124

125+
// Nobody can quite agree on the names for the various sequence-of-JSON formats:
126+
'application/jsonlines': 'json-records',
127+
'application/json-lines': 'json-records',
128+
'application/x-jsonlines': 'json-records',
129+
'application/jsonl': 'json-records',
130+
'application/x-ndjson': 'json-records',
131+
'application/json-seq': 'json-records',
132+
124133
'application/octet-stream': 'raw'
125134
} as const;
126135

127-
export const jsonRecordsSeparators = [
128-
0x1E, // SignalR record separator https://github.com/dotnet/aspnetcore/blob/v8.0.0/src/SignalR/docs/specs/HubProtocol.md#json-encoding
129-
];
130-
131136
export function getContentType(mimeType: string | undefined): ViewableContentType | undefined {
132137
const baseContentType = getBaseContentType(mimeType);
133138
return mimeTypeToContentTypeMap[baseContentType!];
@@ -193,26 +198,18 @@ export function getCompatibleTypes(
193198
body = body.decodedData;
194199
}
195200

196-
// Examine the first char of the body, assuming it's ascii
197-
const firstChar = body && body.subarray(0, 1).toString('ascii');
198-
199201
// Allow optionally formatting non-JSON-records as JSON-records, if it looks like it might be
200-
if (body && body.length > 2 && firstChar === '{'
201-
&& jsonRecordsSeparators.indexOf(body[body.length - 1]) > -1
202-
) {
203-
const secondToLastChar = body.subarray(body.length - 2, body.length - 1).toString('ascii');
204-
if (secondToLastChar === '}') {
205-
types.add('json-records');
206-
}
202+
if (!types.has('json-records') && isProbablyJsonRecords(body)) {
203+
types.add('json-records');
207204
}
208205

209-
// Allow optionally formatting non-JSON as JSON, if it looks like it might be
210-
if ((firstChar === '{' && !types.has('json-records')) || firstChar === '[') {
206+
if (!types.has('json-records') && isProbablyJson(body)) {
207+
// Allow optionally formatting non-JSON as JSON, if it's anything remotely close
211208
types.add('json');
212209
}
213210

214211
// Allow optionally formatting non-XML as XML, if it looks like it might be
215-
if (firstChar === '<') {
212+
if (body?.subarray(0, 1).toString('ascii') === '<') {
216213
types.add('xml');
217214
}
218215

0 commit comments

Comments
 (0)