diff --git a/server/galaxyls/server.py b/server/galaxyls/server.py index cc38831..90e2657 100644 --- a/server/galaxyls/server.py +++ b/server/galaxyls/server.py @@ -22,6 +22,8 @@ DocumentFormattingParams, DocumentLink, DocumentLinkParams, + DocumentSymbol, + DocumentSymbolParams, Hover, INITIALIZED, InitializeParams, @@ -34,6 +36,7 @@ TEXT_DOCUMENT_DID_OPEN, TEXT_DOCUMENT_DID_SAVE, TEXT_DOCUMENT_DOCUMENT_LINK, + TEXT_DOCUMENT_DOCUMENT_SYMBOL, TEXT_DOCUMENT_FORMATTING, TEXT_DOCUMENT_HOVER, TextDocumentIdentifier, @@ -198,6 +201,16 @@ def process_code_actions(server: GalaxyToolsLanguageServer, params: CodeActionPa return server.service.get_available_refactoring_actions(xml_document, params) +@language_server.feature(TEXT_DOCUMENT_DOCUMENT_SYMBOL) +def document_symbol(server: GalaxyToolsLanguageServer, params: DocumentSymbolParams) -> Optional[List[DocumentSymbol]]: + """Returns a list of symbols defined in the document.""" + document = _get_valid_document(server, params.text_document.uri) + if document: + xml_document = _get_xml_document(document) + return server.service.symbols_provider.get_document_symbols(xml_document) + return None + + @language_server.command(Commands.AUTO_CLOSE_TAGS) def auto_close_tag(server: GalaxyToolsLanguageServer, parameters: CommandParameters) -> Optional[AutoCloseTagResult]: """Responds to a close tag request to close the currently opened node.""" diff --git a/server/galaxyls/services/language.py b/server/galaxyls/services/language.py index 47baff3..359f4fc 100644 --- a/server/galaxyls/services/language.py +++ b/server/galaxyls/services/language.py @@ -26,6 +26,7 @@ from galaxyls.services.definitions import DocumentDefinitionsProvider from galaxyls.services.links import DocumentLinksProvider from galaxyls.services.macros import MacroExpanderService +from galaxyls.services.symbols import DocumentSymbolsProvider from galaxyls.services.tools.common import ( TestsDiscoveryService, ToolParamAttributeSorter, @@ -75,6 +76,7 @@ def __init__(self) -> None: self.linter = GalaxyToolLinter() self.definitions_provider: Optional[DocumentDefinitionsProvider] = None self.link_provider = DocumentLinksProvider() + self.symbols_provider = DocumentSymbolsProvider() def set_workspace(self, workspace: Workspace) -> None: macro_definitions_provider = MacroDefinitionsProvider(workspace) diff --git a/server/galaxyls/services/symbols.py b/server/galaxyls/services/symbols.py new file mode 100644 index 0000000..914fd85 --- /dev/null +++ b/server/galaxyls/services/symbols.py @@ -0,0 +1,73 @@ +from typing import ( + List, + Optional, +) + +from lsprotocol.types import ( + DocumentSymbol, + SymbolKind, +) + +from galaxyls.services.xml.document import XmlDocument +from galaxyls.services.xml.nodes import ( + XmlAttribute, + XmlElement, + XmlSyntaxNode, +) +from galaxyls.services.xml.utils import convert_document_offsets_to_range + + +class DocumentSymbolsProvider: + """Provides symbols defined in the tool document.""" + + def get_document_symbols(self, xml_document: XmlDocument) -> List[DocumentSymbol]: + """Gets all symbols defined in the tool document in a hierarchical structure.""" + if xml_document.root is None: + return [] + return [self._get_element_symbol_definition(xml_document, xml_document.root)] + + def _get_element_children_symbols(self, element: XmlElement, xml_document: XmlDocument) -> List[DocumentSymbol]: + result: List[DocumentSymbol] = [] + for child in element.children: + if isinstance(child, XmlAttribute): + result.append(self._get_attribute_symbol_definition(xml_document, child)) + if isinstance(child, XmlElement): + result.append(self._get_element_symbol_definition(xml_document, child)) + return result + + def _get_element_symbol_definition(self, xml_document: XmlDocument, element: XmlElement) -> DocumentSymbol: + element_range = convert_document_offsets_to_range(xml_document.document, element.start, element.end) + return DocumentSymbol( + name=self._get_node_name(element), + kind=SymbolKind.Field, + detail=self._get_element_symbol_detail(element, xml_document), + range=element_range, + selection_range=element_range, + children=self._get_element_children_symbols(element, xml_document), + ) + + def _get_attribute_symbol_definition(self, xml_document: XmlDocument, attribute: XmlAttribute) -> DocumentSymbol: + attribute_range = convert_document_offsets_to_range(xml_document.document, attribute.start, attribute.end) + return DocumentSymbol( + name=self._get_node_name(attribute), + kind=SymbolKind.Property, + detail=attribute.value.unquoted if attribute.value else None, + range=attribute_range, + selection_range=attribute_range, + ) + + def _get_node_name(self, node: XmlSyntaxNode) -> str: + return node.name or "" + + def _get_element_symbol_detail(self, element: XmlElement, xml_document: XmlDocument) -> Optional[str]: + if element.name in ["option", "when", "add", "remove"]: + detail = element.get_attribute_value("value") + elif element.name in ["citation", "validator"]: + detail = element.get_attribute_value("type") + elif element.name in ["requirement", "import"]: + detail = element.get_content(xml_document.document.source) + elif element.name in ["expand"]: + detail = element.get_attribute_value("macro") + else: + detail = element.get_attribute_value("id") or element.get_attribute_value("name") + return detail diff --git a/server/galaxyls/tests/unit/test_symbols.py b/server/galaxyls/tests/unit/test_symbols.py new file mode 100644 index 0000000..0cfd128 --- /dev/null +++ b/server/galaxyls/tests/unit/test_symbols.py @@ -0,0 +1,44 @@ +from galaxyls.services.symbols import DocumentSymbolsProvider +from galaxyls.tests.unit.utils import TestUtils + +FIELD_SYMBOL_KIND = 8 # SymbolKind.Field +PROPERTY_SYMBOL_KIND = 7 # SymbolKind.Property + + +def test_get_document_symbols(): + xml_document = TestUtils.from_source_to_xml_document("") + provider = DocumentSymbolsProvider() + symbols = provider.get_document_symbols(xml_document) + assert len(symbols) == 1 + assert symbols[0].name == "tool" + assert symbols[0].kind == FIELD_SYMBOL_KIND + assert len(symbols[0].children) == 0 + + +def test_get_element_children_symbols(): + xml_document = TestUtils.from_source_to_xml_document("") + provider = DocumentSymbolsProvider() + symbols = provider.get_document_symbols(xml_document) + element_symbol = symbols[0] + children_symbols = element_symbol.children + assert len(children_symbols) == 1 + assert children_symbols[0].name == "command" + assert children_symbols[0].kind == FIELD_SYMBOL_KIND + + +def test_get_attribute_symbol_definition(): + xml_document = TestUtils.from_source_to_xml_document("") + provider = DocumentSymbolsProvider() + symbols = provider.get_document_symbols(xml_document) + attribute_symbol = symbols[0].children[0].children[0] + assert attribute_symbol.name == "executable" + assert attribute_symbol.kind == PROPERTY_SYMBOL_KIND + + +def test_get_element_symbol_detail(): + xml_document = TestUtils.from_source_to_xml_document('') + provider = DocumentSymbolsProvider() + symbols = provider.get_document_symbols(xml_document) + assert len(symbols) == 1 + element_symbol = symbols[0] + assert element_symbol.detail == "TEST"