mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
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:
committed by
David Lechner
parent
c3851e7139
commit
d95c550248
@@ -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
@@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
"""
|
||||
`;
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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*/);
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 } },
|
||||
|
||||
Vendored
+5
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user