Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
alexarchambault committed Sep 29, 2020
1 parent 1db69fa commit 2e404e2
Show file tree
Hide file tree
Showing 6 changed files with 649 additions and 244 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/camelcase': 'warn',
'@typescript-eslint/quotes': [
'error',
'single',
Expand Down
31 changes: 28 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ name: Build

on:
push:
branches: master
branches: main
tags:
- "v*"
pull_request:
branches: '*'

jobs:
build:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Install node
uses: actions/setup-node@v1
with:
Expand All @@ -31,3 +33,26 @@ jobs:
jupyter labextension install .
python -m jupyterlab.browser_check
publish:
needs: test
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Install Python
uses: actions/setup-python@v1
with:
python-version: '3.7'
architecture: 'x64'
- name: Install dependencies
run: python -m pip install jupyterlab
- name: Publish
run: jlpm && jlpm publish --access public
env:
NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"singleQuote": true
"singleQuote": true,
"trailingComma": "none"
}
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "almond-scalafmt",
"version": "0.1.0",
"name": "@almond-sh/scalafmt",
"version": "0.0.0",
"description": "Scalafmt extension for almond",
"keywords": [
"jupyter",
Expand All @@ -15,7 +15,8 @@
"author": "Alex Archambault",
"files": [
"lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
"style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}"
"style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
"style/*.css"
],
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand All @@ -33,15 +34,16 @@
"watch": "tsc -w"
},
"dependencies": {
"@jupyterlab/application": "^2.0.0"
"@jupyterlab/application": "^2.0.0",
"@jupyterlab/notebook": "^2.0.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^2.25.0",
"@typescript-eslint/parser": "^2.25.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1",
"eslint-plugin-prettier": "^3.1.2",
"prettier": "1.16.4",
"prettier": "^2.1.2",
"rimraf": "^2.6.1",
"typescript": "~3.7.0"
},
Expand Down
235 changes: 233 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,246 @@ import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';
import { ICommandPalette } from '@jupyterlab/apputils';
import { ICellModel } from '@jupyterlab/cells';
import { INotebookTracker } from '@jupyterlab/notebook';
import { Kernel, KernelMessage } from '@jupyterlab/services';
import { IShellMessage } from '@jupyterlab/services/lib/kernel/messages';
import { UUID } from '@lumino/coreutils';

interface IFormatResponseContent {
key: string;
initial_code: string;
code: string | null;
}

function scalafmtConf(notebookTracker: INotebookTracker): Record<string, any> {
let conf: Record<string, any> = {};
const maybeConf = notebookTracker.currentWidget.model.metadata.get(
'scalafmt'
);
if (maybeConf.constructor === {}.constructor) {
conf = maybeConf as Record<string, any>;
}
return conf;
}

function getCells(
allCells: boolean,
notebookTracker: INotebookTracker
): {
cellDict: { [cellId: string]: ICellModel };
requestCells: { [key: string]: string };
} {
const cellDict: { [cellId: string]: ICellModel } = {};
const requestCells: { [key: string]: string } = {};

if (allCells) {
const notebookModel = notebookTracker.currentWidget.model;

if (!notebookModel) {
console.log('no notebook model found');
return;
}

const it = notebookModel.cells.iter();
let cellModel = it.next();
while (cellModel) {
const cellId = cellModel.id;
const initialCode = cellModel.value.text;
if (cellModel.type === 'code' && initialCode.length > 0) {
requestCells[cellId] = initialCode;
cellDict[cellId] = cellModel;
}
cellModel = it.next();
}
} else {
const cellModel = notebookTracker.activeCell?.model;

if (!cellModel) {
console.log('no active cell found for formatting');
return;
}

if (cellModel.type !== 'code') {
console.log('active cell is not a code cell');
return;
}

const cellId = cellModel.id;
const initialCode = cellModel.value.text;

if (initialCode.length === 0) {
console.log('nothing to format');
return;
}

requestCells[cellId] = initialCode;
cellDict[cellId] = cellModel;
}

return { cellDict, requestCells };
}

