editor: change from ace to monaco

This replaces the ace editor with the monaco editor. It fixes quite a
few small paper-cut bugs and should be easier to add code completion
to in the future.

Many of the snippets are omitted but can be added back later if needed.
Otherwise, we have tried to preserve the existing look and feel as much
as possible.
This commit is contained in:
David Lechner
2021-07-02 20:59:09 -05:00
committed by David Lechner
parent c3851e7139
commit d95c550248
20 changed files with 790 additions and 354 deletions
+1 -1
View File
@@ -84,7 +84,7 @@ describe('context menu', () => {
expect(screen.getByText('Copy')).toBeInTheDocument();
userEvent.type(document.activeElement || document.body, '{esc}');
userEvent.type(screen.getByText('Copy'), '{esc}');
await waitForElementToBeRemoved(() => screen.queryByText('Copy'));
+123 -95
View File
@@ -3,11 +3,12 @@
import { Menu, MenuDivider, MenuItem, ResizeSensor } from '@blueprintjs/core';
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
import { Ace, config } from 'ace-builds';
import tomorrowNightEightiesTheme from 'monaco-themes/themes/Tomorrow-Night-Eighties.json';
import xcodeTheme from 'monaco-themes/themes/Xcode_default.json';
import React from 'react';
import AceEditor from 'react-ace';
import { IAceEditor } from 'react-ace/lib/types';
import MonacoEditor, { monaco } from 'react-monaco-editor';
import { connect } from 'react-redux';
import { IDisposable } from 'xterm';
import { compile } from '../mpy/actions';
import { RootState } from '../reducers';
import { toggleBoolean } from '../settings/actions';
@@ -17,15 +18,9 @@ import { isMacOS } from '../utils/os';
import { setEditSession, storageChanged } from './actions';
import { EditorStringId } from './i18n';
import en from './i18n.en.json';
import * as pybricksMicroPython from './pybricksMicroPython';
import { UntitledHintContribution } from './untitledHint';
import 'ace-builds/src-noconflict/mode-python';
import 'ace-builds/src-noconflict/theme-tomorrow_night_eighties';
import 'ace-builds/src-noconflict/theme-xcode';
import 'ace-builds/src-noconflict/ext-searchbox';
import 'ace-builds/src-noconflict/ext-keybinding_menu';
import 'ace-builds/src-noconflict/ext-language_tools';
import './snippets';
import './editor.scss';
type StateProps = {
@@ -34,7 +29,7 @@ type StateProps = {
};
type DispatchProps = {
onSessionChanged: (session?: Ace.EditSession) => void;
onSessionChanged: (session?: monaco.editor.ICodeEditor) => void;
onProgramStorageChanged: (newValue: string) => void;
onCheck: (script: string) => void;
onToggleDocs: () => void;
@@ -42,17 +37,49 @@ type DispatchProps = {
type EditorProps = StateProps & DispatchProps & WithI18nProps;
const pybricksMicroPythonId = 'pybricks-micropython';
monaco.languages.register({ id: pybricksMicroPythonId });
const toDispose = new Array<IDisposable>();
toDispose.push(
monaco.languages.setMonarchTokensProvider(
pybricksMicroPythonId,
pybricksMicroPython.language,
),
);
toDispose.push(
monaco.languages.registerCompletionItemProvider(
pybricksMicroPythonId,
pybricksMicroPython.completions,
),
);
// https://webpack.js.org/api/hot-module-replacement/
if (module.hot) {
module.hot.dispose(() => {
toDispose.forEach((s) => s.dispose());
});
}
const tomorrowNightEightiesId = 'tomorrow-night-eighties';
monaco.editor.defineTheme(
tomorrowNightEightiesId,
tomorrowNightEightiesTheme as monaco.editor.IStandaloneThemeData,
);
const xcodeId = 'xcode';
monaco.editor.defineTheme(xcodeId, xcodeTheme as monaco.editor.IStandaloneThemeData);
class Editor extends React.Component<EditorProps> implements IContextMenuTarget {
private editorRef: React.RefObject<AceEditor>;
private keyBindings?: Array<{ key: string; command: string }>;
private editorRef: React.RefObject<MonacoEditor>;
constructor(props: EditorProps) {
super(props);
this.editorRef = React.createRef();
}
/** convenience property for getting Ace editor object */
private get editor(): IAceEditor | undefined {
/** convenience property for getting editor object */
private get editor(): monaco.editor.IStandaloneCodeEditor | undefined {
return this.editorRef.current?.editor;
}
@@ -78,77 +105,61 @@ class Editor extends React.Component<EditorProps> implements IContextMenuTarget
const { i18n, darkMode, onSessionChanged, onCheck, onToggleDocs } = this.props;
return (
<div className="h-100" onContextMenu={(e) => handleContextMenu(e, this)}>
<ResizeSensor onResize={(): void => this.editor?.resize()}>
<AceEditor
<ResizeSensor onResize={(): void => this.editor?.layout()}>
<MonacoEditor
ref={this.editorRef}
mode="python"
theme={darkMode ? 'tomorrow_night_eighties' : 'xcode'}
fontSize="16pt"
language={pybricksMicroPythonId}
theme={darkMode ? tomorrowNightEightiesId : xcodeId}
width="100%"
height="100%"
focus={true}
placeholder={i18n.translate(EditorStringId.Placeholder)}
defaultValue={localStorage.getItem('program') || undefined}
editorProps={{ $blockScrolling: true }}
setOptions={{
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true,
options={{
fontSize: 18,
minimap: { enabled: false },
contextmenu: false,
rulers: [80],
}}
onLoad={(e): void => {
// default binding is F2 which conflicts with 'check'
e.commands.byName['toggleFoldWidget'].bindKey = {
win: 'Shift-F2',
mac: 'Shift-F2',
};
// we want to use Ctrl-D for docs toggle, so change
// delete line to VSCode default
e.commands.byName['removeline'].bindKey = {
win: 'Ctrl-Shift-K',
mac: 'Cmd-Shift-K',
};
config.loadModule(
'ace/ext/menu_tools/get_editor_keyboard_shortcuts',
(m) => {
this.keyBindings = m.getEditorKeybordShortcuts(e);
value={localStorage.getItem('program')}
editorDidMount={(e, _m): void => {
// FIXME: editor does not respond to changes in i18n
const untitledHintContribution =
new UntitledHintContribution(
e,
i18n.translate(EditorStringId.Placeholder),
);
e.onDidDispose(() => untitledHintContribution.dispose());
e.addAction({
id: 'pybricks.action.toggleDocs',
label: i18n.translate(EditorStringId.ToggleDocs),
run: () => onToggleDocs(),
keybindings: [
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_D,
],
});
e.addAction({
id: 'pybricks.action.check',
label: i18n.translate(EditorStringId.Check),
run: () => onCheck(e.getValue()),
keybindings: [monaco.KeyCode.F2],
});
e.addAction({
id: 'pybricks.action.save',
label: 'Unused',
run: () => {
// We already automatically save the file
// to local storage after every change, so
// CTRL+S is ignored
console.debug('Ctrl-S ignored');
},
);
config.loadModule('ace/ext/keybinding_menu', (m) =>
m.init(e),
);
}}
onFocus={(_, e): void => {
onSessionChanged(e?.session);
keybindings: [
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S,
],
});
e.focus();
onSessionChanged(e);
}}
onChange={(v): void => {
localStorage.setItem('program', v);
}}
commands={[
{
// command to check current program for errors
name: 'check',
bindKey: { win: 'F2', mac: 'F2' },
exec: (editor) => onCheck(editor.getValue()),
},
{
name: 'toggleDocs',
bindKey: { win: 'Ctrl-D', mac: 'Cmd-D' },
exec: () => onToggleDocs(),
},
{
name: 'save',
bindKey: { win: 'Ctrl-S', mac: 'Cmd-S' },
exec: (): void => {
// We already automatically save the file
// to local storage after every change, so
// CTRL+S is ignored
console.debug('CTRL+S ignored');
},
},
]}
/>
</ResizeSensor>
</div>
@@ -161,21 +172,28 @@ class Editor extends React.Component<EditorProps> implements IContextMenuTarget
<Menu>
<MenuItem
onClick={(): void => {
const selected = this.editor?.getSelectedText();
if (selected) {
navigator.clipboard.writeText(selected);
}
this.editor?.focus();
this.editor?.trigger(
null,
'editor.action.clipboardCopyAction',
null,
);
}}
text={i18n.translate(EditorStringId.Copy)}
icon="duplicate"
label={isMacOS() ? 'Cmd-C' : 'Ctrl-C'}
disabled={this.editor?.getSelection().isEmpty()}
disabled={
!this.editor?.getSelection() ||
this.editor?.getSelection()?.isEmpty()
}
/>
<MenuItem
onClick={async (): Promise<void> => {
this.editor?.execCommand(
'paste',
await navigator.clipboard.readText(),
this.editor?.focus();
this.editor?.trigger(
null,
'editor.action.clipboardPasteAction',
null,
);
}}
text={i18n.translate(EditorStringId.Paste)}
@@ -183,33 +201,43 @@ class Editor extends React.Component<EditorProps> implements IContextMenuTarget
label={isMacOS() ? 'Cmd-V' : 'Ctrl-V'}
/>
<MenuItem
onClick={() => this.editor?.selectAll()}
onClick={() => {
this.editor?.focus();
this.editor?.trigger(null, 'editor.action.selectAll', null);
}}
text={i18n.translate(EditorStringId.SelectAll)}
icon="blank"
label={isMacOS() ? 'Cmd-A' : 'Ctrl-A'}
/>
<MenuDivider />
<MenuItem
onClick={(): void => this.editor?.undo()}
onClick={(): void => {
this.editor?.focus();
this.editor?.trigger(null, 'undo', null);
}}
text={i18n.translate(EditorStringId.Undo)}
icon="undo"
label={this.keyBindings?.find((x) => x.command === 'undo')?.key}
disabled={!this.editor?.session.getUndoManager().canUndo()}
label={isMacOS() ? 'Cmd-Z' : 'Ctrl-Z'}
// @ts-expect-error internal method canUndo()
disabled={!this.editor?.getModel()?.canUndo()}
/>
<MenuItem
onClick={(): void => this.editor?.redo()}
onClick={(): void => {
this.editor?.focus();
this.editor?.trigger(null, 'redo', null);
}}
text={i18n.translate(EditorStringId.Redo)}
icon="redo"
label={this.keyBindings?.find((x) => x.command === 'redo')?.key}
active
disabled={!this.editor?.session.getUndoManager().canRedo()}
label={isMacOS() ? 'Cmd-Shift-Z' : 'Ctrl-Shift-Z'}
// @ts-expect-error internal method canUndo()
disabled={!this.editor?.getModel()?.canRedo()}
/>
</Menu>
);
}
onContextMenuClose = () => {
this.editorRef.current?.editor.focus();
this.editor?.focus();
};
}
+4 -4
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2021 The Pybricks Authors
import { Ace } from 'ace-builds';
import { monaco } from 'react-monaco-editor';
import { Action } from 'redux';
export enum EditorActionType {
@@ -28,7 +28,7 @@ export enum EditorActionType {
}
export type CurrentEditorAction = Action<EditorActionType.Current> & {
editSession: Ace.EditSession | undefined;
editSession: monaco.editor.ICodeEditor | undefined;
};
/**
@@ -36,7 +36,7 @@ export type CurrentEditorAction = Action<EditorActionType.Current> & {
* @param editSession The new edit session.
*/
export function setEditSession(
editSession: Ace.EditSession | undefined,
editSession: monaco.editor.ICodeEditor | undefined,
): CurrentEditorAction {
return { type: EditorActionType.Current, editSession };
}
+16 -10
View File
@@ -5,25 +5,19 @@
@import '../variables.scss';
// make ace editor match app backgound color
// make editor match app backgound color
.#{$ns}-dark .ace_gutter {
.#{$ns}-dark .margin-view-overlays {
background-color: $pt-dark-app-background-color;
}
.ace_gutter {
.margin-view-overlays {
background-color: $pt-app-background-color;
}
// higher contrast
.ace-tomorrow-night-eighties .ace_print-margin {
background: $dark-gray4;
}
// add "BETA" watermark
.pb-beta .ace_scroller::after {
.pb-beta .editor-scrollable::after {
content: '';
background: url('./beta.svg');
opacity: 1;
@@ -34,3 +28,15 @@
position: absolute;
pointer-events: none;
}
.#{$ns}-dark .pb-editor-placeholder {
color: $pt-dark-text-color-muted;
}
.pb-editor-placeholder {
pointer-events: none;
width: max-content;
color: $pt-text-color-muted;
font-style: italic;
padding-left: 4px;
}
+2
View File
@@ -1,6 +1,8 @@
{
"editor": {
"placeholder": "Write your program here...",
"check": "Check Syntax",
"toggleDocs": "Toggle Documentation",
"copy": "Copy",
"paste": "Paste",
"selectAll": "Select All",
+3 -1
View File
@@ -1,10 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2021 The Pybricks Authors
//
// Editor translation keys.
export enum EditorStringId {
Placeholder = 'editor.placeholder',
Check = 'editor.check',
ToggleDocs = 'editor.toggleDocs',
Copy = 'editor.copy',
Paste = 'editor.paste',
SelectAll = 'editor.selectAll',
+406
View File
@@ -0,0 +1,406 @@
// Copied from https://github.com/microsoft/monaco-languages/blob/main/src/python/python.ts
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { monaco } from 'react-monaco-editor';
export const language = <monaco.languages.IMonarchLanguage>{
defaultToken: '',
tokenPostfix: '.python',
keywords: [
// This section is the result of running
// `for k in keyword.kwlist: print(' "' + k + '",')` in a Python REPL,
// though note that the output from Python 3 is not a strict superset of the
// output from Python 2.
'False', // promoted to keyword.kwlist in Python 3
'None', // promoted to keyword.kwlist in Python 3
'True', // promoted to keyword.kwlist in Python 3
'and',
'as',
'assert',
'async', // new in Python 3
'await', // new in Python 3
'break',
'class',
'continue',
'def',
'del',
'elif',
'else',
'except',
'exec', // Python 2, but not 3.
'finally',
'for',
'from',
'global',
'if',
'import',
'in',
'is',
'lambda',
'nonlocal', // new in Python 3
'not',
'or',
'pass',
'print', // Python 2, but not 3.
'raise',
'return',
'try',
'while',
'with',
'yield',
'int',
'float',
'long',
'complex',
'hex',
'abs',
'all',
'any',
'apply',
'basestring',
'bin',
'bool',
'buffer',
'bytearray',
'callable',
'chr',
'classmethod',
'cmp',
'coerce',
'compile',
'complex',
'delattr',
'dict',
'dir',
'divmod',
'enumerate',
'eval',
'execfile',
'file',
'filter',
'format',
'frozenset',
'getattr',
'globals',
'hasattr',
'hash',
'help',
'id',
'input',
'intern',
'isinstance',
'issubclass',
'iter',
'len',
'locals',
'list',
'map',
'max',
'memoryview',
'min',
'next',
'object',
'oct',
'open',
'ord',
'pow',
'print',
'property',
'reversed',
'range',
'raw_input',
'reduce',
'reload',
'repr',
'reversed',
'round',
'self',
'set',
'setattr',
'slice',
'sorted',
'staticmethod',
'str',
'sum',
'super',
'tuple',
'type',
'unichr',
'unicode',
'vars',
'xrange',
'zip',
'__dict__',
'__methods__',
'__members__',
'__class__',
'__bases__',
'__name__',
'__mro__',
'__subclasses__',
'__init__',
'__import__',
],
brackets: [
{ open: '{', close: '}', token: 'delimiter.curly' },
{ open: '[', close: ']', token: 'delimiter.bracket' },
{ open: '(', close: ')', token: 'delimiter.parenthesis' },
],
tokenizer: {
root: [
{ include: '@whitespace' },
{ include: '@numbers' },
{ include: '@strings' },
{ include: '@operators' },
[/[,:;]/, 'delimiter'],
[/[{}[\]()]/, '@brackets'],
[/@[a-zA-Z_]\w*/, 'tag'],
[
/[a-zA-Z_]\w*/,
{
cases: {
'@keywords': 'keyword',
'@default': 'identifier',
},
},
],
],
// Deal with white space, including single and multi-line comments
whitespace: [
[/\s+/, 'white'],
[/(^#.*$)/, 'comment'],
[/'''/, 'string', '@endDocString'],
[/"""/, 'string', '@endDblDocString'],
],
endDocString: [
[/[^']+/, 'string'],
[/\\'/, 'string'],
[/'''/, 'string', '@popall'],
[/'/, 'string'],
],
endDblDocString: [
[/[^"]+/, 'string'],
[/\\"/, 'string'],
[/"""/, 'string', '@popall'],
[/"/, 'string'],
],
// Recognize hex, negatives, decimals, imaginaries, longs, and scientific notation
numbers: [
[/-?0x([abcdef]|[ABCDEF]|\d)+[lL]?/, 'constant.numeric.hex'],
[/-?(\d*\.)?\d+([eE][+-]?\d+)?[jJ]?[lL]?/, 'constant.numeric'],
],
// Recognize strings, including those broken across lines with \ (but not without)
strings: [
[/'$/, 'string.escape', '@popall'],
[/'/, 'string.escape', '@stringBody'],
[/"$/, 'string.escape', '@popall'],
[/"/, 'string.escape', '@dblStringBody'],
],
stringBody: [
[/[^\\']+$/, 'string', '@popall'],
[/[^\\']+/, 'string'],
[/\\./, 'string'],
[/'/, 'string.escape', '@popall'],
[/\\$/, 'string'],
],
dblStringBody: [
[/[^\\"]+$/, 'string', '@popall'],
[/[^\\"]+/, 'string'],
[/\\./, 'string'],
[/"/, 'string.escape', '@popall'],
[/\\$/, 'string'],
],
operators: [[/[=+\-*/%@&|<>!~^]/, 'keyword.operator']],
attributes: [
[/\b/, '@pop'],
[/[a-zA-Z_]\w*/, 'attribute'],
],
},
};
export const completions = <monaco.languages.CompletionItemProvider>{
provideCompletionItems: (_model, position, _context, _token) => {
return {
suggestions: [
{
label: 'technichub',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: `from pybricks.hubs import TechnicHub
from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Stop, Color
from pybricks.tools import wait
hub = TechnicHub()`,
range: monaco.Range.fromPositions(position),
},
{
label: 'cityhub',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: `from pybricks.hubs import CityHub
from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Stop, Color
from pybricks.tools import wait
hub = CityHub()`,
range: monaco.Range.fromPositions(position),
},
{
label: 'movehub',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: `from pybricks.hubs import MoveHub
from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Stop, Color
from pybricks.tools import wait
hub = MoveHub()`,
range: monaco.Range.fromPositions(position),
},
{
label: 'primehub',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: `from pybricks.hubs import PrimeHub
from pybricks.pupdevices import Motor, ColorSensor, ForceSensor, UltrasonicSensor
from pybricks.parameters import Port, Stop, Color, Button
from pybricks.tools import wait
hub = PrimeHub()`,
range: monaco.Range.fromPositions(position),
},
{
label: 'inventorhub',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: `from pybricks.hubs import InventorHub
from pybricks.pupdevices import Motor, ColorSensor, UltrasonicSensor
from pybricks.parameters import Port, Stop, Color, Button
from pybricks.tools import wait
hub = InventorHub()`,
range: monaco.Range.fromPositions(position),
},
],
};
},
};
// old snippets from ace editor
// eslint-disable-next-line
const _unused = `
snippet imp
import \${1:module}
snippet from
from \${1:package} import \${2:module}
# Module Docstring
snippet docs
'''
File: \${1:FILENAME:file_name}
Author: \${2:author}
Date: \${3:date}
Description: \${4}
'''
snippet wh
while \${1:condition}:
\${2:# TODO: write code...}
# dowh - does the same as do...while in other languages
snippet dowh
while True:
\${1:# TODO: write code...}
if \${2:condition}:
break
snippet with
with \${1:expr} as \${2:var}:
\${3:# TODO: write code...}
# New Class
snippet cl
class \${1:ClassName}(\${2:object}):
"""\${3:docstring for $1}"""
def __init__(self, \${4:arg}):
\${5:super($1, self).__init__()}
self.$4 = $4
\${6}
# New Function
snippet def
def \${1:fname}(\${2:\`indent('.') ? 'self' : ''\`}):
"""\${3:docstring for $1}"""
\${4:# TODO: write code...}
snippet deff
def \${1:fname}(\${2:\`indent('.') ? 'self' : ''\`}):
\${3:# TODO: write code...}
# New Method
snippet defs
def \${1:mname}(self, \${2:arg}):
\${3:# TODO: write code...}
# New Property
snippet property
@property
def \${1:pname}():
\${2:return self._$1}
# Ifs
snippet if
if \${1:condition}:
\${2:# TODO: write code...}
snippet el
else:
\${1:# TODO: write code...}
snippet ei
elif \${1:condition}:
\${2:# TODO: write code...}
# For
snippet for
for \${1:item} in \${2:items}:
\${3:# TODO: write code...}
# Lambda
snippet ld
\${1:var} = lambda \${2:vars} : \${3:action}
snippet .
self.
snippet try Try/Except
try:
\${1:# TODO: write code...}
except \${2:Exception} as \${3:e}:
\${4:raise $3}
snippet try Try/Except/Else
try:
\${1:# TODO: write code...}
except \${2:Exception} as \${3:e}:
\${4:raise $3}
else:
\${5:# TODO: write code...}
snippet try Try/Except/Finally
try:
\${1:# TODO: write code...}
except \${2:Exception} as \${3:e}:
\${4:raise $3}
finally:
\${5:# TODO: write code...}
snippet try Try/Except/Else/Finally
try:
\${1:# TODO: write code...}
except \${2:Exception} as \${3:e}:
\${4:raise $3}
else:
\${5:# TODO: write code...}
finally:
\${6:# TODO: write code...}
snippet "
"""
\${1:doc}
"""
`;
+2 -2
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { Ace } from 'ace-builds';
import { monaco } from 'react-monaco-editor';
import { Action } from '../actions';
import { setEditSession } from './actions';
import reducers from './reducers';
@@ -17,7 +17,7 @@ test('initial state', () => {
});
test('current', () => {
const session = {} as Ace.EditSession;
const session = {} as monaco.editor.ICodeEditor;
expect(reducers({ current: null } as State, setEditSession(session)).current).toBe(
session,
);
+6 -3
View File
@@ -1,12 +1,15 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2021 The Pybricks Authors
import { Ace } from 'ace-builds';
import { monaco } from 'react-monaco-editor';
import { Reducer, combineReducers } from 'redux';
import { Action } from '../actions';
import { EditorActionType } from './actions';
const current: Reducer<Ace.EditSession | null, Action> = (state = null, action) => {
const current: Reducer<monaco.editor.ICodeEditor | null, Action> = (
state = null,
action,
) => {
switch (action.type) {
case EditorActionType.Current:
return action.editSession || null;
+6 -6
View File
@@ -1,17 +1,17 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2021 The Pybricks Authors
import { Ace } from 'ace-builds';
import { mock } from 'jest-mock-extended';
import { monaco } from 'react-monaco-editor';
import { AsyncSaga } from '../../test';
import { open, reloadProgram, saveAs } from './actions';
import editor from './sagas';
jest.mock('ace-builds');
jest.mock('react-monaco-editor');
jest.mock('file-saver');
test('open', async () => {
const mockEditor = mock<Ace.EditSession>();
const mockEditor = mock<monaco.editor.ICodeEditor>();
const saga = new AsyncSaga(editor, { editor: { current: mockEditor } });
const data = new Uint8Array().buffer;
@@ -23,7 +23,7 @@ test('open', async () => {
});
test('saveAs', async () => {
const mockEditor = mock<Ace.EditSession>();
const mockEditor = mock<monaco.editor.ICodeEditor>();
const saga = new AsyncSaga(editor, { editor: { current: mockEditor } });
saga.put(saveAs());
@@ -34,7 +34,7 @@ test('saveAs', async () => {
});
test('reloadProgram', async () => {
const mockEditor = mock<Ace.EditSession>();
const mockEditor = mock<monaco.editor.ICodeEditor>();
const saga = new AsyncSaga(editor, { editor: { current: mockEditor } });
saga.put(reloadProgram());
-21
View File
@@ -1,21 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import ace from 'ace-builds';
import './snippets';
type Snippets = {
snippetText: string;
scope: string;
};
it('indents with tabs', () => {
const { snippetText, scope } = ace.require('ace/snippets/python') as Snippets;
expect(scope).toBe('python');
for (const line of snippetText.split('\n')) {
expect(line).not.toMatch(/^(\t* )+\t*/);
}
});
-170
View File
@@ -1,170 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ace from 'ace-builds';
// fixups for Ace editor bindings
declare module 'ace-builds' {
export function define(
module: string,
deps: string[],
payload?: (require: any, exports: any, module: any) => void,
): void;
export function require(name: string[], callback: (module: any) => void): any;
}
ace.define(
'ace/snippets/python',
['require', 'exports', 'module'],
function (_require, exports, _module) {
// IMPORTANT!!!!!
// Snippets must be indented with tab character, not spaces!
exports.snippetText = `snippet technichub
from pybricks.hubs import TechnicHub
from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Stop, Color
from pybricks.tools import wait
hub = TechnicHub()
snippet cityhub
from pybricks.hubs import CityHub
from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Stop, Color
from pybricks.tools import wait
hub = CityHub()
snippet movehub
from pybricks.hubs import MoveHub
from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Stop, Color
from pybricks.tools import wait
hub = MoveHub()
snippet primehub
from pybricks.hubs import PrimeHub
from pybricks.pupdevices import Motor, ColorSensor, ForceSensor, UltrasonicSensor
from pybricks.parameters import Port, Stop, Color, Button
from pybricks.tools import wait
hub = PrimeHub()
snippet inventorhub
from pybricks.hubs import InventorHub
from pybricks.pupdevices import Motor, ColorSensor, UltrasonicSensor
from pybricks.parameters import Port, Stop, Color, Button
from pybricks.tools import wait
hub = InventorHub()
snippet uname
from uos import uname
print(uname())
snippet imp
import \${1:module}
snippet from
from \${1:package} import \${2:module}
# Module Docstring
snippet docs
'''
File: \${1:FILENAME:file_name}
Author: \${2:author}
Date: \${3:date}
Description: \${4}
'''
snippet wh
while \${1:condition}:
\${2:# TODO: write code...}
# dowh - does the same as do...while in other languages
snippet dowh
while True:
\${1:# TODO: write code...}
if \${2:condition}:
break
snippet with
with \${1:expr} as \${2:var}:
\${3:# TODO: write code...}
# New Class
snippet cl
class \${1:ClassName}(\${2:object}):
"""\${3:docstring for $1}"""
def __init__(self, \${4:arg}):
\${5:super($1, self).__init__()}
self.$4 = $4
\${6}
# New Function
snippet def
def \${1:fname}(\${2:\`indent('.') ? 'self' : ''\`}):
"""\${3:docstring for $1}"""
\${4:# TODO: write code...}
snippet deff
def \${1:fname}(\${2:\`indent('.') ? 'self' : ''\`}):
\${3:# TODO: write code...}
# New Method
snippet defs
def \${1:mname}(self, \${2:arg}):
\${3:# TODO: write code...}
# New Property
snippet property
@property
def \${1:pname}():
\${2:return self._$1}
# Ifs
snippet if
if \${1:condition}:
\${2:# TODO: write code...}
snippet el
else:
\${1:# TODO: write code...}
snippet ei
elif \${1:condition}:
\${2:# TODO: write code...}
# For
snippet for
for \${1:item} in \${2:items}:
\${3:# TODO: write code...}
# Lambda
snippet ld
\${1:var} = lambda \${2:vars} : \${3:action}
snippet .
self.
snippet try Try/Except
try:
\${1:# TODO: write code...}
except \${2:Exception} as \${3:e}:
\${4:raise $3}
snippet try Try/Except/Else
try:
\${1:# TODO: write code...}
except \${2:Exception} as \${3:e}:
\${4:raise $3}
else:
\${5:# TODO: write code...}
snippet try Try/Except/Finally
try:
\${1:# TODO: write code...}
except \${2:Exception} as \${3:e}:
\${4:raise $3}
finally:
\${5:# TODO: write code...}
snippet try Try/Except/Else/Finally
try:
\${1:# TODO: write code...}
except \${2:Exception} as \${3:e}:
\${4:raise $3}
else:
\${5:# TODO: write code...}
finally:
\${6:# TODO: write code...}
snippet "
"""
\${1:doc}
"""
`;
exports.scope = 'python';
},
);
(function (): void {
ace.require(['ace/snippets/python'], function (m: any) {
if (typeof module == 'object' && typeof exports == 'object' && module) {
module.exports = m;
}
});
})();
+90
View File
@@ -0,0 +1,90 @@
// Copied from https://github.com/microsoft/vscode/blob/167b197e767beb791e3d191c24efb97865a98b42/src/vs/workbench/browser/parts/editor/untitledHint.ts
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { monaco } from 'react-monaco-editor';
export class UntitledHintContribution implements monaco.editor.IEditorContribution {
public static readonly ID = 'editor.contrib.untitledHint';
private toDispose: monaco.IDisposable[];
private untitledHintContentWidget: UntitledHintContentWidget | undefined;
constructor(
private readonly editor: monaco.editor.ICodeEditor,
private readonly placeholder: string,
) {
this.toDispose = [];
this.toDispose.push(editor.onDidChangeModel(() => this.update()));
this.update();
}
private update(): void {
this.untitledHintContentWidget?.dispose();
this.untitledHintContentWidget = new UntitledHintContentWidget(
this.editor,
this.placeholder,
);
}
dispose(): void {
this.toDispose.forEach((d) => d.dispose());
this.untitledHintContentWidget?.dispose();
}
}
class UntitledHintContentWidget implements monaco.editor.IContentWidget {
private static readonly ID = 'editor.widget.untitledHint';
private domNode: HTMLElement | undefined;
private toDispose: monaco.IDisposable[];
constructor(
private readonly editor: monaco.editor.ICodeEditor,
private readonly placeholder: string,
) {
this.toDispose = [];
this.toDispose.push(
editor.onDidChangeModelContent(() => this.onDidChangeModelContent()),
);
this.onDidChangeModelContent();
}
private onDidChangeModelContent(): void {
if (this.editor.getValue() === '') {
this.editor.addContentWidget(this);
} else {
this.editor.removeContentWidget(this);
}
}
getId(): string {
return UntitledHintContentWidget.ID;
}
// Select a language to get started. Start typing to dismiss, or don't show this again.
getDomNode(): HTMLElement {
if (!this.domNode) {
this.domNode = document.createElement('div');
this.domNode.textContent = this.placeholder;
this.domNode.className = 'pb-editor-placeholder';
this.editor.applyFontInfo(this.domNode);
}
return this.domNode;
}
getPosition(): monaco.editor.IContentWidgetPosition | null {
return {
position: { lineNumber: 1, column: 1 },
preference: [monaco.editor.ContentWidgetPositionPreference.EXACT],
};
}
dispose(): void {
this.editor.removeContentWidget(this);
this.toDispose.forEach((d) => d.dispose());
}
}
+3 -3
View File
@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2021 The Pybricks Authors
import { Ace } from 'ace-builds';
import { mock } from 'jest-mock-extended';
import { monaco } from 'react-monaco-editor';
import { AsyncSaga } from '../../test';
import {
BlePybricksServiceCommandActionType,
@@ -22,11 +22,11 @@ import {
} from './actions';
import hub from './sagas';
jest.mock('ace-builds');
jest.mock('react-monaco-editor');
describe('downloadAndRun', () => {
test('no errors', async () => {
const mockEditor = mock<Ace.EditSession>();
const mockEditor = mock<monaco.editor.ICodeEditor>();
const saga = new AsyncSaga(
hub,
{ editor: { current: mockEditor } },
+5
View File
@@ -14,3 +14,8 @@ declare module '*.zip' {
const src: string;
export default src;
}
// https://webpack.js.org/api/hot-module-replacement/
interface NodeModule {
hot?: { dispose: (callback: (data) => void) => void };
}