diff --git a/package-lock.json b/package-lock.json index 8f13508..9dcf849 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@reduxjs/toolkit": "^2.2.6", "ace-builds": "^1.35.1", "electron-debug": "^3.2.0", "electron-log": "^4.4.8", @@ -18,7 +17,6 @@ "react": "^18.2.0", "react-ace": "^12.0.0", "react-dom": "^18.2.0", - "react-redux": "^9.1.2", "react-router-dom": "^6.16.0" }, "devDependencies": { @@ -2033,29 +2031,6 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "dev": true }, - "node_modules/@reduxjs/toolkit": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.6.tgz", - "integrity": "sha512-kH0r495c5z1t0g796eDQAkYbEQ3a1OLYN9o8jQQVZyKyw367pfRGS+qZLkHYvFHiUUdafpoSlQ2QYObIApjPWA==", - "dependencies": { - "immer": "^10.0.3", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, "node_modules/@remix-run/router": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.9.0.tgz", @@ -2653,7 +2628,7 @@ "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "devOptional": true + "dev": true }, "node_modules/@types/qs": { "version": "6.9.7", @@ -2671,7 +2646,7 @@ "version": "18.3.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "devOptional": true, + "dev": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2772,11 +2747,6 @@ "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==", "dev": true }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", - "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" - }, "node_modules/@types/verror": { "version": "1.10.6", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz", @@ -5799,7 +5769,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", - "devOptional": true + "dev": true }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -9282,15 +9252,6 @@ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "dev": true }, - "node_modules/immer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", - "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/immutable": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz", @@ -13699,28 +13660,6 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, - "node_modules/react-redux": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", - "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", - "dependencies": { - "@types/use-sync-external-store": "^0.0.3", - "use-sync-external-store": "^1.0.0" - }, - "peerDependencies": { - "@types/react": "^18.2.25", - "react": "^18.0", - "redux": "^5.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -13861,19 +13800,6 @@ "node": ">=8" } }, - "node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "peerDependencies": { - "redux": "^5.0.0" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -13969,11 +13895,6 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, - "node_modules/reselect": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" - }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -16266,14 +16187,6 @@ "requires-port": "^1.0.0" } }, - "node_modules/use-sync-external-store": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", - "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", diff --git a/package.json b/package.json index 916f8b9..d30d63f 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,6 @@ } }, "dependencies": { - "@reduxjs/toolkit": "^2.2.6", "ace-builds": "^1.35.1", "electron-debug": "^3.2.0", "electron-log": "^4.4.8", @@ -109,7 +108,6 @@ "react": "^18.2.0", "react-ace": "^12.0.0", "react-dom": "^18.2.0", - "react-redux": "^9.1.2", "react-router-dom": "^6.16.0" }, "devDependencies": { diff --git a/src/__tests__/App.test.tsx b/src/__tests__/App.test.tsx index 6a1de2a..09feb6d 100644 --- a/src/__tests__/App.test.tsx +++ b/src/__tests__/App.test.tsx @@ -1,8 +1,12 @@ import '@testing-library/jest-dom'; import { render } from '@testing-library/react'; +import { config as aceConfig } from 'ace-builds'; import App from '../renderer/App'; describe('App', () => { + beforeEach(() => { + aceConfig.set('basePath', 'node_modules/ace-builds/src-noconflict'); + }); it('should render', () => { expect(render()).toBeTruthy(); }); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 8d56b6f..4998314 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -8,7 +8,10 @@ import { useLayoutEffect, } from 'react'; import Topbar from './Topbar'; -import Editor, { EditorContentStatus, KeyboardControlsStatus } from './Editor'; +import Editor, { + EditorContentStatus, + KeyboardControlsStatus, +} from './editor/Editor'; import DeviceInfo from './DeviceInfo'; import AppConsole from './AppConsole'; import type AppConsoleMessage from '../common/AppConsoleMessage'; // No crypto package on the renderer diff --git a/src/renderer/editor/ApiLink.css b/src/renderer/editor/ApiLink.css new file mode 100644 index 0000000..8e63aaa --- /dev/null +++ b/src/renderer/editor/ApiLink.css @@ -0,0 +1,8 @@ +.ApiLink { + color: blue; + text-decoration: underline; + background-color: transparent; + border: none; + padding: 0; + cursor: pointer; +} diff --git a/src/renderer/editor/ApiLink.tsx b/src/renderer/editor/ApiLink.tsx new file mode 100644 index 0000000..371e9f3 --- /dev/null +++ b/src/renderer/editor/ApiLink.tsx @@ -0,0 +1,36 @@ +import './ApiLink.css'; + +/** + * Links to a section of the student API documentation, opening the help window or just jumping to + * the appropriate section if the window is already open. The single text node child of this + * component is used as the link text. + * @param props + * @param props.dest - the section of the docs to jump to. TODO: figure out format + * @param props.code - whether to display the link text in a monospaced font. + */ +export default function ApiLink({ + dest, + code = false, + children, +}: { + dest: string; + code?: boolean; + children: string; +}) { + const text = `(${children})[${dest}]`; + // Placeholder + return ( + + ); +} +/** + * Default props for ApiLink. + */ +ApiLink.defaultProps = { + /** + * By default, link text is displayed in the inherited font. + */ + code: false, +}; diff --git a/src/renderer/Editor.css b/src/renderer/editor/Editor.css similarity index 100% rename from src/renderer/Editor.css rename to src/renderer/editor/Editor.css diff --git a/src/renderer/Editor.tsx b/src/renderer/editor/Editor.tsx similarity index 86% rename from src/renderer/Editor.tsx rename to src/renderer/editor/Editor.tsx index 427c786..977ac24 100644 --- a/src/renderer/Editor.tsx +++ b/src/renderer/editor/Editor.tsx @@ -1,22 +1,12 @@ -import { useState } from 'react'; +import { useState, useEffect, useRef } from 'react'; import AceEditor from 'react-ace'; -import 'ace-builds/src-noconflict/mode-python'; -import uploadSvg from '../../assets/upload.svg'; -import downloadSvg from '../../assets/download.svg'; -import openSvg from '../../assets/open.svg'; -import saveSvg from '../../assets/save.svg'; -import saveAsSvg from '../../assets/save-as.svg'; -import newFileSvg from '../../assets/new-file.svg'; -import pieSvg from '../../assets/pie.svg'; -import consoleSvg from '../../assets/console.svg'; -import consoleClearSvg from '../../assets/console-clear.svg'; -import zoomInSvg from '../../assets/zoom-in.svg'; -import zoomOutSvg from '../../assets/zoom-out.svg'; -import startRobot from '../../assets/start-robot.svg'; -import stopRobot from '../../assets/stop-robot.svg'; -import keyboardKeySvg from '../../assets/keyboard-key.svg'; -import themeSvg from '../../assets/theme.svg'; +import addEditorAutocomplete from './addEditorAutocomplete'; +import addEditorTooltips from './addEditorTooltips'; +import 'ace-builds/src-noconflict/mode-python'; +import 'ace-builds/src-noconflict/snippets/python'; +import 'ace-builds/src-noconflict/ext-language_tools'; +import 'ace-builds/src-noconflict/ext-searchbox'; import 'ace-builds/src-noconflict/theme-chrome'; import 'ace-builds/src-noconflict/theme-clouds'; import 'ace-builds/src-noconflict/theme-dawn'; @@ -26,6 +16,22 @@ import 'ace-builds/src-noconflict/theme-tomorrow_night'; import 'ace-builds/src-noconflict/theme-clouds_midnight'; import 'ace-builds/src-noconflict/theme-ambiance'; +import uploadSvg from '../../../assets/upload.svg'; +import downloadSvg from '../../../assets/download.svg'; +import openSvg from '../../../assets/open.svg'; +import saveSvg from '../../../assets/save.svg'; +import saveAsSvg from '../../../assets/save-as.svg'; +import newFileSvg from '../../../assets/new-file.svg'; +import pieSvg from '../../../assets/pie.svg'; +import consoleSvg from '../../../assets/console.svg'; +import consoleClearSvg from '../../../assets/console-clear.svg'; +import zoomInSvg from '../../../assets/zoom-in.svg'; +import zoomOutSvg from '../../../assets/zoom-out.svg'; +import startRobot from '../../../assets/start-robot.svg'; +import stopRobot from '../../../assets/stop-robot.svg'; +import keyboardKeySvg from '../../../assets/keyboard-key.svg'; +import themeSvg from '../../../assets/theme.svg'; + import './Editor.css'; /** @@ -162,10 +168,18 @@ export default function Editor({ }) { const [opmode, setOpmode] = useState('auto'); const [fontSize, setFontSize] = useState(12); + const editorRef = useRef(null as AceEditor | null); const zoomEditor = (increase: boolean) => { setFontSize((old) => old + (increase ? 1 : -1)); }; + useEffect(() => { + if (editorRef.current !== null) { + const { editor } = editorRef.current; + addEditorAutocomplete(editor); + addEditorTooltips(editor); + } + }, [editorRef]); const [theme, setTheme] = useState('dawn'); // Default theme const handleThemeChange = (newTheme: string) => { @@ -278,7 +292,9 @@ export default function Editor({ name="Editor-toolbar-opmode" > {Object.entries(ACE_THEMES).map(([themeKey, themeName]) => ( - + ))} @@ -325,7 +341,7 @@ export default function Editor({
- Keyboard input sent to robot — disable to edit code + Keyboard input sent to robot -- disable to edit code
diff --git a/src/renderer/editor/HighlightedCode.css b/src/renderer/editor/HighlightedCode.css new file mode 100644 index 0000000..f6b4242 --- /dev/null +++ b/src/renderer/editor/HighlightedCode.css @@ -0,0 +1,12 @@ +.HighlightedCode-editor .ace_scroller { + width: 100%; +} +.HighlightedCode-editor .ace_cursor { + opacity: 0; +} +.HighlightedCode-editor .ace_marker-layer { + display: none; +} +.HighlightedCode-editor .ace_print-margin { + display: none; +} diff --git a/src/renderer/editor/HighlightedCode.tsx b/src/renderer/editor/HighlightedCode.tsx new file mode 100644 index 0000000..afff260 --- /dev/null +++ b/src/renderer/editor/HighlightedCode.tsx @@ -0,0 +1,57 @@ +import AceEditor from 'react-ace'; +import 'ace-builds/src-noconflict/mode-python'; +import './HighlightedCode.css'; + +/** + * Uses a read-only AceEditor to display a code block with Python syntax highlighting. The only + * valid children of this component is text containing the code to be displayed. Common indentation + * is removed from each line of code before display. Only spaces are considered indentation; tabs, + * half-width spaces, and nonbreaking spaces are treated as content. + * @param props + * @param props.indent - number of spaces of indentation to prepend to each line, after common + * indentation is removed. + */ +export default function HighlightedCode({ + children, + indent = 0, +}: { + children: string; + indent?: number; +}) { + const lines = children.split('\n'); + if (lines.length && !lines[0].trim()) { + lines.shift(); + } + if (lines.length && !lines[lines.length - 1].trim()) { + lines.pop(); + } + // Remove common indent + const minIndent = Math.min( + ...lines + .filter((line) => line.trim().length) + .map((line) => line.match(/^ */)![0].length), + ); + const formatted = lines + .map((line) => ' '.repeat(indent) + line.slice(minIndent)) + .join('\n'); + return ( + + ); +} +/** + * Default props for HighlightedCode. + */ +HighlightedCode.defaultProps = { + /** + * No indentation is added to code lines after stripping common indentation by default. + */ + indent: 0, +}; diff --git a/src/renderer/editor/addEditorAutocomplete.ts b/src/renderer/editor/addEditorAutocomplete.ts new file mode 100644 index 0000000..560a01a --- /dev/null +++ b/src/renderer/editor/addEditorAutocomplete.ts @@ -0,0 +1,186 @@ +import { Ace, require as acequire } from 'ace-builds'; +import robotKeyNumberMap from '../robotKeyNumberMap'; +import readApiCall from './readApiCall'; + +const { TokenIterator } = acequire('ace/token_iterator'); + +/** + * The base score to give to our added completions. Set at this value to override Ace's default + * keyword and 'local' variable completions. + */ +const COMP_SCORE = 200; + +/** + * Completer for PiE globals. + */ +const globalCompleter = { + getCompletions: ( + _editor: Ace.Editor, + _session: Ace.EditSession, + _pos: Ace.Point, + _prefix: string, + callback: Ace.CompleterCallback, + ) => { + callback( + null, + ['Robot', 'Keyboard', 'Gamepad'].map((value) => ({ + value, + meta: 'PiE API', + score: COMP_SCORE - 10, + })), + ); + }, +}; + +/** + * Creates a completer that only shows its completions when the tokens before the caret + * match one of the given strings. + * @param ctx - the string to match the tokens before the caret against. Be advised that this is + * only designed to support function calls; parenthese used outside function calls, braces, spaces + * between identifiers, and any kind of punctuation other than periods will never be matched. + * @param completions - the array of completions to show if the tokens around the caret are + * matched with one of the lastToken strings. + * @return The created completer. + */ +function makeContextCompleter(ctx: string, completions: string[]) { + return { + getCompletions: ( + _editor: Ace.Editor, + session: Ace.EditSession, + pos: Ace.Point, + _prefix: string, + callback: Ace.CompleterCallback, + ) => { + const maxLength = + ctx.length + + Math.max(...completions.map((completion) => completion.length)); + const buf = readApiCall(session, pos, maxLength); + const isContext = buf.startsWith(ctx); + callback( + null, + isContext + ? completions + .filter( + (completion) => + (ctx + completion).startsWith(buf.trim()) && + ctx + completion !== buf.trim(), + ) + .map((caption) => { + return { + caption, + // FIXME + // Completion is multiple tokens (e.g. Gamepad.get_value completions): slice is needed + // to remove already-typed string from completion. + // Context is single token: unsliced is needed or else existing text is replaced with + // sliced completion, effectively deleting the already-typed bit. + // value: caption.slice(buf.length - ctx.length), + value: caption, + meta: 'PiE API', + score: COMP_SCORE, + }; + }) + : [], + ); + }, + onInsert: (_editor: Ace.Editor, _completion: Ace.Completion) => { + // Adding something over here could maybe fix sliced completion? + }, + }; +} + +/** + * Wraps a completer so it does not trigger if the current or preceeding token contains a dot. + * @param completer - the completer to wrap. + * @return The wrapped completer. + */ +function adaptGlobalCompleter(completer: Ace.Completer) { + return { + getCompletions: ( + editor: Ace.Editor, + session: Ace.EditSession, + pos: Ace.Point, + prefix: string, + callback: Ace.CompleterCallback, + ) => { + const iter = new TokenIterator(session, pos.row, pos.column); + const curTokenDot = + iter.getCurrentToken() !== undefined && + iter.getCurrentToken().value && + iter.getCurrentToken().value.includes('.'); + const prevTokenDot = + iter.stepBackward() !== null && + iter.getCurrentToken().value && + iter.getCurrentToken().value.includes('.'); + if (!curTokenDot && !prevTokenDot) { + completer.getCompletions(editor, session, pos, prefix, callback); + } + }, + }; +} + +/** + * Adds PiE API completers to the given editor. + * @param editor - the editor to modify + */ +export default function addEditorAutocomplete(editor: Ace.Editor) { + editor.commands.addCommand({ + name: 'dotAutoComplete', + bindKey: { + win: '.', + mac: '.', + }, + exec: () => { + editor.insert('.'); + editor.execCommand('startAutocomplete'); + }, + }); + editor.commands.addCommand({ + name: 'parenAutoComplete', + bindKey: { + win: '(', + mac: '(', + }, + exec: () => { + editor.insert('('); + editor.execCommand('startAutocomplete'); + }, + }); + editor.completers = [ + adaptGlobalCompleter(globalCompleter), + makeContextCompleter('Robot.', [ + 'get_value', + 'set_value', + 'start_pos', + 'sleep', + 'log', + 'is_running', + 'run', + ]), + makeContextCompleter('Gamepad.', ['available', 'get_value']), + makeContextCompleter('Gamepad.get_value(', [ + '"button_a"', + '"button_b"', + '"button_x"', + '"button_y"', + '"l_bumper"', + '"r_bumper"', + '"l_trigger"', + '"r_trigger"', + '"button_back"', + '"button_start"', + '"l_stick"', + '"r_stick"', + '"dpad_up"', + '"dpad_down"', + '"dpad_left"', + '"dpad_right"', + '"button_xbox"', + ]), + makeContextCompleter('Keyboard.', ['available', 'get_value']), + makeContextCompleter( + 'Keyboard.get_value(', + Object.keys(robotKeyNumberMap).map((c) => `"${c}"`), + ), + ...(editor.completers || []).map(adaptGlobalCompleter), + ]; +} diff --git a/src/renderer/editor/addEditorTooltips.tsx b/src/renderer/editor/addEditorTooltips.tsx new file mode 100644 index 0000000..e9f02ba --- /dev/null +++ b/src/renderer/editor/addEditorTooltips.tsx @@ -0,0 +1,78 @@ +import { Ace, require as acequire } from 'ace-builds'; +import { ReactNode } from 'react'; +import { createRoot } from 'react-dom/client'; +import ApiLink from './ApiLink'; +import HighlightedCode from './HighlightedCode'; +import readApiCall from './readApiCall'; + +const { HoverTooltip } = acequire('ace/tooltip'); + +const apiHelpComponents: { + [matchText: string]: () => ReactNode; +} = { + 'Robot.get_value': () => ( +
+ The get_value function returns the current value of a + specified param on a device with the specified{' '} + device_id. +
+ Parameters: +
    +
  • + device_id: the ID that + specifies which PiE device will be read +
  • +
  • + param: identifies which + parameter on the specified PiE device will be read. Possible param + values depend on the specified device. Find a list of params for each + type of device on the{' '} + lowcar devices page. +
  • +
+ The function is useful for checking the current state of devices. For + example, getting the current state of a limit switch using its{' '} + device_id and the param "switch0" will + return True when pressed down and False if not. + {` + # First segment of code ran in the teleop process + limit_switch = "//INSERT SWITCH ID HERE//" + + def teleop_setup(): + print("Tele-operated mode has started!") + + def teleop_main(): + # Example code for getting the value of a limit switch + # First parameter is the limit switch's id + # Second parameter tells which switch to get the value from + # In this case the method will return True or False depending on if the switch is pressed down or not + + Robot.get_value(limit_switch, switch0) + `} +
+ ), + Robot: () =>
Documentation for Robot object.
, +}; + +/** + * Configures hover tooltips for PiE API stuff in the given editor. + * @param editor - the editor to modify + */ +export default function addEditorTooltips(editor: Ace.Editor) { + const tooltip = new HoverTooltip(); + const node = document.createElement('div'); + const root = createRoot(node); + // Check just past longest match in case the very next character + const maxMatchTextLength = + Math.max(...Object.keys(apiHelpComponents).map((s) => s.length)) + 1; + tooltip.setDataProvider((event: any, _editor: Ace.Editor) => { + const pos: Ace.Position = event.getDocumentPosition(); + const range = editor.session.getWordRange(pos.row, pos.column); + const result = readApiCall(editor.session, range.end, maxMatchTextLength); + if (result in apiHelpComponents) { + root.render(apiHelpComponents[result]()); + tooltip.showForRange(editor, range, node, event); + } + }); + tooltip.addToEditor(editor); +} diff --git a/src/renderer/editor/readApiCall.ts b/src/renderer/editor/readApiCall.ts new file mode 100644 index 0000000..4f01611 --- /dev/null +++ b/src/renderer/editor/readApiCall.ts @@ -0,0 +1,71 @@ +import { Ace, require as acequire } from 'ace-builds'; + +const { TokenIterator } = acequire('ace/token_iterator'); + +/** + * Reads a string that looks like an API call just before the given position in the editor. + * An "API call" may include parentheses used to call a function, periods, identifier characters, + * and whitespace before and after non-identifiers. Tokens the Ace tokenizer considers to be part of + * a string in the editor's current mode also delimit an API call. Comments and line breaks around + * non-identifiers are ignored, and string collections continues on the other side of the comment. + * Probably mode-independent, though it hasn't been tested on anything besides Python. + * @param session - the Ace editing session to use to read the editor contents. + * @param pos - the position to start reading behind. + * @param minLength - the minimum length in characters of text to read. A whole number of tokens + * will always be read, but reading is stopped after reading at least this many characters. + * @return The text of the read "API call", which may be an empty string if the first token before + * pos is not part of an API call. + */ +export default ( + session: Ace.EditSession, + pos: Ace.Position, + minLength: number, +): string => { + const iter = new TokenIterator(session, pos.row, pos.column); + let token = iter.getCurrentToken(); + const tokenIsIdent = () => + ['identifier', 'function.support'].includes(token.type); + const firstToken = token; + while (token === undefined || token.value.trim() === '') { + token = iter.stepBackward(); + if (token === null) { + return ''; + } + } + if (token.type === 'comment' || token.type === 'string') { + return ''; + } + let lastWasIdentifier = tokenIsIdent(); + let buf = token.value.trim(); + let posInBuf; + if (token === firstToken) { + posInBuf = pos.column - iter.getCurrentTokenColumn(); + } else { + posInBuf = buf.length; + } + buf = buf.slice(0, posInBuf); + while (buf.length < minLength) { + token = iter.stepBackward(); + if (token === null) { + break; + } + const tokenIsWhitespace = token.value.trim() === ''; + // The following conditions cause a token break if coming before an identifier: + const preIdentBreak = + tokenIsIdent() || + token.type.startsWith('paren') || + token.type === 'string' || + (token.type === 'punctuation' && !token.value.includes('.')); + if (lastWasIdentifier && preIdentBreak) { + break; + } + if (token.type !== 'comment') { + buf = token.value.trim() + buf; + posInBuf += token.value.trim().length; + if (!tokenIsWhitespace) { + lastWasIdentifier = tokenIsIdent(); + } + } + } + return buf; +}; diff --git a/src/renderer/modals/ConfirmModal.tsx b/src/renderer/modals/ConfirmModal.tsx index 50de41d..1c2ce99 100644 --- a/src/renderer/modals/ConfirmModal.tsx +++ b/src/renderer/modals/ConfirmModal.tsx @@ -46,8 +46,7 @@ export default function ConfirmModal({ ); } /** - * Default properties for ConfirmModal. Not sure why we need this if we have the default - * deconstruction parameter but the linter cries if we leave it out. + * Default properties for ConfirmModal. */ ConfirmModal.defaultProps = { /** diff --git a/src/renderer/modals/Modal.tsx b/src/renderer/modals/Modal.tsx index df52b46..77379a2 100644 --- a/src/renderer/modals/Modal.tsx +++ b/src/renderer/modals/Modal.tsx @@ -19,8 +19,8 @@ export default function Modal({ onClose: () => void; isActive: boolean; modalTitle: string; - children: ReactNode; className?: string; + children: ReactNode; }) { return (