Skip to content

Commit

Permalink
fix(apidom-ls): fix YAML completion in end of doc scenario
Browse files Browse the repository at this point in the history
  • Loading branch information
frantuma committed Nov 18, 2024
1 parent 981b197 commit d631641
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 51 deletions.
120 changes: 69 additions & 51 deletions packages/apidom-ls/src/services/completion/completion-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ export class DefaultCompletionService implements CompletionService {

const text: string = textDocument.getText();
let contentLanguage = await findNamespace(textDocument, this.settings?.defaultContentLanguage);

debug('contentLanguage unprocessed', contentLanguage);
const schema = false;

// commit chars for yaml
Expand All @@ -293,52 +293,6 @@ export class DefaultCompletionService implements CompletionService {
debug('doCompletion - position and offset', position, offset);
trace('doCompletion - text', text);

// if no spec version has been set, provide completion for it anyway
// TODO handle also JSON, must identify offset
// TODO move to adapter
if (
contentLanguage.namespace === 'apidom' &&
contentLanguage.format !== 'JSON' &&
textDocument.positionAt(offset).character === 0
) {
const isEmpty = isEmptyLine(textDocument, offset);
trace('doCompletion - no version', { isEmpty });
const asyncItem = CompletionItem.create('asyncapi');
asyncItem.insertText = `asyncapi: '2.6.0$1'${isEmpty ? '' : '\n'}`;
asyncItem.documentation = {
kind: 'markdown',
value:
'The version string signifies the version of the AsyncAPI Specification that the document complies to.\nThe format for this string _must_ be `major`.`minor`.`patch`. The `patch` _may_ be suffixed by a hyphen and extra alphanumeric characters.\n\\\n\\\nA `major`.`minor` shall be used to designate the AsyncAPI Specification version, and will be considered compatible with the AsyncAPI Specification specified by that `major`.`minor` version.\nThe patch version will not be considered by tooling, making no distinction between `1.0.0` and `1.0.1`.\n\\\n\\\nIn subsequent versions of the AsyncAPI Specification, care will be given such that increments of the `minor` version should not interfere with operations of tooling developed to a lower minor version. Thus a hypothetical `1.1.0` specification should be usable with tooling designed for `1.0.0`.',
};
asyncItem.kind = CompletionItemKind.Keyword;
asyncItem.insertTextFormat = 2;
asyncItem.insertTextMode = 2;
completionList.items.push(asyncItem);
const oasItem = CompletionItem.create('openapi');
oasItem.insertText = `openapi: '3.1.0$1'${isEmpty ? '' : '\n'}`;
oasItem.documentation = {
kind: 'markdown',
value:
'**REQUIRED**. This string MUST be the [version number](#versions) of the OpenAPI Specification that the OpenAPI document uses. The `openapi` field SHOULD be used by tooling to interpret the OpenAPI document. This is *not* related to the API [`info.version`](#infoVersion) string.',
};
oasItem.kind = CompletionItemKind.Keyword;
oasItem.insertTextFormat = 2;
oasItem.insertTextMode = 2;
completionList.items.push(oasItem);
const swaggerItem = CompletionItem.create('swagger');
swaggerItem.insertText = `swagger: '2.0'${isEmpty ? '$1' : '\n$1'}`;
swaggerItem.documentation = {
kind: 'markdown',
value:
'**REQUIRED**. Specifies the Swagger Specification version being used. It can be used by the Swagger UI and other clients to interpret the API listing. The value MUST be "2.0".',
};
swaggerItem.kind = CompletionItemKind.Keyword;
swaggerItem.insertTextFormat = 2;
swaggerItem.insertTextMode = 2;
completionList.items.push(swaggerItem);
trace('doCompletion - no version', `completionList: ${JSON.stringify(completionList)}`);
}

/*
process errored YAML input badly handled by YAML parser (see https://github.com/swagger-api/apidom/issues/194)
similarly to what done in swagger-editor: check if we are in a partial "prefix" scenario, in this case add a `:`
Expand Down Expand Up @@ -391,6 +345,53 @@ export class DefaultCompletionService implements CompletionService {
perfEnd(PerfLabels.PARSE_SECOND);
textModified = true;
}
debug('contentLanguage processed', contentLanguage);
// if no spec version has been set, provide completion for it anyway
// TODO handle also JSON, must identify offset
// TODO move to adapter
if (
contentLanguage.namespace === 'apidom' &&
contentLanguage.format !== 'JSON' &&
textDocument.positionAt(offset).character === 0
) {
const isEmpty = isEmptyLine(textDocument, offset);
trace('doCompletion - no version', { isEmpty });
const asyncItem = CompletionItem.create('asyncapi');
asyncItem.insertText = `asyncapi: '2.6.0$1'${isEmpty ? '' : '\n'}`;
asyncItem.documentation = {
kind: 'markdown',
value:
'The version string signifies the version of the AsyncAPI Specification that the document complies to.\nThe format for this string _must_ be `major`.`minor`.`patch`. The `patch` _may_ be suffixed by a hyphen and extra alphanumeric characters.\n\\\n\\\nA `major`.`minor` shall be used to designate the AsyncAPI Specification version, and will be considered compatible with the AsyncAPI Specification specified by that `major`.`minor` version.\nThe patch version will not be considered by tooling, making no distinction between `1.0.0` and `1.0.1`.\n\\\n\\\nIn subsequent versions of the AsyncAPI Specification, care will be given such that increments of the `minor` version should not interfere with operations of tooling developed to a lower minor version. Thus a hypothetical `1.1.0` specification should be usable with tooling designed for `1.0.0`.',
};
asyncItem.kind = CompletionItemKind.Keyword;
asyncItem.insertTextFormat = 2;
asyncItem.insertTextMode = 2;
completionList.items.push(asyncItem);
const oasItem = CompletionItem.create('openapi');
oasItem.insertText = `openapi: '3.1.0$1'${isEmpty ? '' : '\n'}`;
oasItem.documentation = {
kind: 'markdown',
value:
'**REQUIRED**. This string MUST be the [version number](#versions) of the OpenAPI Specification that the OpenAPI document uses. The `openapi` field SHOULD be used by tooling to interpret the OpenAPI document. This is *not* related to the API [`info.version`](#infoVersion) string.',
};
oasItem.kind = CompletionItemKind.Keyword;
oasItem.insertTextFormat = 2;
oasItem.insertTextMode = 2;
completionList.items.push(oasItem);
const swaggerItem = CompletionItem.create('swagger');
swaggerItem.insertText = `swagger: '2.0'${isEmpty ? '$1' : '\n$1'}`;
swaggerItem.documentation = {
kind: 'markdown',
value:
'**REQUIRED**. Specifies the Swagger Specification version being used. It can be used by the Swagger UI and other clients to interpret the API listing. The value MUST be "2.0".',
};
swaggerItem.kind = CompletionItemKind.Keyword;
swaggerItem.insertTextFormat = 2;
swaggerItem.insertTextMode = 2;
completionList.items.push(swaggerItem);
trace('doCompletion - no version', `completionList: ${JSON.stringify(completionList)}`);
}

if (!result) return completionList;

const { api } = result;
Expand Down Expand Up @@ -474,7 +475,7 @@ export class DefaultCompletionService implements CompletionService {
}
}
}

debug('targetOffset first', targetOffset);
/*
This is a hack to handle empty nodes in YAML, e.g in a situation like:
Expand All @@ -496,6 +497,7 @@ export class DefaultCompletionService implements CompletionService {
}
}
}
debug('targetOffset second', targetOffset);
} else if (contentLanguage.format === 'JSON' && position.character > 0) {
/*
This is a hack to handle empty nodes in JSON, e.g in a situation like:
Expand Down Expand Up @@ -538,6 +540,7 @@ export class DefaultCompletionService implements CompletionService {
}
}
}
debug('targetOffset third', targetOffset);
// check if we are at the end of text, get root node if that's the case
const endOfText =
contentLanguage.format !== 'JSON' &&
Expand All @@ -550,13 +553,28 @@ export class DefaultCompletionService implements CompletionService {
if (endOfText) {
targetOffset = 0;
}
debug('doCompletion - text length', textDocument.getText().length);
debug('doCompletion - trimmed text length', textDocument.getText().trimEnd().length);
let endOfTrimmedText = false;
if (!endOfText) {
endOfTrimmedText =
contentLanguage.format !== 'JSON' &&
textDocument.getText().length > 0 &&
position.character === 0 &&
offset - 1 === textDocument.getText().trimEnd().length;

if (endOfTrimmedText) {
targetOffset = 0;
}
}
trace('doCompletion - text', textDocument.getText());
debug('doCompletion - offset', offset, textDocument.positionAt(offset));
debug('doCompletion - targetOffset', targetOffset, textDocument.positionAt(targetOffset));
// find the current node
const node = endOfText
? api
: findAtOffset({ offset: targetOffset, includeRightBound: true }, api);
const node =
endOfText || endOfTrimmedText
? api
: findAtOffset({ offset: targetOffset, includeRightBound: true }, api);
// only if we have a node
let completionNode: Element | undefined;
if (node) {
Expand Down
103 changes: 103 additions & 0 deletions packages/apidom-ls/test/complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ const specCompletionRequiredJson = fs
.readFileSync(path.join(__dirname, 'fixtures', 'async-required.json'))
.toString();

const specCompletionEmptyLineYamlError = fs
.readFileSync(path.join(__dirname, 'fixtures', 'async-info-newline.yaml'))
.toString();

describe('apidom-ls-complete', function () {
const asyncJsonSchemavalidationProvider = new Asyncapi20JsonSchemaValidationProvider();

Expand Down Expand Up @@ -1467,4 +1471,103 @@ describe('apidom-ls-complete', function () {
},
] as ApidomCompletionItem[]);
});

it('openapi / yaml - test end of trimmed doc with offset 0', async function () {
const completionContext: CompletionContext = {
maxNumberOfItems: 100,
};

const doc: TextDocument = TextDocument.create(
'foo://bar/yamlnewline.yaml',
'yaml',
0,
specCompletionEmptyLineYamlError,
);

const pos = Position.create(4, 0);
const result = await languageService.doCompletion(
doc,
{ textDocument: doc, position: pos },
completionContext,
);
assert.deepEqual(result!.items, [
{
label: 'id',
insertText: 'id: $1',
kind: 14,
insertTextFormat: 2,
documentation: {
kind: 'markdown',
value:
'[Identifier](https://www.asyncapi.com/docs/reference/specification/v2.6.0#A2SIdString)\n\\\n\\\nIdentifier of the [application](https://www.asyncapi.com/docs/reference/specification/v2.6.0#definitionsApplication) the AsyncAPI document is defining. This field represents a unique universal identifier of the [application](#definitionsApplication) the AsyncAPI document is defining. It must conform to the URI format, according to [RFC3986](https://tools.ietf.org/html/rfc3986).\n\\\n\\\nIt is RECOMMENDED to use a [URN](https://tools.ietf.org/html/rfc8141) to globally and uniquely identify the application during long periods of time, even after it becomes unavailable or ceases to exist.',
},
},
{
label: 'servers',
insertText: 'servers: \n $1',
kind: 14,
insertTextFormat: 2,
documentation: {
kind: 'markdown',
value:
'[Servers Object](https://www.asyncapi.com/docs/reference/specification/v2.6.0#serversObject)\n\\\n\\\nProvides connection details of servers. The Servers Object is a map of [Server Objects](https://www.asyncapi.com/docs/reference/specification/v2.6.0#serverObject).',
},
},
{
label: 'defaultContentType',
insertText: 'defaultContentType: $1',
kind: 14,
insertTextFormat: 2,
documentation: {
kind: 'markdown',
value:
"[Default Content Type](https://www.asyncapi.com/docs/reference/specification/v2.6.0#defaultContentTypeString)\n\\\n\\\nDefault content type to use when encoding/decoding a message's payload.\n\\\n\\\nIt's a string representing the default content type to use when encoding/decoding a message's payload. The value MUST be a specific media type (e.g. `application/json`). This value MUST be used by schema parsers when the [contentType](https://www.asyncapi.com/docs/reference/specification/v2.6.0#messageObjectContentType) property is omitted.\n\nIn case a message can't be encoded/decoded using this value, schema parsers MUST use their default content type.",
},
},
{
label: 'channels',
insertText: 'channels: \n $1',
kind: 14,
insertTextFormat: 2,
documentation: {
kind: 'markdown',
value:
'[Channels Object](https://www.asyncapi.com/docs/reference/specification/v2.6.0#channelsObject)\n\\\n\\\n**REQUIRED**. The available channels and messages for the API. Holds the relative paths to the individual channel and their operations. Channel paths are relative to servers. Channels are also known as "topics", "routing keys", "event types" or "paths".',
},
},
{
label: 'components',
insertText: 'components: \n $1',
kind: 14,
insertTextFormat: 2,
documentation: {
kind: 'markdown',
value:
'[Components Object](https://www.asyncapi.com/docs/reference/specification/v2.6.0#componentsObject)\n\\\n\\\nAn element to hold various schemas for the specification. Holds a set of reusable objects for different aspects of the AsyncAPI specification. All objects defined within the components object will have no effect on the API unless they are explicitly referenced from properties outside the components object.',
},
},
{
label: 'tags',
insertText: 'tags: \n - $1',
kind: 14,
insertTextFormat: 2,
documentation: {
kind: 'markdown',
value:
'[Tags Object](https://www.asyncapi.com/docs/reference/specification/v2.6.0#tagsObject)\n\\\n\\\nA list of tags used by the specification with additional metadata. Each tag name in the list **MUST** be unique.',
},
},
{
label: 'externalDocs',
insertText: 'externalDocs: \n $1',
kind: 14,
insertTextFormat: 2,
documentation: {
kind: 'markdown',
value:
'[External Documentation Object](https://www.asyncapi.com/docs/reference/specification/v2.6.0#externalDocumentationObject)\n\\\n\\\nAdditional external documentation. Allows referencing an external resource for extended documentation.',
},
},
] as ApidomCompletionItem[]);
});
});
5 changes: 5 additions & 0 deletions packages/apidom-ls/test/fixtures/async-info-newline.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
asyncapi: '2.2.0'
info:
tit
version: '1.0.0'

0 comments on commit d631641

Please sign in to comment.