Get active cell type in JupyterLab extension - jupyter-lab

I'm trying to write a simple extension with commands to navigate cells. The point of these commands is to automatically enter edit mode after movement unless target cell is of text or markdown type. Following this guide I've added command that executes sequence of some pre-built commands. But I'm stuck on creating condition for cell type, where can I get info about current cell?
import {
JupyterFrontEnd,
JupyterFrontEndPlugin,
} from '#jupyterlab/application';
/**
* Initialization data for the commands example.
*/
const extension: JupyterFrontEndPlugin<void> = {
id: 'commands',
autoStart: true,
activate: (app: JupyterFrontEnd) => {
const { commands } = app;
const command = 'my_command';
// Add a command
commands.addCommand(command, {
label: 'Move down & enter edit mode',
caption: 'Move down & enter edit mode',
execute: (args: any) => {
commands.execute('notebook:enter-command-mode');
commands.execute('notebook:move-cursor-down');
// here I need to check if we are on the code cell
commands.execute('notebook:enter-edit-mode');
},
});
},
};
export default extension;

Related

Accessing notebook cell metadata and HTML class attributes in JupyterLab Extensions

In a minimum viable JupyterLab extension, as for example tested using the JupyterLab Plugin Playground, how can I add a toolbar button that will toggle a particular class attribute on the HTML associated with one or more selected notebook cells (either code cell or markdown cell)?
To generalise the example further:
how would I apply different class attributes to code cells and markdown cells?
how would I add a class to the HTML based on the the presence of a particular metadata attribute or metadata tag element in the notebook JSON structure?
As a starting point the following code (taken from the JupyterLab extension examples) should add a button to the toolbar:
import { IDisposable, DisposableDelegate } from '#lumino/disposable';
import {
JupyterFrontEnd,
JupyterFrontEndPlugin,
} from '#jupyterlab/application';
import { ToolbarButton } from '#jupyterlab/apputils';
import { DocumentRegistry } from '#jupyterlab/docregistry';
import {
NotebookActions,
NotebookPanel,
INotebookModel,
} from '#jupyterlab/notebook';
/**
* The plugin registration information.
*/
const plugin: JupyterFrontEndPlugin<void> = {
activate,
id: 'toolbar-button',
autoStart: true,
};
/**
* A notebook widget extension that adds a button to the toolbar.
*/
export class ButtonExtension
implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel>
{
/**
* Create a new extension for the notebook panel widget.
*
* #param panel Notebook panel
* #param context Notebook context
* #returns Disposable on the added button
*/
createNew(
panel: NotebookPanel,
context: DocumentRegistry.IContext<INotebookModel>
): IDisposable {
const myButtonAction = () => {
// Perform some action
};
const button = new ToolbarButton({
className: 'my-action-button',
label: 'My Button',
onClick: myButtonAction,
tooltip: 'Perform My Button action',
});
panel.toolbar.insertItem(10, 'myNewAction', button);
return new DisposableDelegate(() => {
button.dispose();
});
}
}
/**
* Activate the extension.
*
* #param app Main application object
*/
function activate(app: JupyterFrontEnd): void {
app.docRegistry.addWidgetExtension('Notebook', new ButtonExtension());
}
/**
* Export the plugin as default.
*/
export default plugin;
You shall start by getting a handle of the Notebook class instance (which is the content of the notebook panel which you already have available):
// in anonymous function assigned to myButtonAction
const notebook = panel.content;
The notebook instance provides you with the active Cell widget:
const activeCell = notebook.activeCell;
The cell widget has two attributes of interest: model which enables you to access the metadata, and node which enables manipulation of the DOM structure.
For example, you could toggle class of the node of a cell if it is a markdown cell (=ICellModel has .type (CellType) equal to 'markdown'):
if (activeCell.model.type === 'markdown') {
activeCell.node.classList.toggle('someClass');
}
The metadata is stored in cell.model.metadata.
For selection of cells something as follows should work:
const {head, anchor} = notebook.getContiguousSelection();
if (head === null || anchor === null) {
// no selection
return;
}
const start = head > anchor ? anchor : head;
const end = head > anchor ? head : anchor;
for (let cellIndex = start; cellIndex <= end; cellIndex++) {
const cell = notebook.widgets[cellIndex];
if (cell.model.type === 'code') {
cell.node.classList.toggle('someOtherClass');
}
}
There is a problem with this approach however, as when the notebook gets opened in a separate view, or simply reloaded, the classes will go away (as they are only toggled on the DOM nodes). If you require persistence, I would recommend to:
use the button to only write to cell metadata
add a separate callback function which will listen to any changes to the notebook model, roughly (not tested!):
// in createNew()
const notebook = panel.content;
notebook.modelChanged.connect((notebook) => {
// iterate cells and toggle DOM classes as needed, e.g.
for (const cell of notebook.widgets) {
if (cell.model.metadata.get('someMetaData')) {
cell.node.classList.toggle('someOtherClass');
}
}
});
which should also work (in principle) with collaborative editing.

