From bbfafd62802456862b3959344ec8107c2f3afd3b Mon Sep 17 00:00:00 2001 From: liushitong Date: Tue, 12 Aug 2025 12:01:00 +0800 Subject: [PATCH] [LSP]: Support UI decorator completion Issue: https://gitee.com/openharmony/arkcompiler_ets_frontend/issues/ICSFRT Signed-off-by: liushitong Change-Id: If196ca92c9a7557549d1e21b7efafcce0c36c6ab --- ets2panda/bindings/test/cases.ts | 4 +- .../expected/getCompletionAtPosition.json | 39 +++++++++++++ .../getCompletionsAtPosition16.ets | 23 ++++++++ .../getCompletionsAtPosition17.ets | 24 ++++++++ ets2panda/lsp/include/completions.h | 3 +- ets2panda/lsp/src/completions.cpp | 58 +++++++++++++++++++ ets2panda/test/unit/lsp/get_completions_1.cpp | 53 +++++++++++++++++ 7 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 ets2panda/bindings/test/testcases/getCompletionAtPosition/getCompletionsAtPosition16.ets create mode 100644 ets2panda/bindings/test/testcases/getCompletionAtPosition/getCompletionsAtPosition17.ets diff --git a/ets2panda/bindings/test/cases.ts b/ets2panda/bindings/test/cases.ts index b742dad5a4..81bba3a2e1 100644 --- a/ets2panda/bindings/test/cases.ts +++ b/ets2panda/bindings/test/cases.ts @@ -138,7 +138,9 @@ export const basicCases: TestCases = { '12': [resolveTestPath('test/testcases/getCompletionAtPosition/getCompletionsAtPosition12.ets'), 720], '13': [resolveTestPath('test/testcases/getCompletionAtPosition/getCompletionsAtPosition13.ets'), 658], '14': [resolveTestPath('test/testcases/getCompletionAtPosition/getCompletionsAtPosition14.ets'), 659], - '15': [resolveTestPath('test/testcases/getCompletionAtPosition/getCompletionsAtPosition15.ets'), 722] + '15': [resolveTestPath('test/testcases/getCompletionAtPosition/getCompletionsAtPosition15.ets'), 722], + '16': [resolveTestPath('test/testcases/getCompletionAtPosition/getCompletionsAtPosition17.ets'), 764], + '17': [resolveTestPath('test/testcases/getCompletionAtPosition/getCompletionsAtPosition17.ets'), 782] }, toLineColumnOffset: { expectedFilePath: resolveTestPath('test/expected/toLineColumnOffset.json'), diff --git a/ets2panda/bindings/test/expected/getCompletionAtPosition.json b/ets2panda/bindings/test/expected/getCompletionAtPosition.json index 9a1ba0a4ab..a437e80c65 100644 --- a/ets2panda/bindings/test/expected/getCompletionAtPosition.json +++ b/ets2panda/bindings/test/expected/getCompletionAtPosition.json @@ -216,5 +216,44 @@ "kind": 2, "data": null } + ], + "16": [ + { + "name": "Entry", + "sortText": "15", + "insertText": "", + "kind": 27, + "data": null + }, + { + "name": "Entry2", + "sortText": "15", + "insertText": "", + "kind": 27, + "data": null + } + ], + "17": [ + { + "name": "Entry", + "sortText": "15", + "insertText": "", + "kind": 27, + "data": null + }, + { + "name": "Entry2", + "sortText": "15", + "insertText": "", + "kind": 27, + "data": null + }, + { + "name": "TestAnnotation", + "sortText": "15", + "insertText": "", + "kind": 27, + "data": null + } ] } diff --git a/ets2panda/bindings/test/testcases/getCompletionAtPosition/getCompletionsAtPosition16.ets b/ets2panda/bindings/test/testcases/getCompletionAtPosition/getCompletionsAtPosition16.ets new file mode 100644 index 0000000000..c0d3b068da --- /dev/null +++ b/ets2panda/bindings/test/testcases/getCompletionAtPosition/getCompletionsAtPosition16.ets @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export @interface Entry { + routeName: string = ""; + storage: string = ""; +} +export @interface TestAnnotation { + routeName: string = ""; + storage: string = ""; +} diff --git a/ets2panda/bindings/test/testcases/getCompletionAtPosition/getCompletionsAtPosition17.ets b/ets2panda/bindings/test/testcases/getCompletionAtPosition/getCompletionsAtPosition17.ets new file mode 100644 index 0000000000..9630f909cf --- /dev/null +++ b/ets2panda/bindings/test/testcases/getCompletionAtPosition/getCompletionsAtPosition17.ets @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entry, TestAnnotation } from './getCompletionsAtPosition16'; +export @interface Entry2 { + routeName: string = ""; + storage: string = ""; +} +@E +struct Index {} +@ +struct Index1 {} diff --git a/ets2panda/lsp/include/completions.h b/ets2panda/lsp/include/completions.h index 2aa0f39252..9b3c85492b 100644 --- a/ets2panda/lsp/include/completions.h +++ b/ets2panda/lsp/include/completions.h @@ -53,7 +53,8 @@ enum class CompletionEntryKind { EVENT = 23, OPERATOR = 24, TYPE_PARAMETER = 25, - ALIAS_TYPE = 26 + ALIAS_TYPE = 26, + ANNOTATION = 27 }; namespace sort_text { diff --git a/ets2panda/lsp/src/completions.cpp b/ets2panda/lsp/src/completions.cpp index 8b1961ff2f..1fbf46c0d1 100644 --- a/ets2panda/lsp/src/completions.cpp +++ b/ets2panda/lsp/src/completions.cpp @@ -711,6 +711,58 @@ CompletionEntry InitEntry(const ir::AstNode *decl) return CompletionEntry(name, kind, std::string(sortText)); } +bool IsAnnotationBeginning(std::string sourceCode, size_t pos) +{ + return sourceCode.at(pos - 1) == '@'; +} + +std::vector GetAnnotationCompletions(es2panda_Context *context, size_t pos, + ir::AstNode *node = nullptr) +{ + std::vector completions; + auto ctx = reinterpret_cast(context); + auto ast = ctx->parserProgram->Ast(); + auto importStatements = std::vector(); + auto annotationDeclarations = std::vector(); + for (auto &statement : ast->Statements()) { + if (statement != nullptr && statement->IsETSImportDeclaration()) { + importStatements.push_back(statement); + } + if (statement != nullptr && statement->Range().end.index < pos && statement->IsAnnotationDeclaration()) { + annotationDeclarations.push_back(statement); + } + } + auto addAnnotationCompletion = [&completions, &node](const std::string &name) { + if (node == nullptr || + (node->IsIdentifier() && name.find(node->AsIdentifier()->Name().Utf8()) != std::string::npos)) { + completions.emplace_back(name, CompletionEntryKind::ANNOTATION, + std::string(sort_text::GLOBALS_OR_KEYWORDS)); + } + }; + for (auto &import : importStatements) { + auto specifiers = import->AsETSImportDeclaration()->Specifiers(); + for (auto &specifier : specifiers) { + std::string localName; + ir::AstNode *decl = nullptr; + if (specifier->IsImportSpecifier()) { + localName = std::string(specifier->AsImportSpecifier()->Local()->AsIdentifier()->Name()); + decl = compiler::DeclarationFromIdentifier(specifier->AsImportSpecifier()->Imported()); + } else if (specifier->IsImportDefaultSpecifier()) { + localName = std::string(specifier->AsImportDefaultSpecifier()->Local()->AsIdentifier()->Name()); + decl = compiler::DeclarationFromIdentifier(specifier->AsImportDefaultSpecifier()->Local()); + } + if (decl != nullptr && decl->IsAnnotationDeclaration()) { + addAnnotationCompletion(localName); + } + } + } + for (auto &annotation : annotationDeclarations) { + auto annotationName = std::string(annotation->AsAnnotationDeclaration()->GetBaseName()->Name()); + addAnnotationCompletion(annotationName); + } + return completions; +} + void GetIdentifiersInScope(const varbinder::Scope *scope, size_t position, ArenaVector &results) { if (scope->Node() == nullptr) { @@ -822,11 +874,17 @@ std::vector GetCompletionsAtPositionImpl(es2panda_Context *cont } auto allocator = ctx->allocator; std::string sourceCode(ctx->parserProgram->SourceCode()); + if (IsAnnotationBeginning(sourceCode, pos)) { + return GetAnnotationCompletions(context, pos); // need to filter annotation + } // Current GetPrecedingPosition cannot get token of "obj." with position. auto precedingToken = FindPrecedingToken(pos, ctx->parserProgram->Ast(), allocator); if (precedingToken == nullptr) { return {}; } + if (IsAnnotationBeginning(sourceCode, precedingToken->Start().index)) { + return GetAnnotationCompletions(context, pos, precedingToken); // need to filter annotation + } auto triggerValue = GetCurrentTokenValueImpl(context, pos); // Unsupported yet because of ast parsing problem if (IsEndWithValidPoint(triggerValue)) { diff --git a/ets2panda/test/unit/lsp/get_completions_1.cpp b/ets2panda/test/unit/lsp/get_completions_1.cpp index e7a0b9af4e..101f0cc34a 100644 --- a/ets2panda/test/unit/lsp/get_completions_1.cpp +++ b/ets2panda/test/unit/lsp/get_completions_1.cpp @@ -389,4 +389,57 @@ let myColor: Color = Color.R)delimiter"}; initializer.DestroyContext(ctx); ASSERT_EQ(entry1, entries[0]); } + +TEST_F(LSPCompletionsTests, getCompletionsAtPositionAnnotation1) +{ + std::vector files = {"CompletionAnnotation1.ets", "CompletionAnnotation2.ets"}; + std::vector texts = {R"( +export @interface Entry { + routeName: string = ""; + storage: string = ""; +} +export @interface TestAnnotation { + routeName: string = ""; + storage: string = ""; +} +)", + R"( +import { Entry, TestAnnotation } from './CompletionAnnotation1'; +export @interface Entry2 { + routeName: string = ""; + storage: string = ""; +} +@E +struct Index {} +@ +struct Index1 {} +)"}; + auto filePaths = CreateTempFile(files, texts); + + int const expectedFileCount = 2; + ASSERT_EQ(filePaths.size(), expectedFileCount); + + LSPAPI const *lspApi = GetImpl(); + size_t const offset1 = 151; + size_t const offset2 = 169; + Initializer initializer = Initializer(); + auto ctx = initializer.CreateContext(filePaths[1].c_str(), ES2PANDA_STATE_CHECKED); + auto res1 = lspApi->getCompletionsAtPosition(ctx, offset1); + auto res2 = lspApi->getCompletionsAtPosition(ctx, offset2); + auto firstExpectedEntries = std::vector { + CompletionEntry("Entry", ark::es2panda::lsp::CompletionEntryKind::ANNOTATION, std::string(GLOBALS_OR_KEYWORDS)), + CompletionEntry("Entry2", ark::es2panda::lsp::CompletionEntryKind::ANNOTATION, + std::string(GLOBALS_OR_KEYWORDS))}; + auto firstUnexpectedEntries = std::vector {CompletionEntry( + "TestAnnotation", ark::es2panda::lsp::CompletionEntryKind::ANNOTATION, std::string(GLOBALS_OR_KEYWORDS))}; + auto secondExpectedEntries = std::vector { + CompletionEntry("Entry", ark::es2panda::lsp::CompletionEntryKind::ANNOTATION, std::string(GLOBALS_OR_KEYWORDS)), + CompletionEntry("Entry2", ark::es2panda::lsp::CompletionEntryKind::ANNOTATION, + std::string(GLOBALS_OR_KEYWORDS)), + CompletionEntry("TestAnnotation", ark::es2panda::lsp::CompletionEntryKind::ANNOTATION, + std::string(GLOBALS_OR_KEYWORDS))}; + AssertCompletionsContainAndNotContainEntries(res1.GetEntries(), firstExpectedEntries, firstUnexpectedEntries); + AssertCompletionsContainAndNotContainEntries(res2.GetEntries(), secondExpectedEntries, {}); + initializer.DestroyContext(ctx); +} } // namespace \ No newline at end of file -- Gitee