Skip to content

Commit 5a57181

Browse files
committed
Implement editor auto format on save
1 parent 4e42fa8 commit 5a57181

File tree

2 files changed

+144
-28
lines changed

2 files changed

+144
-28
lines changed

src/formatter.ts

Lines changed: 118 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -268,39 +268,130 @@ export class JupyterlabFileEditorCodeFormatter extends JupyterlabCodeFormatter {
268268
}
269269

270270
formatAction(config: any, formatter: string) {
271+
return this.formatEditor(config, { saving: false }, formatter);
272+
}
273+
274+
public async formatEditor(
275+
config: any,
276+
context: Context,
277+
formatter?: string,
278+
) {
271279
if (this.working) {
272280
return;
273281
}
274-
const editorWidget = this.editorTracker.currentWidget;
275-
this.working = true;
276-
const editor = editorWidget!.content.editor;
277-
const code = editor.model.sharedModel.source;
278-
this.formatCode(
279-
[code],
280-
formatter,
281-
config[formatter],
282-
false,
283-
config.cacheFormatters
284-
)
285-
.then(data => {
286-
if (data.code[0].error) {
287-
void showErrorMessage(
288-
'Jupyterlab Code Formatter Error',
289-
data.code[0].error
282+
try {
283+
this.working = true;
284+
285+
const formattersToUse = await this.getFormattersToUse(config, formatter);
286+
await this.applyFormatters(
287+
formattersToUse,
288+
config,
289+
context
290+
);
291+
} catch (error) {
292+
await showErrorMessage('Jupyterlab Code Formatter Error', error);
293+
}
294+
this.working = false;
295+
}
296+
297+
private getEditorType() {
298+
if (!this.editorTracker.currentWidget) {
299+
return null;
300+
}
301+
302+
const mimeType =
303+
this.editorTracker.currentWidget.content.model!.mimeType;
304+
305+
const mimeTypes = new Map([
306+
['text/x-python', 'python'],
307+
['application/x-rsrc', 'r'],
308+
['application/x-scala', 'scala'],
309+
['application/x-rustsrc', 'rust'],
310+
['application/x-c++src', 'cpp'], // Not sure that this is right, whatever.
311+
// Add more MIME types and corresponding programming languages here
312+
]);
313+
314+
return mimeTypes.get(mimeType);
315+
}
316+
317+
private getDefaultFormatters(config: any): Array<string> {
318+
const editorType = this.getEditorType();
319+
if (editorType) {
320+
const defaultFormatter =
321+
config.preferences.default_formatter[editorType];
322+
if (defaultFormatter instanceof Array) {
323+
return defaultFormatter;
324+
} else if (defaultFormatter !== undefined) {
325+
return [defaultFormatter];
326+
}
327+
}
328+
return [];
329+
}
330+
331+
private async getFormattersToUse(config: any, formatter?: string) {
332+
const defaultFormatters = this.getDefaultFormatters(config);
333+
const formattersToUse =
334+
formatter !== undefined ? [formatter] : defaultFormatters;
335+
336+
if (formattersToUse.length === 0) {
337+
await showErrorMessage(
338+
'Jupyterlab Code Formatter Error',
339+
'Unable to find default formatters to use, please file an issue on GitHub.'
340+
);
341+
}
342+
343+
return formattersToUse;
344+
}
345+
346+
private async applyFormatters(
347+
formattersToUse: string[],
348+
config: any,
349+
context: Context
350+
) {
351+
for (const formatterToUse of formattersToUse) {
352+
if (formatterToUse === 'noop' || formatterToUse === 'skip') {
353+
continue;
354+
}
355+
const showErrors =
356+
!(config.suppressFormatterErrors ?? false) &&
357+
!(
358+
(config.suppressFormatterErrorsIFFAutoFormatOnSave ?? false) &&
359+
context.saving
290360
);
291-
this.working = false;
292-
return;
293-
}
294-
this.editorTracker.currentWidget!.content.editor.model.sharedModel.source =
295-
data.code[0].code;
296-
this.working = false;
297-
})
298-
.catch(error => {
299-
this.working = false;
300-
void showErrorMessage('Jupyterlab Code Formatter Error', error);
301-
});
361+
362+
const editorWidget = this.editorTracker.currentWidget;
363+
this.working = true;
364+
const editor = editorWidget!.content.editor;
365+
const code = editor.model.sharedModel.source;
366+
this.formatCode(
367+
[code],
368+
formatterToUse,
369+
config[formatterToUse],
370+
false,
371+
config.cacheFormatters
372+
)
373+
.then(data => {
374+
if (data.code[0].error) {
375+
if (showErrors) {
376+
void showErrorMessage(
377+
'Jupyterlab Code Formatter Error',
378+
data.code[0].error
379+
);
380+
}
381+
this.working = false;
382+
return;
383+
}
384+
this.editorTracker.currentWidget!.content.editor.model.sharedModel.source =
385+
data.code[0].code;
386+
this.working = false;
387+
})
388+
.catch(error => {
389+
void showErrorMessage('Jupyterlab Code Formatter Error', error);
390+
});
391+
}
302392
}
303393

394+
304395
applicable(formatter: string, currentWidget: Widget) {
305396
const currentEditorWidget = this.editorTracker.currentWidget;
306397
// TODO: Handle showing just the correct formatter for the language later

src/index.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DocumentRegistry } from '@jupyterlab/docregistry';
1+
import { DocumentRegistry, DocumentWidget, DocumentModel } from '@jupyterlab/docregistry';
22
import {
33
INotebookModel,
44
INotebookTracker,
@@ -113,8 +113,33 @@ class JupyterLabCodeFormatter
113113
}
114114
}
115115

116+
private createNewEditor(widget: DocumentWidget, context: DocumentRegistry.IContext<DocumentModel>): IDisposable {
117+
// Connect to save(State) signal, to be able to detect document save event
118+
context.saveState.connect(this.onSaveEditor, this);
119+
// Return an empty disposable, because we don't create any object
120+
return new DisposableDelegate(() => { });
121+
}
122+
123+
private async onSaveEditor(
124+
context: DocumentRegistry.IContext<DocumentModel>,
125+
state: DocumentRegistry.SaveState
126+
) {
127+
if (state === 'started' && this.config.formatOnSave) {
128+
this.fileEditorCodeFormatter.formatEditor(
129+
this.config,
130+
{saving: true},
131+
undefined,
132+
)
133+
}
134+
}
135+
116136
private setupWidgetExtension() {
117137
this.app.docRegistry.addWidgetExtension('Notebook', this);
138+
this.app.docRegistry.addWidgetExtension('editor', {
139+
createNew: (widget: DocumentWidget, context: DocumentRegistry.IContext<DocumentModel>): IDisposable => {
140+
return this.createNewEditor(widget, context);
141+
}
142+
});
118143
}
119144

120145
private setupContextMenu() {

0 commit comments

Comments
 (0)