Eslint rule is running multiple times

I'm trying to write an eslint rule that enforces making sure the name property is defined on any classes that extend from other Error/Exception named classes (and fixes them).
As far as I can tell, it works in the astexplorer.net individually, but when I'm running it alongside other rules, it ends up getting ran multiple times, so the name property ends up being repeated multiple times in the resulting "fixed" file.
Is there anything in particular I can do to prevent it being run multiple times? I'm assuming what's happening is that it's inserting my name = 'ClassName';, then prettier is needing to reformat the code, which it does, but then maybe it's re-running my rule? I'm not sure.
Rule/fix code shown below. I've tried things like using *fix and yield, but that doesn't seem to help either (see commented code below, based on information in the eslint documentation)
module.exports = {
meta: {
hasSuggestions: true,
type: 'suggestion',
docs: {},
fixable: 'code',
schema: [], // no options,
},
create: function (context) {
return {
ClassDeclaration: function (node) {
const regex = /.*(Error|Exception)$/;
// If the parent/superClass is has "Error" or "Exception" in the name
if (node.superClass && regex.test(node.superClass.name)) {
let name = null;
const className = node.id.name;
// Test class object name
if (!regex.test(className)) {
context.report({
node: node,
message: 'Error extensions must end with "Error" or "Exception".',
});
}
// Find name ClassProperty
node.body.body.some(function (a) {
if (a.type === 'ClassProperty' && a.key.name === 'name') {
name = a.value.value;
return true;
}
});
// Name property is required
if (!name) {
context.report({
node: node,
message: 'Error extensions should have a descriptive name',
fix(fixer) {
return fixer.replaceTextRange(
[node.body.range[0]+1, node.body.range[0]+1],
`name = '${className}';`
);
},
// *fix(fixer) {
// name = className;
// yield fixer.replaceTextRange(
// [node.body.range[0]+1, node.body.range[0]+1],
// `name = '${className}';`
// );
//
// // extend range of the fix to the range of `node.parent`
// yield fixer.insertTextBefore(node.body, '');
// yield fixer.insertTextAfter(node.body, '');
// },
});
}
}
},
};
},
};
Turns out I had the AST Explorer set to the wrong parser, so it was showing me the wrong string name for the ClassProperty node. I should have been using PropertyDefinition instead.

Tiptap how to create a paragraph (p) on Shift-Enter, instead of a br?

