Skip to content

Commit 2ded62c

Browse files
committed
feat(apidom-ls): completion and validation fixes
1 parent 433a2c2 commit 2ded62c

22 files changed

+1288
-172
lines changed

package-lock.json

Lines changed: 89 additions & 89 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/apidom-ls/src/apidom-language-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ export interface ValidationProvider {
6262
): Promise<Diagnostic[]>;
6363

6464
configure(settings: LanguageSettings): void;
65+
66+
name(): string;
6567
}
6668

6769
export interface LanguageSettings {

packages/apidom-ls/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export {
1212
} from '@swagger-api/apidom-core';
1313

1414
export { default as getLanguageService } from './apidom-language-service';
15+
export { JsonSchemaValidationProvider } from './services/validation/providers/json-schema-validation-provider';
16+
export { Asyncapi20JsonSchemaValidationProvider } from './services/validation/providers/asyncapi-20-json-schema-validation-provider';
17+
export { OpenAPi31JsonSchemaValidationProvider } from './services/validation/providers/openapi-31-json-schema-validation-provider';
1518

1619
export { isJsonDoc, isAsyncDoc, getText } from './parser-factory';
1720

packages/apidom-ls/src/parser-factory.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,23 @@ export async function parse(
7171
textDocument: TextDocument,
7272
metadataMaps: MetadataMaps | undefined,
7373
): Promise<ParseResultElement> {
74-
const parser = getParser(textDocument);
74+
// TODO improve detection mechanism
7575
const text: string = textDocument.getText();
76-
const result = await parser.parse(text, { sourceMap: true });
76+
const async = isAsyncDoc(textDocument);
77+
const json = isJsonDoc(textDocument);
78+
let result;
79+
if (async && json) {
80+
result = await asyncapi2Adapter.parse(text, { sourceMap: true });
81+
} else if (async && !json) {
82+
result = await asyncapi2Adapter_Yaml.parse(text, { sourceMap: true });
83+
} else if (!async && json) {
84+
result = await openapi3_1Adapter.parse(text, { sourceMap: true });
85+
} else if (!async && !json) {
86+
result = await openapi3_1Adapter_Yaml.parse(text, { sourceMap: true });
87+
} else {
88+
// fallback
89+
result = await openapi3_1Adapter.parse(text, { sourceMap: true });
90+
}
7791
const { api } = result;
7892
if (api === undefined) return result;
7993
const docNs: string = isAsyncDoc(text) ? 'asyncapi' : 'openapi';

packages/apidom-ls/src/services/completion/completion-service.ts

Lines changed: 150 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -205,16 +205,14 @@ export class DefaultCompletionService implements CompletionService {
205205
// find the current node
206206
const node = findAtOffset({ offset, includeRightBound: true }, api);
207207
// only if we have a node
208-
// TODO add jsonSchema completion, see experiments/apidom-monaco and vscode-json-languageservice
209208
if (node) {
210-
// const sm = getSourceMap(node);
211209
const caretContext = this.resolveCaretContext(node, offset);
212210
const completionNode = this.resolveCompletionNode(node, caretContext);
213-
// const completionNodeSm = getSourceMap(completionNode);
214211
const completionNodeContext = this.resolveCompletionNodeContext(caretContext);
215212
// const currentWord = DefaultCompletionService.getCurrentWord(textDocument, offset);
216213

217-
let overwriteRange: Range;
214+
let overwriteRange: Range | undefined;
215+
let quotes: string | undefined;
218216

219217
const supportsCommitCharacters = false; // this.doesSupportsCommitCharacters(); disabled for now, waiting for new API: https://github.com/microsoft/vscode/issues/42544
220218

@@ -225,6 +223,7 @@ export class DefaultCompletionService implements CompletionService {
225223
const item: CompletionItem = JSON.parse(JSON.stringify(suggestion));
226224
let { label } = item;
227225
const existing = proposed[label];
226+
// don't suggest properties that are already present
228227
if (!existing) {
229228
label = label.replace(/[\n]/g, '↵');
230229
if (label.length > 60) {
@@ -257,9 +256,8 @@ export class DefaultCompletionService implements CompletionService {
257256
},
258257
};
259258

260-
// don't suggest properties that are already present
261259
if (
262-
isObject(completionNode) && // TODO added to get type check on node
260+
isObject(completionNode) &&
263261
(CompletionNodeContext.OBJECT === completionNodeContext ||
264262
CompletionNodeContext.VALUE_OBJECT === completionNodeContext) &&
265263
(caretContext === CaretContext.KEY_INNER ||
@@ -273,12 +271,6 @@ export class DefaultCompletionService implements CompletionService {
273271
proposed[p.key.toValue()] = CompletionItem.create('__');
274272
}
275273
}
276-
}
277-
278-
if (schema) {
279-
// TODO complete schema based, see json language service and "lsp" branch
280-
// DefaultCompletionService.getJsonSchemaPropertyCompletions(schema, api, node, addValue, separatorAfter, collector);
281-
} else {
282274
const inNewLine = text.substring(offset, text.indexOf('\n', offset)).trim().length === 0;
283275
DefaultCompletionService.getMetadataPropertyCompletions(
284276
api,
@@ -287,6 +279,116 @@ export class DefaultCompletionService implements CompletionService {
287279
!isJsonDoc(textDocument),
288280
inNewLine,
289281
);
282+
} else if (
283+
// in a primitive value node
284+
!isObject(completionNode) &&
285+
(caretContext === CaretContext.MEMBER ||
286+
caretContext === CaretContext.PRIMITIVE_VALUE_INNER ||
287+
caretContext === CaretContext.PRIMITIVE_VALUE_END ||
288+
caretContext === CaretContext.PRIMITIVE_VALUE_START)
289+
) {
290+
const inNewLine = text.substring(offset, text.indexOf('\n', offset)).trim().length === 0;
291+
const nodeSourceMap = getSourceMap(completionNode);
292+
// TODO Apidom doesn't hold quotes in its content currently, therefore we must use text + offset
293+
const nodeValueFromText = text.substring(nodeSourceMap.offset, nodeSourceMap.endOffset);
294+
quotes =
295+
nodeValueFromText.charAt(0) === '"' || nodeValueFromText.charAt(0) === "'"
296+
? nodeValueFromText.charAt(0)
297+
: undefined;
298+
const word = DefaultCompletionService.getCurrentWord(textDocument, offset);
299+
proposed[completionNode.toValue()] = CompletionItem.create('__');
300+
proposed[nodeValueFromText] = CompletionItem.create('__');
301+
let withQuotes = false;
302+
// if node is not empty we must replace text
303+
if (nodeValueFromText.length > 0) {
304+
/*
305+
cases:
306+
307+
- quoted string, offset inside quotes
308+
- quoted string, offset before quotes
309+
- quoted empty string, offset before quotes
310+
- quoted empty string, offset before quoted
311+
- non quoted string
312+
*/
313+
314+
enum CompletionOffsetContextEnum {
315+
NON_QUOTED,
316+
QUOTED_INSIDE,
317+
QUOTED_BEFORE,
318+
EMPTY_QUOTED_INSIDE,
319+
EMPTY_QUOTED_BEFORE,
320+
}
321+
322+
let completionOffsetContext = CompletionOffsetContextEnum.NON_QUOTED;
323+
324+
if (quotes && offset > nodeSourceMap.offset && completionNode.toValue().length > 0) {
325+
completionOffsetContext = CompletionOffsetContextEnum.QUOTED_INSIDE;
326+
} else if (
327+
quotes &&
328+
offset <= nodeSourceMap.offset &&
329+
completionNode.toValue().length > 0
330+
) {
331+
completionOffsetContext = CompletionOffsetContextEnum.QUOTED_BEFORE;
332+
} else if (
333+
quotes &&
334+
offset <= nodeSourceMap.offset &&
335+
completionNode.toValue().length === 0
336+
) {
337+
completionOffsetContext = CompletionOffsetContextEnum.EMPTY_QUOTED_BEFORE;
338+
} else if (
339+
quotes &&
340+
offset > nodeSourceMap.offset &&
341+
completionNode.toValue().length === 0
342+
) {
343+
completionOffsetContext = CompletionOffsetContextEnum.EMPTY_QUOTED_INSIDE;
344+
}
345+
const location = { offset: nodeSourceMap.offset, length: nodeSourceMap.length };
346+
let targetRangeStart = location.offset;
347+
let targetRangeEnd = location.offset + location.length;
348+
349+
if (completionOffsetContext === CompletionOffsetContextEnum.QUOTED_INSIDE) {
350+
withQuotes = false;
351+
targetRangeStart = location.offset + 1;
352+
targetRangeEnd = location.offset - 1 + location.length;
353+
} else if (completionOffsetContext === CompletionOffsetContextEnum.QUOTED_BEFORE) {
354+
withQuotes = true;
355+
targetRangeStart = location.offset;
356+
targetRangeEnd = location.offset + location.length;
357+
} else if (completionOffsetContext === CompletionOffsetContextEnum.EMPTY_QUOTED_INSIDE) {
358+
withQuotes = false;
359+
targetRangeStart = location.offset + 1;
360+
targetRangeEnd = location.offset - 1 + location.length;
361+
} else if (completionOffsetContext === CompletionOffsetContextEnum.EMPTY_QUOTED_BEFORE) {
362+
withQuotes = true;
363+
targetRangeStart = location.offset;
364+
targetRangeEnd = location.offset + location.length;
365+
}
366+
367+
overwriteRange = Range.create(
368+
textDocument.positionAt(targetRangeStart),
369+
textDocument.positionAt(targetRangeEnd),
370+
);
371+
} else {
372+
// node is empty
373+
overwriteRange = undefined;
374+
}
375+
DefaultCompletionService.getMetadataPropertyCompletions(
376+
api,
377+
completionNode,
378+
collector,
379+
!isJsonDoc(textDocument),
380+
inNewLine,
381+
word,
382+
quotes,
383+
withQuotes,
384+
);
385+
}
386+
387+
if (schema) {
388+
// TODO complete schema based, see json language service and "lsp" branch
389+
// DefaultCompletionService.getJsonSchemaPropertyCompletions(schema, api, node, addValue, separatorAfter, collector);
390+
} else {
391+
//
290392
}
291393
}
292394

@@ -356,19 +458,48 @@ export class DefaultCompletionService implements CompletionService {
356458
collector: CompletionsCollector,
357459
yaml: boolean,
358460
inNewLine: boolean,
461+
word?: string,
462+
quotes?: string,
463+
withQuotes?: boolean,
359464
): void {
360-
const apidomCompletions: CompletionItem[] = doc.meta
361-
.get('metadataMap')
362-
?.get(node.element)
363-
?.get(yaml ? 'yaml' : 'json')
364-
?.get('completion')
365-
?.toValue();
465+
const apidomCompletions: CompletionItem[] = [];
466+
if (node.classes) {
467+
const set: string[] = Array.from(new Set(node.classes.toValue()));
468+
set.unshift(node.element);
469+
set.forEach((s) => {
470+
const classCompletions: CompletionItem[] = doc.meta
471+
.get('metadataMap')
472+
?.get(s)
473+
?.get(yaml ? 'yaml' : 'json')
474+
?.get('completion')
475+
?.toValue();
476+
if (classCompletions) {
477+
apidomCompletions.push(...classCompletions);
478+
}
479+
});
480+
}
481+
366482
if (apidomCompletions) {
367483
for (const item of apidomCompletions) {
484+
if (withQuotes) {
485+
const completionTextQuotes =
486+
item.insertText?.charAt(0) === '"' || item.insertText?.charAt(0) === "'"
487+
? item.insertText?.charAt(0)
488+
: undefined;
489+
if (!completionTextQuotes && quotes) {
490+
item.insertText = quotes + item.insertText + quotes;
491+
}
492+
}
368493
if (inNewLine) {
369494
item.insertText = item.insertText?.substring(0, item.insertText?.length - 1);
370495
}
371-
collector.add(item);
496+
497+
const strippedQuotesWord = quotes && word ? word.substring(1, word.length) : word!;
498+
if (word && word.length > 0 && item.insertText?.startsWith(strippedQuotesWord)) {
499+
collector.add(item);
500+
} else if (!word) {
501+
collector.add(item);
502+
}
372503
}
373504
}
374505
}

packages/apidom-ls/src/services/validation/linter-functions.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,30 @@ export const standardLinterfunctions = [
1515
return true;
1616
},
1717
},
18+
{
19+
functionName: 'apilintFieldValueRegex',
20+
function: (element: Element, key: string, regexString: string): boolean => {
21+
if (element && isObject(element)) {
22+
if (element.get(key)) {
23+
const regex = new RegExp(regexString);
24+
if (!regex.test(element.get(key).toValue())) {
25+
return false;
26+
}
27+
}
28+
}
29+
return true;
30+
},
31+
},
32+
{
33+
functionName: 'apilintValueRegex',
34+
function: (element: Element, regexString: string): boolean => {
35+
if (element) {
36+
const regex = new RegExp(regexString);
37+
if (!regex.test(element.toValue())) {
38+
return false;
39+
}
40+
}
41+
return true;
42+
},
43+
},
1844
];

packages/apidom-ls/src/services/validation/providers/asyncapi-20-json-schema-validation-provider.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,9 @@ export class Asyncapi20JsonSchemaValidationProvider extends JsonSchemaValidation
1616
namespaces(): string[] {
1717
return ['asyncapi'];
1818
}
19+
20+
// eslint-disable-next-line class-methods-use-this
21+
name(): string {
22+
return 'asyncapi schema';
23+
}
1924
}

packages/apidom-ls/src/services/validation/providers/json-schema-validation-provider.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,10 @@ export abstract class JsonSchemaValidationProvider implements ValidationProvider
8484
return;
8585
}
8686
let range: Range;
87-
const errorOnValue = error.keyword === 'pattern' || error.keyword === 'format';
87+
const errorOnValue =
88+
error.keyword === 'pattern' ||
89+
error.keyword === 'format' ||
90+
error.keyword === 'errorMessage';
8891
// if errors are related to root, mark only the first char
8992
if (!error.instancePath || error.instancePath.length === 0) {
9093
const endChar = !originalDocument || originalDocument.length === 0 ? 0 : 1;
@@ -127,14 +130,17 @@ export abstract class JsonSchemaValidationProvider implements ValidationProvider
127130
error.message || '',
128131
DiagnosticSeverity.Error,
129132
0,
133+
this.name(),
130134
);
131135
diagnostics.push(diagnostic);
132136
});
133137
}
134138
}
135139
}
136140

137-
abstract break(): boolean;
141+
public abstract break(): boolean;
138142

139-
abstract namespaces(): string[];
143+
public abstract namespaces(): string[];
144+
145+
public abstract name(): string;
140146
}

packages/apidom-ls/src/services/validation/providers/openapi-31-json-schema-validation-provider.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,9 @@ export class OpenAPi31JsonSchemaValidationProvider extends JsonSchemaValidationP
2121
namespaces(): string[] {
2222
return ['openapi'];
2323
}
24+
25+
// eslint-disable-next-line class-methods-use-this
26+
name(): string {
27+
return 'openapi schema';
28+
}
2429
}

0 commit comments

Comments
 (0)