Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature Insert param reference/output filter #264

Merged
merged 9 commits into from
Sep 30, 2024
26 changes: 26 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@
"category": "Galaxy Tools",
"enablement": "galaxytools:isActive",
"icon": "$(open-preview)"
},
{
"command": "galaxytools.insert.paramReference",
"title": "Insert a reference to a param element.",
"category": "Galaxy Tools",
"enablement": "galaxytools:isActive",
"icon": "$(insert)",
"when": "editorTextFocus"
},
{
"command": "galaxytools.insert.paramFilterReference",
"title": "Insert a reference to a param element to be used as output filter.",
"category": "Galaxy Tools",
"enablement": "galaxytools:isActive",
"icon": "$(insert)",
"when": "editorTextFocus"
}
],
"keybindings": [
Expand All @@ -154,6 +170,16 @@
"command": "galaxytools.sort.documentParamsAttributes",
"key": "ctrl+alt+s ctrl+alt+d",
"mac": "cmd+alt+s cmd+alt+d"
},
{
"command": "galaxytools.insert.paramReference",
"key": "ctrl+alt+i ctrl+alt+p",
"mac": "cmd+alt+i cmd+alt+p"
},
{
"command": "galaxytools.insert.paramFilterReference",
"key": "ctrl+alt+i ctrl+alt+f",
"mac": "cmd+alt+i cmd+alt+f"
}
],
"configuration": {
Expand Down
41 changes: 41 additions & 0 deletions client/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export namespace Commands {
export const OPEN_TERMINAL_AT_DIRECTORY_ITEM: ICommand = getCommands("openTerminalAtDirectory");
export const GENERATE_EXPANDED_DOCUMENT: ICommand = getCommands("generate.expandedDocument");
export const PREVIEW_EXPANDED_DOCUMENT: ICommand = getCommands("preview.expandedDocument");
export const INSERT_PARAM_REFERENCE: ICommand = getCommands("insert.paramReference");
export const INSERT_PARAM_FILTER_REFERENCE: ICommand = getCommands("insert.paramFilterReference");
}

interface GeneratedSnippetResult {
Expand All @@ -47,6 +49,10 @@ interface ReplaceTextRangeResult {
replace_range: Range;
}

interface ParamReferencesResult {
references: string[];
}

export interface GeneratedExpandedDocument {
content: string;
error_message: string;
Expand All @@ -65,6 +71,8 @@ export function setupCommands(client: LanguageClient, context: ExtensionContext)

setupGenerateExpandedDocument(client, context);

setupInsertParamReference(client, context);

context.subscriptions.push(
commands.registerCommand(Commands.PREVIEW_EXPANDED_DOCUMENT.internal, previewExpandedDocument)
);
Expand Down Expand Up @@ -121,6 +129,15 @@ function setupGenerateTestCases(client: LanguageClient, context: ExtensionContex
context.subscriptions.push(commands.registerCommand(Commands.GENERATE_TEST.internal, generateTest));
}

function setupInsertParamReference(client: LanguageClient, context: ExtensionContext) {
context.subscriptions.push(commands.registerCommand(Commands.INSERT_PARAM_REFERENCE.internal, () => {
pickParamReferenceToInsert(client, Commands.INSERT_PARAM_REFERENCE.external);
}));
context.subscriptions.push(commands.registerCommand(Commands.INSERT_PARAM_FILTER_REFERENCE.internal, () => {
pickParamReferenceToInsert(client, Commands.INSERT_PARAM_FILTER_REFERENCE.external);
}))
}

function setupAutoCloseTags(client: LanguageClient, context: ExtensionContext) {
const tagProvider = async (document: TextDocument, position: Position) => {
let param = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position);
Expand Down Expand Up @@ -212,6 +229,30 @@ async function requestInsertSnippet(client: LanguageClient, command: string) {
}
}

async function pickParamReferenceToInsert(client: LanguageClient, command: string, pickerTitle: string = "Select a parameter reference to insert") {
const activeEditor = window.activeTextEditor;
if (!activeEditor) return;

const document = activeEditor.document;

const param = client.code2ProtocolConverter.asTextDocumentIdentifier(document);
const response = await commands.executeCommand<ParamReferencesResult>(command, param);
if (!response || !response.references || response.references.length === 0) {
return;
}

try {
const selected = await window.showQuickPick(response.references, { title: pickerTitle });
if (!selected) return;

activeEditor.edit(editBuilder => {
editBuilder.insert(activeEditor.selection.active, selected);
});
} catch (err: any) {
window.showErrorMessage(err);
}
}

async function ensureDocumentIsSaved(editor: TextEditor): Promise<Boolean> {
if (editor.document.isDirty) {
await editor.document.save();
Expand Down
2 changes: 2 additions & 0 deletions server/galaxyls/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class Commands:
DISCOVER_TESTS_IN_WORKSPACE = "gls.tests.discoverInWorkspace"
DISCOVER_TESTS_IN_DOCUMENT = "gls.tests.discoverInDocument"
GENERATE_EXPANDED_DOCUMENT = "gls.generate.expandedDocument"
INSERT_PARAM_REFERENCE = "gls.insert.paramReference"
INSERT_PARAM_FILTER_REFERENCE = "gls.insert.paramFilterReference"


class DiagnosticCodes:
Expand Down
27 changes: 27 additions & 0 deletions server/galaxyls/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
CommandParameters,
GeneratedExpandedDocument,
GeneratedSnippetResult,
ParamReferencesResult,
ReplaceTextRangeResult,
TestSuiteInfoResult,
)
Expand Down Expand Up @@ -298,6 +299,32 @@ def discover_tests_in_document_command(
return None


@language_server.command(Commands.INSERT_PARAM_REFERENCE)
async def cmd_insert_param_reference(
server: GalaxyToolsLanguageServer, parameters: CommandParameters
) -> Optional[ParamReferencesResult]:
"""Provides a list of possible parameter references to be inserted in the command section of the document."""
params = convert_to(parameters[0], TextDocumentIdentifier)
document = _get_valid_document(server, params.uri)
if document:
xml_document = _get_xml_document(document)
return server.service.param_references_provider.get_param_command_references(xml_document)
return None


@language_server.command(Commands.INSERT_PARAM_FILTER_REFERENCE)
async def cmd_insert_param_filter_reference(
server: GalaxyToolsLanguageServer, parameters: CommandParameters
) -> Optional[ParamReferencesResult]:
"""Provides a list of possible parameter references to be inserted as output filters."""
params = convert_to(parameters[0], TextDocumentIdentifier)
document = _get_valid_document(server, params.uri)
if document:
xml_document = _get_xml_document(document)
return server.service.param_references_provider.get_param_filter_references(xml_document)
return None


def _validate(server: GalaxyToolsLanguageServer, params) -> None:
"""Validates the Galaxy tool and reports any problem found."""
diagnostics: List[Diagnostic] = []
Expand Down
2 changes: 2 additions & 0 deletions server/galaxyls/services/language.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.references import ParamReferencesProvider
from galaxyls.services.symbols import DocumentSymbolsProvider
from galaxyls.services.tools.common import (
TestsDiscoveryService,
Expand Down Expand Up @@ -78,6 +79,7 @@ def __init__(self) -> None:
self.definitions_provider: Optional[DocumentDefinitionsProvider] = None
self.link_provider = DocumentLinksProvider()
self.symbols_provider = DocumentSymbolsProvider()
self.param_references_provider = ParamReferencesProvider()

def set_workspace(self, workspace: Workspace) -> None:
macro_definitions_provider = MacroDefinitionsProvider(workspace)
Expand Down
72 changes: 72 additions & 0 deletions server/galaxyls/services/references.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from typing import Callable, List, Optional

from galaxyls.services.tools.document import GalaxyToolXmlDocument
from galaxyls.services.xml.document import XmlDocument
from galaxyls.services.xml.nodes import XmlElement
from galaxyls.types import ParamReferencesResult

ReferenceBuilder = Callable[[XmlElement], Optional[str]]


class ParamReferencesProvider:
def get_param_command_references(self, xml_document: XmlDocument) -> Optional[ParamReferencesResult]:
"""Returns a list of references for the input parameters of the tool that can be used in the command section."""
return self._get_param_references(xml_document, self._build_command_reference)

def get_param_filter_references(self, xml_document: XmlDocument) -> Optional[ParamReferencesResult]:
"""Returns a list of references for the input parameters of the tool that can be used in output filters."""
return self._get_param_references(xml_document, self._build_filter_reference)

def _get_param_references(
self, xml_document: XmlDocument, reference_builder: ReferenceBuilder
) -> Optional[ParamReferencesResult]:
tool = GalaxyToolXmlDocument.from_xml_document(xml_document).get_expanded_tool_document()
references = []
params = tool.get_input_params()
for param in params:
reference = reference_builder(param)
if reference:
references.append(reference)
return ParamReferencesResult(references)

def _build_command_reference(self, param: XmlElement) -> Optional[str]:
reference = None
path = self._get_param_path(param)
if path:
reference = f"${'.'.join(path)}"
return reference

def _build_filter_reference(self, param: XmlElement) -> Optional[str]:
reference = None
path = self._get_param_path(param)
if path:
reference = path[0]
for elem in path[1:]:
reference += f"['{elem}']"
return reference

def _get_param_path(self, param: XmlElement) -> List[str]:
path = []
# Skip the first 3 ancestors (document root, tool, inputs) to start at the input element.
ancestors = param.ancestors[3:]
for ancestor in ancestors:
name = ancestor.get_attribute_value("name")
if name:
path.append(name)
name = self._get_param_name(param)
if name:
path.append(name)
return path

def _get_param_name(self, param: XmlElement) -> Optional[str]:
name = param.get_attribute_value("name")
if not name:
name = param.get_attribute_value("argument")
if name:
return self._normalize_argument_name(name)
return name

def _normalize_argument_name(self, argument: str) -> str:
if argument.startswith("--"):
argument = argument[2:]
return argument.replace("-", "_")
29 changes: 29 additions & 0 deletions server/galaxyls/services/tools/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
)

from anytree import find # type: ignore
from galaxy.util import xml_macros
from lsprotocol.types import (
Position,
Range,
)
from lxml import etree
from pygls.workspace import Document

from galaxyls.services.tools.constants import (
Expand Down Expand Up @@ -169,6 +171,17 @@ def get_outputs(self) -> List[XmlElement]:
return outputs.elements
return []

def get_input_params(self) -> List[XmlElement]:
"""Gets the input params of this document as a list of elements.

Returns:
List[XmlElement]: The params defined in the document.
"""
inputs = self.find_element(INPUTS)
if inputs:
return inputs.get_recursive_descendants_with_name("param")
return []

def get_tool_element(self) -> Optional[XmlElement]:
"""Gets the root tool element"""
return self.find_element(TOOL)
Expand Down Expand Up @@ -217,6 +230,22 @@ def get_import_macro_file_range(self, file_path: Optional[str]) -> Optional[Rang
return self.xml_document.get_full_range(imp)
return None

def get_expanded_tool_document(self) -> "GalaxyToolXmlDocument":
"""If the given tool document uses macros, a new tool document with the expanded macros is returned,
otherwise, the same document is returned.
"""
if self.uses_macros:
try:
document = self.document
expanded_tool_tree, _ = xml_macros.load_with_references(document.path)
expanded_tool_tree = cast(etree._ElementTree, expanded_tool_tree) # type: ignore
expanded_source = etree.tostring(expanded_tool_tree, encoding=str)
expanded_document = Document(uri=document.uri, source=expanded_source, version=document.version)
return GalaxyToolXmlDocument(expanded_document)
except BaseException:
return self
return self

def get_tool_id(self) -> Optional[str]:
"""Gets the identifier of the tool"""
tool_element = self.get_tool_element()
Expand Down
21 changes: 1 addition & 20 deletions server/galaxyls/services/tools/generators/snippets.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,10 @@
cast,
)

from galaxy.util import xml_macros
from lsprotocol.types import (
Position,
Range,
)
from lxml import etree
from pygls.workspace import Document

from galaxyls.services.tools.constants import (
DASH,
Expand All @@ -32,7 +29,7 @@ class SnippetGenerator(ABC):

def __init__(self, tool_document: GalaxyToolXmlDocument, tabSize: int = 4) -> None:
self.tool_document = tool_document
self.expanded_document = self._get_expanded_tool_document(tool_document)
self.expanded_document = tool_document.get_expanded_tool_document()
self.tabstop_count: int = 0
self.indent_spaces: str = " " * tabSize
super().__init__()
Expand Down Expand Up @@ -63,22 +60,6 @@ def _find_snippet_insert_position(self) -> Union[Position, Range]:
snippet will be inserted."""
pass

def _get_expanded_tool_document(self, tool_document: GalaxyToolXmlDocument) -> GalaxyToolXmlDocument:
"""If the given tool document uses macros, a new tool document with the expanded macros is returned,
otherwise, the same document is returned.
"""
if tool_document.uses_macros:
try:
document = tool_document.document
expanded_tool_tree, _ = xml_macros.load_with_references(document.path)
expanded_tool_tree = cast(etree._ElementTree, expanded_tool_tree) # type: ignore
expanded_source = etree.tostring(expanded_tool_tree, encoding=str)
expanded_document = Document(uri=document.uri, source=expanded_source, version=document.version)
return GalaxyToolXmlDocument(expanded_document)
except BaseException:
return tool_document
return tool_document

def _get_next_tabstop(self) -> str:
"""Increments the tabstop count and returns the current tabstop
in TextMate format.
Expand Down
9 changes: 9 additions & 0 deletions server/galaxyls/services/xml/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,15 @@ def get_children_with_name(self, name: str) -> List["XmlElement"]:
children = [child for child in self.children if child.name == name]
return list(children)

def get_recursive_descendants_with_name(self, name: str) -> List["XmlElement"]:
descendants = []
for child in self.children:
if child.name == name:
descendants.append(child)
if isinstance(child, XmlElement):
descendants.extend(child.get_recursive_descendants_with_name(name))
return descendants

def get_cdata_section(self) -> Optional["XmlCDATASection"]:
"""Gets the CDATA node inside this element or None if it doesn't have a CDATA section."""
return next((node for node in self.children if type(node) is XmlCDATASection), None)
Expand Down
10 changes: 10 additions & 0 deletions server/galaxyls/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,13 @@ class GeneratedExpandedDocument:

content: Optional[str] = attrs.field(default=None)
error_message: Optional[str] = attrs.field(default=None, alias="errorMessage")


class ParamReferencesResult:
"""Contains information about the references to a parameter in the document."""

def __init__(
self,
references: List[str],
) -> None:
self.references = references