Using TipTap, I'm trying to avoid adding a <br />, but create a <p></p> instead, with the focus inside that <p>|</p> when the user hit shift-Enter but I can't make it work.
Here's what I did so far:
new (class extends Extension {
keys () {
return {
'Shift-Enter' (state, dispatch, view) {
const { schema, tr } = view.state
const paragraph = schema.nodes.paragraph
console.log(tr.storedMarks)
const transaction = tr.deleteSelection().replaceSelectionWith(paragraph.create(), true).scrollIntoView()
view.dispatch(transaction)
return true
}
}
}
})()
How can I do this?
I don't know if this is still relevant but as I was looking for the same thing, I found two ways to make this work.
NOTE:
I'm using tiptap v2, if that's not a problem, then:
I overrode the HardBreak extension, since it's the one that use the Shift-Enter keybinding. It looks something like;
const CustomHardBreak = HardBreak.extend({
addKeyboardShortcuts() {
return {
"Mod-Enter": () => this.editor.commands.setHardBreak(),
"Shift-Enter": () => this.editor.commands.addNewline(),
};
},
});
And used it like so;
editor = new Editor({
extensions: [
customNewline,
CustomHardBreak,
]
});
Use the default editor command createParagraphNear. E.g this.editor.commands.createParagraphNear()
I tried creating a custom extension from your code and ended up with something similar to the command above, i.e;
export const customNewline = Extension.create({
name: "newline",
priority: 1000, // Optional
addCommands() {
return {
addNewline:
() =>
({ state, dispatch }) => {
const { schema, tr } = state;
const paragraph = schema.nodes.paragraph;
const transaction = tr
.deleteSelection()
.replaceSelectionWith(paragraph.create(), true)
.scrollIntoView();
if (dispatch) dispatch(transaction);
return true;
},
};
},
addKeyboardShortcuts() {
return {
"Shift-Enter": () => this.editor.commands.addNewline(),
};
},
});
And added this as an extension in my editor instance.
PS:
They both work, almost exactly the same, I haven't found a difference yet. But there's somewhat of a 'catch' if you would call it that; Both these methods don't work on empty lines/nodes, a character has to be added before the cursor for it to work, any character, even a space.
In TipTap 2.0 I am able to use this custom extension:
const ShiftEnterCreateExtension = Extension.create({
addKeyboardShortcuts() {
return {
"Shift-Enter": ({ editor }) => {
editor.commands.enter();
return true;
},
};
},
});
To make shift + enter behave like enter.
In my case I actually wanted enter to do something different. So I use prosemirror events to set a ref flag on whether shift was pressed. Than I check that flag under the "Enter" keyboard event -- which could be triggered normally or through the shift + enter extension.

Cucumber Puppeteer: error trying to call step function with arguments

Below is code where I try to include arguments in a Then statement (last line) when I am calling a step function
I get an error message stating invalid third argument.
So how do I pass parameters to the function?
async function confirmSheetCreated(folderName,sheetName) {
await this.page.waitFor(1500);
let sheetExists=await this.sheetExists(folderName,sheetName)
await this.page.isTrue(sheetExists);
}
When('I click plus button in top menu', { timeout: 5*1000 }, clickAddSheet);
When('I click Sheet option in dropdown', { timeout: 5*1000 }, clickSelectSheet);
When('I fill in New Sheet Lookup dialog', { timeout: 5*1000 },fillNewLookupSheetDialog);
When('I click Create New Sheet button',{ timeout: 5*1000 },clickCreateNewSheet);
Then( 'I confirm new sheet created',{ timeout: 5*1000 }, confirmSheetCreated('Revenue','Test Sheet 1'));
The key is that the parameters are created in the cucumber feature file by putting the values in quotes like this:
I confirm "Test Sheet 1" created in the "Revenue" folder
Then in the step you enter placeholders like this:
Then ('I confirm {string} created in the {string} folder,{timeout:5*1000},confirmSheetCreated);
and finally the function looks like this:
async function confirmSheetCreated(sheetName, folderName) {
await this.page.waitFor(1500);
let sheetExists=await this.sheetExists(folderName,sheetName)
await this.page.isTrue(sheetExists);
}
The first string becames the sheet name, and the second the Folder.

AutoFocus on Framework 7 Searchbar not working

So i have a mobile app with 5 tabs one of which is the search tab and i would like that every time i click on the search tab the search comes automatically enabled and the keyboard pops up also meaning that the cursor will be ready in place.I search in the html in the browser and found out that everytime i click on the search bar to write something a new class is added called input-focused so i tried to add this automatically using a methos called enable and which is calling the method searchEnabled but still its not working... any ideas?
<f7-searchbar
#searchbar:enable="searchEnabled"
#searchbar:search="search"
search-container=".search-list"
search-in=".item-title"
></f7-searchbar>
methods: {
search (searchbar, query) {
if (query === '') {
this.results = []
} else {
this.filter.q = query
Search.filter(this.filter).then(({data}) => {
this.results = data
})
.catch((error) => {
console.log('error:', error)
})
}
},
searchEnabled (searchbar) {
searchbar.$inputEl[0].classList.add(['input-focused'])
}
}

Resources