function handleResponse(
cellDict: { [cellId: string]: ICellModel },
requestCells: { [key: string]: string }
): (msg: KernelMessage.IIOPubMessage<KernelMessage.IOPubMessageType>) => void {
return (msg) => {
if ((msg.header.msg_type as string) === 'format_response') {
const response = msg.content as IFormatResponseContent;
if (response.key in cellDict && response.key in requestCells) {
const cellModel = cellDict[response.key];
const initialCode = requestCells[response.key];
const codeObservable = cellModel.value;
if (
response.code &&
response.initial_code === initialCode &&
codeObservable.text === initialCode
) {
// FIXME Seems there's no way to set the content in one go
codeObservable.insert(0, response.code);
codeObservable.remove(
response.code.length,
codeObservable.text.length
);
} else {
console.log(
'Cell code changed, not updating it with stale formatted code'
);
}
}
}
};
}

function formatCellsMessage(
requestCells: { [key: string]: string },
scalafmtConf: Record<string, any>,
kernel: Kernel.IKernelConnection
): IShellMessage {
const msg = {
channel: 'shell',
content: {
cells: requestCells,
conf: scalafmtConf
},
metadata: {},
parent_header: {},
header: {
date: new Date().toISOString(),
msg_id: UUID.uuid4(),
msg_type: 'format_request',
session: kernel.clientId,
username: kernel.username ?? '',
version: '5.2'
}
};

return msg as IShellMessage;
}

function formatCells(
notebookTracker: INotebookTracker,
kernels: { [id: string]: Kernel.IKernelConnection },
allCells: boolean
): void {
if (!notebookTracker.currentWidget) {
console.log('no current widget found for formatting');
return;
}

const notebookId = notebookTracker.currentWidget.id;
const conf = scalafmtConf(notebookTracker);

if (!(notebookId in kernels)) {
console.log('no kernel found for panel ' + notebookId);
return;
}

const kernel = kernels[notebookId];

const { cellDict, requestCells } = getCells(allCells, notebookTracker);

if (Object.keys(cellDict).length === 0) {
console.log('nothing to format');
return;
}

const msg = formatCellsMessage(requestCells, conf, kernel);
const future = kernel.sendShellMessage(msg, true, true);
future.onIOPub = handleResponse(cellDict, requestCells);
}

function watchKernelChanged(
notebookTracker: INotebookTracker,
kernels: { [id: string]: Kernel.IKernelConnection }
): void {
notebookTracker.widgetAdded.connect((sender, nbPanel) => {
nbPanel.sessionContext.kernelChanged.connect((sender, args) => {
const kernel = args.newValue;
if (kernel) {
kernels[nbPanel.id] = kernel;
} else {
delete kernels[nbPanel.id];
}
});
});
}

const formatCurrentCellCommand = 'almond:scalafmt-current-cell';
const formatAllCellsCommand = 'almond:scalafmt-all-cells';
const category = 'Scala';

function activateExtension(
app: JupyterFrontEnd,
palette: ICommandPalette,
notebookTracker: INotebookTracker
): void {
const kernels: { [id: string]: Kernel.IKernelConnection } = {};

app.commands.addCommand(formatCurrentCellCommand, {
label: 'Format current cell with scalafmt',
caption: 'Format current cell with scalafmt',
execute: (args: any) => {
formatCells(notebookTracker, kernels, false);
}
});
app.commands.addCommand(formatAllCellsCommand, {
label: 'Format all code cells with scalafmt',
caption: 'Format all code cells with scalafmt',
execute: (args: any) => {
formatCells(notebookTracker, kernels, true);
}
});

palette.addItem({
command: formatCurrentCellCommand,
category,
args: { origin: 'palette' }
});
palette.addItem({
command: formatAllCellsCommand,
category,
args: { origin: 'palette' }
});

watchKernelChanged(notebookTracker, kernels);
}

/**
* Initialization data for the almond-scalafmt extension.
*/
const extension: JupyterFrontEndPlugin<void> = {
id: 'almond-scalafmt',
autoStart: true,
activate: (app: JupyterFrontEnd) => {
console.log('JupyterLab extension almond-scalafmt is activated!');
requires: [ICommandPalette, INotebookTracker],
activate: (
app: JupyterFrontEnd,
palette: ICommandPalette,
notebookTracker: INotebookTracker
) => {
activateExtension(app, palette, notebookTracker);
}
};

Expand Down
Loading

0 comments on commit 2e404e2

Please sign in to comment.