Skip to content

Commit

Permalink
allow pairing from extension
Browse files Browse the repository at this point in the history
  • Loading branch information
marius311 committed Nov 14, 2024
1 parent 31de5f0 commit f339a21
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 32 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
# Jupytext Paired Notebooks Extension for VSCode

An extension to handle paired Jupytext notebook files. Opening and saving paired notebook files or scripts will automatically run a `jupytext --sync`, keeping files up to do date with each other.
An extension to handle paired Jupytext notebook files.

The `jupytext` command must be available in the user $PATH or set in the extension settings.
A command `Jupytext: Pair current notebook with script(s)` is provided to pair the current active notebook with one or more script files. The extension suggests a pairing based on the kernel language and default format chosen in the settings, but anything that `jupytext --set-formats` accepts can be chosen.

Does not currently support initial pairing of files, that should be done externally, e.g. with the Jupytext CLI:
Opening and saving paired notebook files or their script counterparts will automatically run a `jupytext --sync`, keeping files up to do date with each other.

```bash
jupytext --set-formats ipynb,py:percent notebook.ipynb
```
The `jupytext` command must be available in the user $PATH or set in the extension settings.

Does not currently support opening a script file "as a notebook".

## Extension Settings

This extension contributes the following settings:

* `jupytext-paired:jupytextCommand`: The command which runs the Jupytext CLI. Can be an absolute path or a command available in the user $PATH (default `jupytext`).
* `jupytext-paired:jupytextCommand`: The command which runs the Jupytext CLI. Can be an absolute path or a command available in the user $PATH (default: `jupytext`).

* `jupytext-paired:defaultFormat`: The default script format to suggest when pairing a notebook, e.g. `light`, `percent`, `markdown`, etc... (default: `light`).

## Release Notes

Expand Down
16 changes: 14 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "jupytext-paired",
"displayName": "Jupytext Paired Notebooks",
"description": "Support for paired Jupytext files.",
"version": "1.0.0",
"version": "1.1.0",
"engines": {
"vscode": "^1.95.0"
},
Expand All @@ -29,9 +29,21 @@
"type": "string",
"default": "jupytext",
"description": "The command which runs the Jupytext CLI. Can be an absolute path or a command available in the user $PATH."
},
"jupytext-paired.defaultFormat": {
"type": "string",
"enum": ["hydrogen", "light", "markdown", "myst", "nomarker", "pandoc", "percent", "quarto", "rmarkdown", "spin", "sphinx"],
"default": "light",
"description": "The default format to suggest when pairing a notebook with a script."
}
}
}
},
"commands": [
{
"command": "jupytext-paired.pairFile",
"title": "Jupytext: Pair current notebook with script(s)"
}
]
},
"scripts": {
"vscode:prepublish": "npm run compile",
Expand Down
83 changes: 60 additions & 23 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,99 @@
import * as vscode from 'vscode';
import path from 'path';
import util from 'util';
import { EXTENSION_FORMATS } from './languages';

const exec = util.promisify(require('child_process').exec);

let jupytextConsole: vscode.OutputChannel;

export async function activate(context: vscode.ExtensionContext) {

jupytextConsole = vscode.window.createOutputChannel("Jupytext");

context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(async event => {
if (_isPossibleNotebookFile(event.fileName) && !event.fileName.endsWith('.ipynb')) {
return _runSync(event.fileName);
return _run_jupytext_sync(event.fileName);
}
}));

context.subscriptions.push(vscode.workspace.onDidOpenNotebookDocument(async event => {
if (event.uri.scheme === 'file' && _isPossibleNotebookFile(event.uri.path)) {
return _runSync(event.uri.path);
return _run_jupytext_sync(event.uri.path);
}
}));

context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(async (document: vscode.TextDocument) => {
if (_isPossibleNotebookFile(document.fileName)) {
return _runSync(document.fileName);
return _run_jupytext_sync(document.fileName);
}
}));

