diff --git a/vscode-antimony/all-requirements.txt b/vscode-antimony/all-requirements.txt index a36641665..0fdd6d23c 100644 --- a/vscode-antimony/all-requirements.txt +++ b/vscode-antimony/all-requirements.txt @@ -19,3 +19,5 @@ bioservices==1.8.3 # ols_client==0.0.9 AMAS-sb==0.0.1 orjson==3.8.0 +SBMLDiagrams +tellurium \ No newline at end of file diff --git a/vscode-antimony/package.json b/vscode-antimony/package.json index 5188d797c..2f282c6e2 100644 --- a/vscode-antimony/package.json +++ b/vscode-antimony/package.json @@ -29,6 +29,7 @@ "onCommand:antimony.switchIndicationOff", "onCommand:antimony.convertAntimonyToSBML", "onCommand:antimony.convertSBMLToAntimony", + "onCommand:antimony.convertAntimonyToDiagram", "onCommand:antimony.startSBMLWebview", "onCommand:antimony.startAntimonyWebview", "onCustomEditor:antimony.sbmlEditor", @@ -122,6 +123,10 @@ "command": "antimony.convertSBMLToAntimony", "title": "Convert to Antimony" }, + { + "command": "antimony.convertAntimonyToDiagram", + "title": "Convert to Diagram" + }, { "command": "antimony.startSBMLWebview", "title": "preview SBML", @@ -189,6 +194,12 @@ "title": "Convert to Antimony", "group": "1_modification", "when": "editorLangId == xml" + }, + { + "command": "antimony.convertAntimonyToDiagram", + "title": "Convert to Diagram", + "group": "1_modification", + "when": "editorLangId == antimony" } ], "editor/title": [ diff --git a/vscode-antimony/src/extension.ts b/vscode-antimony/src/extension.ts index f81b69b22..fcad1dab8 100644 --- a/vscode-antimony/src/extension.ts +++ b/vscode-antimony/src/extension.ts @@ -130,6 +130,13 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('antimony.convertSBMLToAntimony', (...args: any[]) => convertSBMLToAntimony(context, args))); + // SBMLDiagram + context.subscriptions.push( + vscode.commands.registerCommand('antimony.convertAntimonyToDiagram', + (...args: any[]) => { + convertAntimonyToDiagram(context, args); + })); + // custom editor context.subscriptions.push(await SBMLEditorProvider.register(context, client)); context.subscriptions.push(await AntimonyEditorProvider.register(context, client)); @@ -281,6 +288,84 @@ async function checkConversionResult(result, type) { } } +async function convertAntimonyToDiagram(context: vscode.ExtensionContext, args: any[]) { + if (!client) { + utils.pythonInterpreterError(); + return; + } + await client.onReady(); + + await vscode.commands.executeCommand("workbench.action.focusActiveEditorGroup"); + + const doc = vscode.window.activeTextEditor.document; + const uri = doc.uri.toString(); + + let speciesStr; + vscode.commands.executeCommand('antimony.getDiagramQuickpick', uri).then(async (result) => { + speciesStr = result; + let speciesList = speciesStr.species_list.split(' '); + let selectedSpeciesList = await vscode.window.showQuickPick(speciesList, {canPickMany: true, placeHolder: 'select species to include in your diagram'}); + if (selectedSpeciesList && selectedSpeciesList.length === 0) { + vscode.window.showErrorMessage('Please select at least one species!'); + } else { + const options: vscode.OpenDialogOptions = { + openLabel: "Select", + canSelectFolders: true, + canSelectFiles: false, + canSelectMany: false, + filters: { + 'Images': ['png'] + }, + title: "Select a location to save your SBML diagram" + }; + vscode.window.showOpenDialog(options).then(fileUri => { + if (fileUri && fileUri[0]) { + let diagram; + vscode.commands.executeCommand('antimony.antFiletoDiagram', vscode.window.activeTextEditor.document, + fileUri[0].fsPath, selectedSpeciesList).then(async (result) => { + let error = await checkSBMLDiagramResult(result); + diagram = result; + if (!diagram.error) { + const panel = vscode.window.createWebviewPanel( + 'antimony', + 'SBMLDiagram', + vscode.ViewColumn.Two, + { + localResourceRoots: [vscode.Uri.file(path.dirname(diagram.file))] + } + ); + const pngSrc = panel.webview.asWebviewUri(vscode.Uri.file(diagram.file)); + panel.webview.html = getWebviewContent(pngSrc); + } + }); + } + }); + } + }); +} + +function getWebviewContent(uri: vscode.Uri) { + return ` + + + + + SBMLDiagram + + + + + `; + } + +async function checkSBMLDiagramResult(result) { + if (result.error) { + vscode.window.showErrorMessage(`Could not convert file to diagram: ${result.error}`); + } else { + vscode.window.showInformationMessage(`${result.msg}`); + } +} + async function createAnnotationDialog(context: vscode.ExtensionContext, args: any[]) { // wait till client is ready, or the Python server might not have started yet. // note: this is necessary for any command that might use the Python language server. @@ -290,7 +375,6 @@ async function createAnnotationDialog(context: vscode.ExtensionContext, args: an } await client.onReady(); await vscode.commands.executeCommand("workbench.action.focusActiveEditorGroup"); - // dialog for annotation const selection = vscode.window.activeTextEditor.selection; diff --git a/vscode-antimony/src/server/main.py b/vscode-antimony/src/server/main.py index 25b3a2862..78c29a8e0 100644 --- a/vscode-antimony/src/server/main.py +++ b/vscode-antimony/src/server/main.py @@ -36,6 +36,8 @@ import time from AMAS import recommender, species_annotation from bioservices import ChEBI +import SBMLDiagrams +import tellurium as te # TODO remove this for production logging.basicConfig(filename='vscode-antimony-dep.log', filemode='w', level=logging.DEBUG) @@ -149,6 +151,47 @@ def sbml_file_to_ant_file(ls: LanguageServer, args): 'file': full_path_name } +@server.thread() +@server.command('antimony.getDiagramQuickpick') +def ant_file_to_sbml_file(ls: LanguageServer, args): + uri = args[0] + doc = server.workspace.get_document(uri) + antfile_cache = get_antfile(doc) + reaction_list = antfile_cache.analyzer.reaction_list + species_set = set() + for species_names, reaction_part_str in reaction_list: + species_set.update(species_names) + species_list = list(species_set) + species_list.sort() + return { + 'species_list': " ".join(species_list) + } + +@server.thread() +@server.command('antimony.antFiletoDiagram') +def ant_file_to_sbml_file(ls: LanguageServer, args): + ant = args[0].fileName + output_dir = args[1] + selected_species_list = args[2] + model_str = 'model *temp()\n' + reaction_list = antfile_cache.analyzer.reaction_list + for react_prod_list, reaction_str in reaction_list: + if any((match := item) in react_prod_list for item in selected_species_list): + model_str += reaction_str + '\n' + model_str += 'end' + r = te.loada(model_str) + sbmlStr = r.getSBML() + df = SBMLDiagrams.load(sbmlStr) + model_name = os.path.basename(ant) + full_path_name = os.path.join(output_dir, os.path.splitext(model_name)[0]+'_diagram.png') + df.autolayout() + df.draw(output_fileName=full_path_name,showReactionIds=True) + return { + 'msg': 'Diagram has been exported to {}'.format(output_dir), + 'file': full_path_name + } + + @server.thread() @server.command('antimony.sendType') def get_type(ls: LanguageServer, args) -> dict[str, str]: @@ -167,7 +210,6 @@ def get_type(ls: LanguageServer, args) -> dict[str, str]: symbols= antfile_cache.symbols_at(position)[0] symbol = symbols[0].type.__str__() - vscode_logger.info("symbol: " + symbol) return { 'symbol': symbol } diff --git a/vscode-antimony/src/server/stibium/stibium/analysis.py b/vscode-antimony/src/server/stibium/stibium/analysis.py index c0ee2b59d..74ddf84f4 100644 --- a/vscode-antimony/src/server/stibium/stibium/analysis.py +++ b/vscode-antimony/src/server/stibium/stibium/analysis.py @@ -92,6 +92,7 @@ def __init__(self, root: FileNode, path: str): self.unnamed_events_num = 0 base_scope = BaseScope() self.reaction_item = set() + self.reaction_list = list() for child in root.children: if isinstance(child, ErrorToken): continue @@ -492,6 +493,7 @@ def handle_reaction(self, scope: AbstractScope, reaction: Reaction, insert: bool else: self.table.insert(QName(scope, name), SymbolType.Reaction, reaction, comp=comp) + species_names = set() for species in chain(reaction.get_reactants(), reaction.get_products()): if insert: self.import_table.insert(QName(scope, species.get_name()), SymbolType.Species, comp=comp) @@ -499,6 +501,11 @@ def handle_reaction(self, scope: AbstractScope, reaction: Reaction, insert: bool else: self.table.insert(QName(scope, species.get_name()), SymbolType.Species, comp=comp) self.table.get(QName(scope, species.get_name()))[0].in_reaction = True + species_names.add(species.get_name_text()) + reaction_str = reaction.to_string() + reaction_part_str = reaction_str.split(';')[0] + ';' + self.reaction_list.append((species_names, reaction_part_str)) + rate_law = reaction.get_rate_law() if rate_law is not None: self.handle_arith_expr(scope, rate_law, insert) diff --git a/vscode-antimony/src/server/stibium/stibium/ant_types.py b/vscode-antimony/src/server/stibium/stibium/ant_types.py index 9449e268c..3968de0bb 100644 --- a/vscode-antimony/src/server/stibium/stibium/ant_types.py +++ b/vscode-antimony/src/server/stibium/stibium/ant_types.py @@ -417,6 +417,17 @@ def get_comp(self): if self.children[6] is not None: return self.children[6] return None + + def to_string(self) -> str: + ret = '' + if isinstance(self, LeafNode): + ret += self.text + else: + for node in self.descendants(): + if isinstance(node, LeafNode): + ret += node.text + return ret + @dataclass class InteractionName(TrunkNode):