Skip to content

Commit

Permalink
Merge pull request #264 from galaxyproject/feature_insert_param_refer…
Browse files Browse the repository at this point in the history
…ence

Add feature Insert param reference/output filter
  • Loading branch information
davelopez authored Sep 30, 2024
2 parents 4d3b747 + d7c5ce1 commit 375f77b
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 20 deletions.
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

0 comments on commit 375f77b

Please sign in to comment.