context.subscriptions.push(vscode.workspace.onDidSaveNotebookDocument((document: vscode.NotebookDocument) => {
if (document.uri.scheme === 'file' && _isPossibleNotebookFile(document.uri.path)) {
return _runSync(document.uri.path);
return _run_jupytext_sync(document.uri.path);
}
}));

}
context.subscriptions.push(vscode.commands.registerTextEditorCommand('jupytext-paired.pairFile', async (textEditor: vscode.TextEditor, edit: vscode.TextEditorEdit) => {

export function deactivate() { }
const activeNotebookEditor = vscode.window.activeNotebookEditor;
if (!activeNotebookEditor) { return }

if (activeNotebookEditor.notebook.isUntitled) {
// triggering a save on an untitled notebook via the API
// closes it after saving, so just make the user do it
// (which automatically reopens it via some magic code
// somewhere I don't know how to replicate)
vscode.window.showInformationMessage("Please save the notebook before pairing.")
return
}

const EXTENSION_FORMATS = [
'.ipynb', '.md', '.markdown', '.Rmd', '.py', '.coco', '.R', '.r', '.jl',
'.cpp', '.ss', '.clj', '.scm', '.sh', '.ps1', '.q', '.m', '.wolfram',
'.pro', '.js', '.ts', '.scala', '.rs', '.robot', '.resource', '.cs',
'.fsx', '.fs', '.sos', '.java', '.groovy', '.sage', '.ml', '.hs', '.tcl',
'.mac', '.gp', '.do', '.sas', '.xsh', '.lua', '.go', '.qmd', '.myst',
'.mystnb', '.mnb', '.auto'
];
// for a non-untitled notebook, doing a save is fine
await activeNotebookEditor.notebook.save();

// use the kernel language to determine a default for the script file extension
const language = activeNotebookEditor.notebook.metadata.metadata.kernelspec?.language;
const scriptExt = Object.keys(EXTENSION_FORMATS)[Object.values(EXTENSION_FORMATS).indexOf(language)];
const defaultFormat = vscode.workspace.getConfiguration('jupytext-paired').get<string>('defaultFormat', 'light');
const defaultPairings = scriptExt ? `${scriptExt}:${defaultFormat}` : '';

const formats = await vscode.window.showInputBox({
prompt: `Enter script formats to pair together with the current notebook (see --set-formats in Jupytext docs)`,
value: defaultPairings
})
if (formats) {
const fileName = textEditor.document.fileName;
_run_jupytext(`--set-formats ipynb,${formats} ${fileName}`);
}
}));
}

export function deactivate() {
jupytextConsole.dispose();
}

function _isPossibleNotebookFile(fileName: string): boolean {
const ext = path.extname(fileName);
return EXTENSION_FORMATS.includes(ext);
const ext = path.extname(fileName).slice(1);
return Object.keys(EXTENSION_FORMATS).includes(ext);
}

async function _runSync(fileName: string) {
const jupytextCommand = vscode.workspace.getConfiguration('jupytext-paired').get<string>('jupytextCommand', 'jupytext');
async function _run_jupytext_sync(fileName: string) {
return _run_jupytext(`--sync ${fileName}`);
}

async function _run_jupytext(args: string) {
const jupytext = vscode.workspace.getConfiguration('jupytext-paired').get<string>('jupytextCommand', 'jupytext');
const cmd = `${jupytext} ${args}`
try {
const { stdout, stderr } = await exec(`${jupytextCommand} --sync ${fileName}`);
console.log(stdout);
console.error(stderr);
const { stdout, stderr } = await exec(cmd);
jupytextConsole.appendLine(stdout);
jupytextConsole.appendLine(stderr);
} catch (error) {
vscode.window.showErrorMessage(`Jupytext sync failed for: \`${path.basename(fileName)}\`. See output console for more info.`);
console.error(error);
jupytextConsole.appendLine(String(error));
const selection = await vscode.window.showErrorMessage(`Calling \`${cmd}\` failed.`, "Show Output");
if (selection === "Show Output") {
jupytextConsole.show();
}
};
}
53 changes: 53 additions & 0 deletions src/languages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@

// maps file extensions supported by Jupytext's --set-formats to their
// respective Jupyter kernelspec kernel languages. lots of blanks that
// I don't know, please PR if you need support
export const EXTENSION_FORMATS = {
'ipynb': null,
'md': null,
'markdown': null,
'Rmd': null,
'py': 'python',
'coco': null,
'R': null,
'r': null,
'jl': 'julia',
'cpp': null,
'ss': null,
'clj': null,
'scm': null,
'sh': null,
'ps1': null,
'q': null,
'm': null,
'wolfram': null,
'pro': null,
'js': null,
'ts': null,
'scala': null,
'rs': null,
'robot': null,
'resource': null,
'cs': null,
'fsx': null,
'fs': null,
'sos': null,
'java': null,
'groovy': null,
'sage': null,
'ml': null,
'hs': null,
'tcl': null,
'mac': null,
'gp': null,
'do': null,
'sas': null,
'xsh': null,
'lua': null,
'go': null,
'qmd': null,
'myst': null,
'mystnb': null,
'mnb': null,
'auto': null
};

0 comments on commit f339a21

Please sign in to comment.