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"