mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-08 01:48:32 +00:00
Moving repo to bmci-labs
This commit is contained in:
1
arduino-ide-extension/.gitignore
vendored
Normal file
1
arduino-ide-extension/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
src/node/cli-protocol
|
||||
1955
arduino-ide-extension/data/ino.tmLanguage.json
Normal file
1955
arduino-ide-extension/data/ino.tmLanguage.json
Normal file
File diff suppressed because it is too large
Load Diff
42
arduino-ide-extension/package.json
Normal file
42
arduino-ide-extension/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "arduino-ide-extension",
|
||||
"version": "0.0.1",
|
||||
"description": "An extension for Theia building the Arduino IDE",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@theia/core": "next",
|
||||
"@theia/editor": "next",
|
||||
"@theia/filesystem": "next",
|
||||
"@theia/languages": "next",
|
||||
"@theia/monaco": "next",
|
||||
"@theia/workspace": "next"
|
||||
},
|
||||
"scripts": {
|
||||
"generate-protoc": "./scripts/generate-protoc.sh",
|
||||
"prepare": "yarn run clean && yarn generate-protoc && yarn run build",
|
||||
"clean": "rimraf lib",
|
||||
"lint": "tslint -c ./tslint.json --project ./tsconfig.json",
|
||||
"build": "tsc && cp -rf src/node/cli-protocol lib/node && yarn lint",
|
||||
"watch": "tsc -w"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/google-protobuf": "^3.2.7",
|
||||
"grpc-tools": "^1.7.3",
|
||||
"grpc_tools_node_protoc_ts": "^2.5.0",
|
||||
"rimraf": "^2.6.1",
|
||||
"tslint": "^5.5.0",
|
||||
"typescript": "2.9.1"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"src",
|
||||
"build",
|
||||
"data"
|
||||
],
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"backend": "lib/node/arduino-backend-module",
|
||||
"frontend": "lib/browser/arduino-frontend-module"
|
||||
}
|
||||
]
|
||||
}
|
||||
45
arduino-ide-extension/scripts/generate-protoc.sh
Executable file
45
arduino-ide-extension/scripts/generate-protoc.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT=`realpath -s $0`
|
||||
SCRIPTPATH=`dirname $SCRIPT`
|
||||
WORKDIR=/tmp/arduino-cli-protoc
|
||||
echo "Working in $WORKDIR"
|
||||
|
||||
# this could be a Git submodule, but that feels to clunky for just building the protobuf stuff
|
||||
mkdir -p $WORKDIR
|
||||
pushd $WORKDIR
|
||||
if [ ! -d arduino-cli ]; then
|
||||
git clone https://github.com/cmaglie/arduino-cli
|
||||
cd arduino-cli
|
||||
git checkout daemon
|
||||
cd -
|
||||
|
||||
mkdir -p go/src/github.com/arduino
|
||||
ln -s $PWD/arduino-cli go/src/github.com/arduino
|
||||
export GOPATH=$PWD/go
|
||||
cd go/src/github.com/arduino/arduino-cli
|
||||
GOOS=linux go build -o arduino-cli.linux
|
||||
# GOOS=darwin go build -o arduino-cli.darwin
|
||||
fi
|
||||
popd
|
||||
|
||||
# make sure the output path exists
|
||||
mkdir -p src/node/cli-protocol
|
||||
|
||||
export PATH=$PATH:$PWD/node_modules/.bin
|
||||
# generate js codes via grpc-tools
|
||||
grpc_tools_node_protoc \
|
||||
--js_out=import_style=commonjs,binary:./src/node/cli-protocol \
|
||||
--grpc_out=./src/node/cli-protocol \
|
||||
--plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` \
|
||||
-I /usr/lib/protoc/include \
|
||||
-I $WORKDIR/arduino-cli/rpc \
|
||||
$WORKDIR/arduino-cli/rpc/*.proto
|
||||
|
||||
# generate d.ts codes
|
||||
protoc \
|
||||
--plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \
|
||||
--ts_out=./src/node/cli-protocol \
|
||||
-I /usr/lib/protoc/include \
|
||||
-I $WORKDIR/arduino-cli/rpc \
|
||||
$WORKDIR/arduino-cli/rpc/*.proto
|
||||
13
arduino-ide-extension/src/browser/arduino-commands.ts
Normal file
13
arduino-ide-extension/src/browser/arduino-commands.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Command } from '@theia/core/lib/common/command';
|
||||
|
||||
export namespace ArduinoCommands {
|
||||
|
||||
export const VERIFY: Command = {
|
||||
id: 'arduino-verify'
|
||||
}
|
||||
|
||||
export const UPLOAD: Command = {
|
||||
id: 'arduino-upload'
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import * as React from 'react';
|
||||
import { injectable, inject, postConstruct } from 'inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { DefaultFrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { BoardsService } from '../common/protocol/boards-service';
|
||||
import { ArduinoCommands } from './arduino-commands';
|
||||
import { ConnectedBoards } from './components/connected-boards';
|
||||
import { CoreService } from '../common/protocol/core-service';
|
||||
import { WorkspaceServiceExt } from './workspace-service-ext';
|
||||
|
||||
|
||||
@injectable()
|
||||
export class ArduinoFrontendContribution extends DefaultFrontendApplicationContribution implements TabBarToolbarContribution, CommandContribution {
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardService: BoardsService;
|
||||
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
|
||||
@inject(WorkspaceServiceExt)
|
||||
protected readonly workspaceServiceExt: WorkspaceServiceExt;
|
||||
|
||||
@postConstruct()
|
||||
protected async init(): Promise<void> {
|
||||
// This is a hack. Otherwise, the backend services won't bind.
|
||||
await this.workspaceServiceExt.roots();
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: ArduinoCommands.VERIFY.id,
|
||||
command: ArduinoCommands.VERIFY.id,
|
||||
tooltip: 'Verify',
|
||||
group: 'arduino',
|
||||
text: '$(check)'
|
||||
});
|
||||
registry.registerItem({
|
||||
id: ArduinoCommands.UPLOAD.id,
|
||||
command: ArduinoCommands.UPLOAD.id,
|
||||
tooltip: 'Upload',
|
||||
group: 'arduino',
|
||||
text: '$(arrow-right)'
|
||||
});
|
||||
registry.registerItem({
|
||||
id: ConnectedBoards.TOOLBAR_ID,
|
||||
render: () => <ConnectedBoards boardsService={this.boardService}/>,
|
||||
isVisible: widget => this.isArduinoEditor(widget)
|
||||
})
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(ArduinoCommands.VERIFY, {
|
||||
isVisible: widget => this.isArduinoEditor(widget),
|
||||
isEnabled: widget => this.isArduinoEditor(widget),
|
||||
execute: async widget => {
|
||||
const uri = this.toUri(widget);
|
||||
if (uri) {
|
||||
const result = await this.coreService.compile({ uri: uri.toString() });
|
||||
console.log('compile result', result);
|
||||
}
|
||||
}
|
||||
});
|
||||
registry.registerCommand(ArduinoCommands.UPLOAD, {
|
||||
isVisible: widget => this.isArduinoEditor(widget),
|
||||
isEnabled: widget => this.isArduinoEditor(widget),
|
||||
execute: widget => {
|
||||
const uri = this.toUri(widget);
|
||||
if (uri) {
|
||||
this.messageService.info(`Uploading ${uri.toString()}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private isArduinoEditor(maybeEditorWidget: any): boolean {
|
||||
if (maybeEditorWidget instanceof EditorWidget) {
|
||||
return maybeEditorWidget.editor.uri.toString().endsWith('.ino');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private toUri(arg: any): URI | undefined {
|
||||
if (arg instanceof URI) {
|
||||
return arg;
|
||||
}
|
||||
if (typeof arg === 'string') {
|
||||
return new URI(arg);
|
||||
}
|
||||
if (arg instanceof EditorWidget) {
|
||||
return arg.editor.uri;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
70
arduino-ide-extension/src/browser/arduino-frontend-module.ts
Normal file
70
arduino-ide-extension/src/browser/arduino-frontend-module.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ContainerModule, interfaces } from 'inversify';
|
||||
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
|
||||
import { CommandContribution } from '@theia/core/lib/common/command';
|
||||
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'
|
||||
import { LanguageGrammarDefinitionContribution } from '@theia/monaco/lib/browser/textmate';
|
||||
import { LibraryListWidget } from './library/library-list-widget';
|
||||
import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
|
||||
import { ArduinoLanguageGrammarContribution } from './language/arduino-language-grammar-contribution';
|
||||
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
|
||||
import { BoardsService, BoardsServicePath } from '../common/protocol/boards-service';
|
||||
import { LibraryListWidgetFrontendContribution } from './library/list-widget-frontend-contribution';
|
||||
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
|
||||
import { BoardsListWidget } from './boards/boards-list-widget';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
|
||||
import { WorkspaceServiceExt, WorkspaceServiceExtPath } from './workspace-service-ext';
|
||||
import { WorkspaceServiceExtImpl } from './workspace-service-ext-impl';
|
||||
|
||||
import '../../src/browser/style/index.css';
|
||||
|
||||
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
|
||||
// Commands and toolbar items
|
||||
bind(ArduinoFrontendContribution).toSelf().inSingletonScope();
|
||||
bind(CommandContribution).toService(ArduinoFrontendContribution);
|
||||
bind(TabBarToolbarContribution).toService(ArduinoFrontendContribution);
|
||||
|
||||
// `ino` TextMate grammar
|
||||
bind(LanguageGrammarDefinitionContribution).to(ArduinoLanguageGrammarContribution).inSingletonScope();
|
||||
|
||||
// Library service
|
||||
bind(LibraryService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, LibraryServicePath)).inSingletonScope();
|
||||
|
||||
// Library list widget
|
||||
bind(LibraryListWidget).toSelf();
|
||||
bindViewContribution(bind, LibraryListWidgetFrontendContribution);
|
||||
bind(WidgetFactory).toDynamicValue(context => ({
|
||||
id: LibraryListWidget.WIDGET_ID,
|
||||
createWidget: () => context.container.get(LibraryListWidget)
|
||||
}));
|
||||
bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution);
|
||||
|
||||
// Boards service
|
||||
bind(BoardsService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath)).inSingletonScope();
|
||||
|
||||
// Boards list widget
|
||||
bind(BoardsListWidget).toSelf();
|
||||
bindViewContribution(bind, BoardsListWidgetFrontendContribution);
|
||||
bind(WidgetFactory).toDynamicValue(context => ({
|
||||
id: BoardsListWidget.WIDGET_ID,
|
||||
createWidget: () => context.container.get(BoardsListWidget)
|
||||
}));
|
||||
bind(FrontendApplicationContribution).toService(BoardsListWidgetFrontendContribution);
|
||||
|
||||
// Core service
|
||||
bind(CoreService)
|
||||
.toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, CoreServicePath))
|
||||
.inSingletonScope();
|
||||
|
||||
// The workspace service extension
|
||||
bind(WorkspaceServiceExt).to(WorkspaceServiceExtImpl).inSingletonScope().onActivation(({ container }, workspaceServiceExt) => {
|
||||
WebSocketConnectionProvider.createProxy(container, WorkspaceServiceExtPath, workspaceServiceExt);
|
||||
// Eagerly active the core, library, and boards services.
|
||||
container.get(CoreService);
|
||||
container.get(LibraryService);
|
||||
container.get(BoardsService);
|
||||
return workspaceServiceExt;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ListWidget } from './list-widget';
|
||||
|
||||
export class BoardsListWidget extends ListWidget {
|
||||
|
||||
static WIDGET_ID = 'boards-list-widget';
|
||||
static WIDGET_LABEL = 'Boards Manager';
|
||||
|
||||
protected widgetProps(): ListWidget.Props {
|
||||
return {
|
||||
id: BoardsListWidget.WIDGET_ID,
|
||||
title: BoardsListWidget.WIDGET_LABEL,
|
||||
iconClass: 'fa fa-microchip'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { ListWidget } from './list-widget';
|
||||
import { BoardsListWidget } from './boards-list-widget';
|
||||
|
||||
@injectable()
|
||||
export abstract class ListWidgetFrontendContribution extends AbstractViewContribution<ListWidget> implements FrontendApplicationContribution {
|
||||
|
||||
async initializeLayout(): Promise<void> {
|
||||
await this.openView();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: BoardsListWidget.WIDGET_ID,
|
||||
widgetName: BoardsListWidget.WIDGET_LABEL,
|
||||
defaultWidgetOptions: {
|
||||
area: 'left',
|
||||
rank: 600
|
||||
},
|
||||
toggleCommandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
|
||||
toggleKeybinding: 'ctrlcmd+shift+b'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
72
arduino-ide-extension/src/browser/boards/list-widget.tsx
Normal file
72
arduino-ide-extension/src/browser/boards/list-widget.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as React from 'react';
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import { Message } from '@phosphor/messaging';
|
||||
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { FilterableListContainer } from '../components/component-list/filterable-list-container';
|
||||
import { BoardsService } from '../../common/protocol/boards-service';
|
||||
|
||||
@injectable()
|
||||
export abstract class ListWidget extends ReactWidget {
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(WindowService)
|
||||
protected readonly windowService: WindowService;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const { id, title, iconClass } = this.widgetProps();
|
||||
this.id = id;
|
||||
this.title.label = title;
|
||||
this.title.caption = title;
|
||||
this.title.iconClass = iconClass;
|
||||
this.title.closable = true;
|
||||
this.addClass(ListWidget.Styles.LIST_WIDGET_CLASS);
|
||||
this.node.tabIndex = 0; // To be able to set the focus on the widget.
|
||||
}
|
||||
|
||||
protected abstract widgetProps(): ListWidget.Props;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
this.node.focus();
|
||||
this.render();
|
||||
}
|
||||
|
||||
protected onUpdateRequest(msg: Message): void {
|
||||
super.onUpdateRequest(msg);
|
||||
this.render();
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return <FilterableListContainer
|
||||
service={this.boardsService}
|
||||
windowService={this.windowService}
|
||||
/>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace ListWidget {
|
||||
|
||||
/**
|
||||
* Props for customizing the abstract list widget.
|
||||
*/
|
||||
export interface Props {
|
||||
readonly id: string;
|
||||
readonly title: string;
|
||||
readonly iconClass: string;
|
||||
}
|
||||
|
||||
export namespace Styles {
|
||||
export const LIST_WIDGET_CLASS = 'arduino-list-widget'
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import * as React from 'react';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
|
||||
export class ComponentListItem extends React.Component<ComponentListItem.Props> {
|
||||
|
||||
private onClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => {
|
||||
const { target } = event.nativeEvent;
|
||||
if (target instanceof HTMLAnchorElement) {
|
||||
this.props.windowService.openNewWindow(target.href);
|
||||
event.nativeEvent.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { item } = this.props;
|
||||
|
||||
const style = ComponentListItem.Styles;
|
||||
const name = <span className={style.NAME_CLASS}>{item.name}</span>;
|
||||
const author = <span className={style.AUTHOR_CLASS}>{item.author}</span>;
|
||||
const installedVersion = !!item.installedVersion && <React.Fragment>
|
||||
<span className={style.VERSION_CLASS}>Version {item.installedVersion}</span>
|
||||
<span className={style.INSTALLED_CLASS}>INSTALLED</span>
|
||||
</React.Fragment>;
|
||||
|
||||
const summary = <div className={style.SUMMARY_CLASS}>{item.summary}</div>;
|
||||
const description = !!item.description && <div className={style.DESCRIPTION_CLASS}>{item.description}</div>;
|
||||
|
||||
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
|
||||
const install = item.installable && !item.installedVersion && <button className={style.INSTALL_BTN_CLASS}>INSTALL</button>;
|
||||
|
||||
return <div className={[style.LIST_ITEM_CLASS, style.NO_SELECT_CLASS].join(' ')}>
|
||||
<div className={style.HEADER_CLASS}>
|
||||
<span>{name} by {author}</span>
|
||||
{installedVersion}
|
||||
</div>
|
||||
<div className={style.CONTENT_CLASS}>
|
||||
{summary}
|
||||
{description}
|
||||
</div>
|
||||
<div className={style.FOOTER_CLASS}>
|
||||
{moreInfo}
|
||||
{install}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace ComponentListItem {
|
||||
|
||||
export interface Props {
|
||||
readonly item: ArduinoComponent;
|
||||
readonly windowService: WindowService;
|
||||
}
|
||||
|
||||
export namespace Styles {
|
||||
export const LIST_ITEM_CLASS = 'component-list-item';
|
||||
export const HEADER_CLASS = 'header';
|
||||
export const CONTENT_CLASS = 'content';
|
||||
export const FOOTER_CLASS = 'footer';
|
||||
export const INSTALLED_CLASS = 'installed';
|
||||
export const NO_SELECT_CLASS = 'noselect';
|
||||
|
||||
export const NAME_CLASS = 'name';
|
||||
export const AUTHOR_CLASS = 'author';
|
||||
export const VERSION_CLASS = 'version';
|
||||
export const SUMMARY_CLASS = 'summary';
|
||||
export const DESCRIPTION_CLASS = 'description';
|
||||
export const INSTALL_BTN_CLASS = 'install';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as React from 'react';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { ComponentListItem } from './component-list-item';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
|
||||
export class ComponentList extends React.Component<ComponentList.Props> {
|
||||
|
||||
render(): React.ReactNode {
|
||||
return <div>
|
||||
{this.props.items.map(item => <ComponentListItem key={item.name} item={item} windowService={this.props.windowService}/>)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace ComponentList {
|
||||
|
||||
export interface Props {
|
||||
readonly items: ArduinoComponent[];
|
||||
readonly windowService: WindowService;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import * as React from 'react';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { ComponentList } from './component-list';
|
||||
import { SearchBar } from './search-bar';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
|
||||
export class FilterableListContainer extends React.Component<FilterableListContainer.Props, FilterableListContainer.State> {
|
||||
|
||||
constructor(props: Readonly<FilterableListContainer.Props>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
filterText: '',
|
||||
items: []
|
||||
};
|
||||
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount(): void {
|
||||
this.handleFilterTextChange('');
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return <div className={FilterableListContainer.Styles.FILTERABLE_LIST_CONTAINER_CLASS}>
|
||||
<SearchBar
|
||||
filterText={this.state.filterText}
|
||||
onFilterTextChanged={this.handleFilterTextChange}
|
||||
/>
|
||||
<ComponentList
|
||||
items={this.state.items}
|
||||
windowService={this.props.windowService}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
private handleFilterTextChange(filterText: string): void {
|
||||
this.props.service.search({ query: filterText }).then(result => {
|
||||
const { items } = result;
|
||||
this.setState({
|
||||
filterText,
|
||||
items
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace FilterableListContainer {
|
||||
|
||||
export interface Props {
|
||||
readonly service: ComponentSource;
|
||||
readonly windowService: WindowService;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
filterText: string;
|
||||
items: ArduinoComponent[];
|
||||
}
|
||||
|
||||
export namespace Styles {
|
||||
export const FILTERABLE_LIST_CONTAINER_CLASS = 'filterable-list-container';
|
||||
}
|
||||
|
||||
export interface ComponentSource {
|
||||
search(req: { query: string }): Promise<{ items: ArduinoComponent[] }>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export class SearchBar extends React.Component<SearchBar.Props> {
|
||||
|
||||
constructor(props: Readonly<SearchBar.Props>) {
|
||||
super(props);
|
||||
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return <form className={SearchBar.Styles.SEARCH_BAR_CLASS}>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='Search'
|
||||
size={1}
|
||||
value={this.props.filterText}
|
||||
onChange={this.handleFilterTextChange}
|
||||
/>
|
||||
</form>;
|
||||
}
|
||||
|
||||
private handleFilterTextChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
this.props.onFilterTextChanged(event.target.value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace SearchBar {
|
||||
|
||||
export interface Props {
|
||||
filterText: string;
|
||||
onFilterTextChanged(filterText: string): void;
|
||||
}
|
||||
|
||||
export namespace Styles {
|
||||
export const SEARCH_BAR_CLASS = 'search-bar';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import * as React from 'react';
|
||||
// TODO: make this `async`.
|
||||
// import { Async } from 'react-select/lib/Async';
|
||||
import { BoardsService, Board } from '../../common/protocol/boards-service';
|
||||
|
||||
export class ConnectedBoards extends React.Component<ConnectedBoards.Props, ConnectedBoards.State> {
|
||||
|
||||
static TOOLBAR_ID: 'connected-boards-toolbar';
|
||||
|
||||
render(): React.ReactNode {
|
||||
return <div className={ConnectedBoards.Styles.CONNECTED_BOARDS_CLASS}>
|
||||
{this.select(this.state ? this.state.boards : undefined, this.state ? this.state.current : undefined)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.props.boardsService.connectedBoards().then(result => {
|
||||
const { boards, current } = result;
|
||||
this.setState({
|
||||
boards,
|
||||
current
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private select(boards: Board[] | undefined, current: Board | undefined): React.ReactNode {
|
||||
// Initial pessimistic.
|
||||
const options = [<option>Loading...</option>];
|
||||
if (boards) {
|
||||
options.length = 0;
|
||||
options.push(...boards.map(b => b.name).map(name => <option value={name} key={name}>{name}</option>));
|
||||
}
|
||||
const onChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const current = (boards || []).find(board => board.name === event.target.value);
|
||||
this.setState({ current });
|
||||
};
|
||||
return <select
|
||||
onChange={onChange}
|
||||
value={current ? current.name : 'Loading...'}
|
||||
name={current ? current.name : 'Loading...'}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace ConnectedBoards {
|
||||
|
||||
export interface Props {
|
||||
readonly boardsService: BoardsService;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
boards?: Board[];
|
||||
current?: Board;
|
||||
}
|
||||
|
||||
export namespace Styles {
|
||||
export const CONNECTED_BOARDS_CLASS = 'connected-boards';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { LanguageGrammarDefinitionContribution, TextmateRegistry } from '@theia/monaco/lib/browser/textmate';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoLanguageGrammarContribution implements LanguageGrammarDefinitionContribution {
|
||||
|
||||
static INO_LANGUAGE_ID = 'ino';
|
||||
|
||||
registerTextmateLanguage(registry: TextmateRegistry) {
|
||||
monaco.languages.register({
|
||||
id: ArduinoLanguageGrammarContribution.INO_LANGUAGE_ID,
|
||||
extensions: ['.ino'],
|
||||
aliases: ['INO', 'Ino', 'ino'],
|
||||
});
|
||||
|
||||
monaco.languages.setLanguageConfiguration(ArduinoLanguageGrammarContribution.INO_LANGUAGE_ID, this.configuration);
|
||||
|
||||
const inoGrammar = require('../../../data/ino.tmLanguage.json');
|
||||
registry.registerTextmateGrammarScope('source.ino', {
|
||||
async getGrammarDefinition() {
|
||||
return {
|
||||
format: 'json',
|
||||
content: inoGrammar
|
||||
};
|
||||
}
|
||||
});
|
||||
registry.mapLanguageIdToTextmateGrammar(ArduinoLanguageGrammarContribution.INO_LANGUAGE_ID, 'source.ino');
|
||||
}
|
||||
|
||||
private readonly configuration: monaco.languages.LanguageConfiguration = {
|
||||
comments: {
|
||||
lineComment: '//',
|
||||
blockComment: ['/*', '*/'],
|
||||
},
|
||||
brackets: [
|
||||
['{', '}'],
|
||||
['[', ']'],
|
||||
['(', ')']
|
||||
],
|
||||
autoClosingPairs: [
|
||||
{ open: '[', close: ']' },
|
||||
{ open: '{', close: '}' },
|
||||
{ open: '(', close: ')' },
|
||||
{ open: '\'', close: '\'', notIn: ['string', 'comment'] },
|
||||
{ open: '"', close: '"', notIn: ['string'] },
|
||||
{ open: '/*', close: ' */', notIn: ['string'] }
|
||||
],
|
||||
surroundingPairs: [
|
||||
{ open: '{', close: '}' },
|
||||
{ open: '[', close: ']' },
|
||||
{ open: '(', close: ')' },
|
||||
{ open: '"', close: '"' },
|
||||
{ open: '\'', close: '\'' },
|
||||
],
|
||||
folding: {
|
||||
markers: {
|
||||
start: new RegExp('^\\s*#pragma\\s+region\\b'),
|
||||
end: new RegExp('^\\s*#pragma\\s+endregion\\b')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ListWidget } from './list-widget';
|
||||
|
||||
export class LibraryListWidget extends ListWidget {
|
||||
|
||||
static WIDGET_ID = 'library-list-widget';
|
||||
static WIDGET_LABEL = 'Library Manager';
|
||||
|
||||
protected widgetProps(): ListWidget.Props {
|
||||
return {
|
||||
id: LibraryListWidget.WIDGET_ID,
|
||||
title: LibraryListWidget.WIDGET_LABEL,
|
||||
iconClass: 'fa fa-book' // TODO: find a better icon
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { ListWidget } from './list-widget';
|
||||
import { LibraryListWidget } from './library-list-widget';
|
||||
|
||||
@injectable()
|
||||
export abstract class ListWidgetFrontendContribution extends AbstractViewContribution<ListWidget> implements FrontendApplicationContribution {
|
||||
|
||||
async initializeLayout(): Promise<void> {
|
||||
await this.openView();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendContribution {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: LibraryListWidget.WIDGET_ID,
|
||||
widgetName: LibraryListWidget.WIDGET_LABEL,
|
||||
defaultWidgetOptions: {
|
||||
area: 'left',
|
||||
rank: 600
|
||||
},
|
||||
toggleCommandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
|
||||
toggleKeybinding: 'ctrlcmd+shift+l'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
72
arduino-ide-extension/src/browser/library/list-widget.tsx
Normal file
72
arduino-ide-extension/src/browser/library/list-widget.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as React from 'react';
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import { Message } from '@phosphor/messaging';
|
||||
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { FilterableListContainer } from '../components/component-list/filterable-list-container';
|
||||
import { LibraryService } from '../../common/protocol/library-service';
|
||||
|
||||
@injectable()
|
||||
export abstract class ListWidget extends ReactWidget {
|
||||
|
||||
@inject(LibraryService)
|
||||
protected readonly libraryService: LibraryService;
|
||||
|
||||
@inject(WindowService)
|
||||
protected readonly windowService: WindowService;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const { id, title, iconClass } = this.widgetProps();
|
||||
this.id = id;
|
||||
this.title.label = title;
|
||||
this.title.caption = title;
|
||||
this.title.iconClass = iconClass;
|
||||
this.title.closable = true;
|
||||
this.addClass(ListWidget.Styles.LIST_WIDGET_CLASS);
|
||||
this.node.tabIndex = 0; // To be able to set the focus on the widget.
|
||||
}
|
||||
|
||||
protected abstract widgetProps(): ListWidget.Props;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
this.node.focus();
|
||||
this.render();
|
||||
}
|
||||
|
||||
protected onUpdateRequest(msg: Message): void {
|
||||
super.onUpdateRequest(msg);
|
||||
this.render();
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return <FilterableListContainer
|
||||
service={this.libraryService}
|
||||
windowService={this.windowService}
|
||||
/>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace ListWidget {
|
||||
|
||||
/**
|
||||
* Props for customizing the abstract list widget.
|
||||
*/
|
||||
export interface Props {
|
||||
readonly id: string;
|
||||
readonly title: string;
|
||||
readonly iconClass: string;
|
||||
}
|
||||
|
||||
export namespace Styles {
|
||||
export const LIST_WIDGET_CLASS = 'arduino-list-widget'
|
||||
}
|
||||
|
||||
}
|
||||
1
arduino-ide-extension/src/browser/style/index.css
Normal file
1
arduino-ide-extension/src/browser/style/index.css
Normal file
@@ -0,0 +1 @@
|
||||
@import './list-widget.css';
|
||||
76
arduino-ide-extension/src/browser/style/list-widget.css
Normal file
76
arduino-ide-extension/src/browser/style/list-widget.css
Normal file
@@ -0,0 +1,76 @@
|
||||
.arduino-list-widget {
|
||||
color: var(--theia-ui-font-color1);
|
||||
}
|
||||
|
||||
.arduino-list-widget .search-bar > input {
|
||||
margin: 0px 5px 0px 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.component-list-item {
|
||||
padding: 10px;
|
||||
font-size: var(--theia-ui-font-size1);
|
||||
}
|
||||
|
||||
.component-list-item:hover {
|
||||
background: var(--theia-accent-color4);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.component-list-item:hover .meta-info {
|
||||
color: var(--theia-ui-font-color1);
|
||||
}
|
||||
|
||||
.component-list-item .meta-info {
|
||||
color: var(--theia-ui-font-color3);
|
||||
}
|
||||
|
||||
.component-list-item .header {
|
||||
padding-bottom: 2px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.component-list-item .header .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.component-list-item .header .author {
|
||||
font-weight: bold;
|
||||
color: var(--theia-ui-font-color2);
|
||||
}
|
||||
|
||||
.component-list-item .header .version {
|
||||
margin-left: auto;
|
||||
justify-self: end;
|
||||
color: var(--theia-ui-font-color2);
|
||||
}
|
||||
|
||||
.component-list-item .header .installed {
|
||||
margin-left: 4px;
|
||||
justify-self: end;
|
||||
background-color: var(--theia-accent-color2);
|
||||
padding: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.component-list-item .footer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.component-list-item .footer a {
|
||||
color: var(--theia-brand-color1);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.component-list-item .footer .install {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.component-list-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.component-list-item strong.installed {
|
||||
color: rgb(0, 151, 157)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { WorkspaceServiceExt } from './workspace-service-ext';
|
||||
|
||||
/**
|
||||
* This is a workaround to be able to inject the workspace service to the backend with its service path.
|
||||
*/
|
||||
@injectable()
|
||||
export class WorkspaceServiceExtImpl implements WorkspaceServiceExt {
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly delegate: WorkspaceService;
|
||||
|
||||
async roots(): Promise<string[]> {
|
||||
const stats = await this.delegate.roots;
|
||||
return stats.map(stat => stat.uri);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const WorkspaceServiceExtPath = '/services/workspace-service-ext';
|
||||
export const WorkspaceServiceExt = Symbol('WorkspaceServiceExt');
|
||||
export interface WorkspaceServiceExt {
|
||||
roots(): Promise<string[]>;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
export interface ArduinoComponent {
|
||||
readonly name: string;
|
||||
readonly author: string;
|
||||
readonly summary: string;
|
||||
readonly description: string;
|
||||
readonly moreInfoLink?: string;
|
||||
|
||||
readonly availableVersions: string[];
|
||||
readonly installable: boolean;
|
||||
|
||||
readonly installedVersion?: string;
|
||||
}
|
||||
11
arduino-ide-extension/src/common/protocol/boards-service.ts
Normal file
11
arduino-ide-extension/src/common/protocol/boards-service.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ArduinoComponent } from "./arduino-component";
|
||||
|
||||
export const BoardsServicePath = '/services/boards-service';
|
||||
export const BoardsService = Symbol('BoardsService');
|
||||
export interface BoardsService {
|
||||
connectedBoards(): Promise<{ boards: Board[], current?: Board }>;
|
||||
search(options: { query?: string }): Promise<{ items: Board[] }>;
|
||||
}
|
||||
|
||||
export interface Board extends ArduinoComponent {
|
||||
}
|
||||
14
arduino-ide-extension/src/common/protocol/core-service.ts
Normal file
14
arduino-ide-extension/src/common/protocol/core-service.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const CoreServicePath = '/services/core-service';
|
||||
export const CoreService = Symbol('CoreService');
|
||||
export interface CoreService {
|
||||
compile(options: CoreService.Compile.Options): Promise<string>;
|
||||
upload(): Promise<void>;
|
||||
}
|
||||
|
||||
export namespace CoreService {
|
||||
export namespace Compile {
|
||||
export interface Options {
|
||||
readonly uri: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
arduino-ide-extension/src/common/protocol/library-service.ts
Normal file
16
arduino-ide-extension/src/common/protocol/library-service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ArduinoComponent } from "./arduino-component";
|
||||
|
||||
export const LibraryServicePath = '/services/library-service';
|
||||
export const LibraryService = Symbol('LibraryService');
|
||||
export interface LibraryService {
|
||||
search(options: { query?: string }): Promise<{ items: Library[] }>;
|
||||
}
|
||||
|
||||
export interface Library extends ArduinoComponent {
|
||||
readonly builtIn?: boolean;
|
||||
}
|
||||
|
||||
export namespace Library {
|
||||
// TODO: figure out whether we need a dedicated `version` type.
|
||||
export type Version = string;
|
||||
}
|
||||
66
arduino-ide-extension/src/node/arduino-backend-module.ts
Normal file
66
arduino-ide-extension/src/node/arduino-backend-module.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { ArduinoDaemon } from './arduino-daemon';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
|
||||
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
|
||||
import { BoardsService, BoardsServicePath } from '../common/protocol/boards-service';
|
||||
import { LibraryServiceImpl } from './library-service-impl';
|
||||
import { BoardsServiceImpl } from './boards-service-impl';
|
||||
import { CoreServiceImpl } from './core-service-impl';
|
||||
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
|
||||
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
|
||||
import { WorkspaceServiceExtPath, WorkspaceServiceExt } from '../browser/workspace-service-ext';
|
||||
import { CoreClientProviderImpl } from './core-client-provider-impl';
|
||||
import { CoreClientProviderPath, CoreClientProvider } from './core-client-provider';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(ArduinoDaemon).toSelf().inSingletonScope();
|
||||
bind(BackendApplicationContribution).toService(ArduinoDaemon);
|
||||
|
||||
// Library service
|
||||
const libraryServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(LibraryServiceImpl).toSelf().inSingletonScope();
|
||||
bind(LibraryService).toService(LibraryServiceImpl);
|
||||
bindBackendService(LibraryServicePath, LibraryService);
|
||||
});
|
||||
bind(ConnectionContainerModule).toConstantValue(libraryServiceConnectionModule);
|
||||
|
||||
// Boards service
|
||||
const boardsServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(BoardsServiceImpl).toSelf().inSingletonScope();
|
||||
bind(BoardsService).toService(BoardsServiceImpl);
|
||||
bindBackendService(BoardsServicePath, BoardsService);
|
||||
});
|
||||
bind(ConnectionContainerModule).toConstantValue(boardsServiceConnectionModule);
|
||||
|
||||
// Arduino core client provider per Theia connection.
|
||||
const coreClientProviderConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(CoreClientProviderImpl).toSelf().inSingletonScope();
|
||||
bind(CoreClientProvider).toService(CoreClientProviderImpl);
|
||||
bindBackendService(CoreClientProviderPath, CoreClientProvider);
|
||||
});
|
||||
bind(ConnectionContainerModule).toConstantValue(coreClientProviderConnectionModule);
|
||||
|
||||
// Core service -> `verify` and `upload`. One per Theia connection.
|
||||
const connectionConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(CoreServiceImpl).toSelf().inSingletonScope();
|
||||
bind(CoreService).toService(CoreServiceImpl);
|
||||
bindBackendService(BoardsServicePath, BoardsService);
|
||||
bindBackendService(CoreClientProviderPath, CoreClientProvider);
|
||||
bindBackendService(CoreServicePath, CoreService);
|
||||
});
|
||||
bind(ConnectionContainerModule).toConstantValue(connectionConnectionModule);
|
||||
|
||||
// Bind the workspace service extension to the backend per Theia connection.
|
||||
// So that we can access the workspace roots of the frontend.
|
||||
const workspaceServiceExtConnectionModule = ConnectionContainerModule.create(({ bindFrontendService }) => {
|
||||
bindFrontendService(WorkspaceServiceExtPath, WorkspaceServiceExt);
|
||||
});
|
||||
bind(ConnectionContainerModule).toConstantValue(workspaceServiceExtConnectionModule);
|
||||
|
||||
// Logger for the Arduino daemon
|
||||
bind(ILogger).toDynamicValue(ctx => {
|
||||
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||
return parentLogger.child('daemon');
|
||||
}).inSingletonScope().whenTargetNamed('daemon');
|
||||
});
|
||||
46
arduino-ide-extension/src/node/arduino-daemon.ts
Normal file
46
arduino-ide-extension/src/node/arduino-daemon.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as os from 'os';
|
||||
import { exec } from 'child_process';
|
||||
import { join, resolve } from 'path';
|
||||
import { inject, injectable, named } from 'inversify';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { BackendApplicationContribution } from '@theia/core/lib/node';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { DaemonLog } from './daemon-log';
|
||||
|
||||
const EXECUTABLE_PATH = resolve(join(__dirname, '..', '..', 'build', `arduino-cli.${os.platform()}`))
|
||||
|
||||
@injectable()
|
||||
export class ArduinoDaemon implements BackendApplicationContribution {
|
||||
|
||||
@inject(ILogger)
|
||||
@named('daemon')
|
||||
protected readonly logger: ILogger
|
||||
|
||||
protected isReady = new Deferred<boolean>();
|
||||
|
||||
async onStart() {
|
||||
try {
|
||||
const daemon = exec(`${EXECUTABLE_PATH} --debug daemon`, (err, stdout, stderr) => {
|
||||
if (err || stderr) {
|
||||
console.log(err || new Error(stderr));
|
||||
return;
|
||||
}
|
||||
console.log(stdout);
|
||||
});
|
||||
if (daemon.stdout) {
|
||||
daemon.stdout.on('data', data => DaemonLog.log(this.logger, data.toString()));
|
||||
}
|
||||
if (daemon.stderr) {
|
||||
daemon.stderr.on('data', data => DaemonLog.log(this.logger, data.toString()));
|
||||
}
|
||||
if (daemon.stderr) {
|
||||
daemon.on('exit', (code, signal) => DaemonLog.log(this.logger, `Daemon exited with code: ${code}. Signal was: ${signal}.`));
|
||||
}
|
||||
await new Promise((resolve, reject) => setTimeout(resolve, 2000));
|
||||
this.isReady.resolve();
|
||||
} catch (error) {
|
||||
this.isReady.reject(error || new Error('failed to start arduino-cli'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
37
arduino-ide-extension/src/node/boards-service-impl.ts
Normal file
37
arduino-ide-extension/src/node/boards-service-impl.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { BoardsService, Board } from '../common/protocol/boards-service';
|
||||
import { PlatformSearchReq, PlatformSearchResp } from './cli-protocol/core_pb';
|
||||
import { CoreClientProvider } from './core-client-provider';
|
||||
|
||||
@injectable()
|
||||
export class BoardsServiceImpl implements BoardsService {
|
||||
|
||||
@inject(CoreClientProvider)
|
||||
protected readonly coreClientProvider: CoreClientProvider;
|
||||
|
||||
async connectedBoards(): Promise<{ boards: Board[], current?: Board }> {
|
||||
return { boards: [] };
|
||||
}
|
||||
|
||||
async search(options: { query?: string }): Promise<{ items: Board[] }> {
|
||||
let items: Board[] = [];
|
||||
|
||||
const { client, instance } = await this.coreClientProvider.getClient();
|
||||
|
||||
const req = new PlatformSearchReq();
|
||||
req.setSearchArgs(options.query || "");
|
||||
req.setInstance(instance);
|
||||
const resp = await new Promise<PlatformSearchResp>((resolve, reject) => client.platformSearch(req, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp)));
|
||||
items = resp.getSearchOutputList().map(o => <Board>{
|
||||
name: o.getName(),
|
||||
author: "Someone",
|
||||
availableVersions: [],
|
||||
description: "lorem ipsum sit dolor amet",
|
||||
installable: false,
|
||||
summary: "has none"
|
||||
});
|
||||
|
||||
return { items };
|
||||
}
|
||||
|
||||
}
|
||||
91
arduino-ide-extension/src/node/core-client-provider-impl.ts
Normal file
91
arduino-ide-extension/src/node/core-client-provider-impl.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import * as grpc from 'grpc';
|
||||
import { ArduinoCoreClient } from './cli-protocol/commands_grpc_pb';
|
||||
import { InitResp, InitReq, Configuration, UpdateIndexReq, UpdateIndexResp } from './cli-protocol/commands_pb';
|
||||
import { WorkspaceServiceExt } from '../browser/workspace-service-ext';
|
||||
import { FileSystem } from '@theia/filesystem/lib/common';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { CoreClientProvider, Client } from './core-client-provider';
|
||||
|
||||
@injectable()
|
||||
export class CoreClientProviderImpl implements CoreClientProvider {
|
||||
|
||||
@inject(FileSystem)
|
||||
protected readonly fileSystem: FileSystem;
|
||||
|
||||
@inject(WorkspaceServiceExt)
|
||||
protected readonly workspaceServiceExt: WorkspaceServiceExt;
|
||||
|
||||
protected clients = new Map<string, Client>();
|
||||
|
||||
async getClient(workspaceRootOrResourceUri?: string): Promise<Client> {
|
||||
const roots = await this.workspaceServiceExt.roots();
|
||||
if (!workspaceRootOrResourceUri) {
|
||||
return this.getOrCreateClient(roots[0]);
|
||||
}
|
||||
const root = roots
|
||||
.sort((left, right) => right.length - left.length) // Longest "paths" first
|
||||
.map(uri => new URI(uri))
|
||||
.find(uri => uri.isEqualOrParent(new URI(workspaceRootOrResourceUri)));
|
||||
if (!root) {
|
||||
console.warn(`Could not retrieve the container workspace root for URI: ${workspaceRootOrResourceUri}.`);
|
||||
console.warn(`Falling back to ${roots[0]}`);
|
||||
return this.getOrCreateClient(roots[0]);
|
||||
}
|
||||
return this.getOrCreateClient(root.toString());
|
||||
}
|
||||
|
||||
protected async getOrCreateClient(rootUri: string): Promise<Client> {
|
||||
const existing = this.clients.get(rootUri);
|
||||
if (existing) {
|
||||
console.debug(`Reusing existing client for ${rootUri}.`);
|
||||
return existing;
|
||||
}
|
||||
|
||||
console.info(` >>> Creating and caching a new client for ${rootUri}...`);
|
||||
const client = new ArduinoCoreClient('localhost:50051', grpc.credentials.createInsecure());
|
||||
const config = new Configuration();
|
||||
const path = await this.fileSystem.getFsPath(rootUri);
|
||||
if (!path) {
|
||||
throw new Error(`Could not resolve file-system path of URI: ${rootUri}.`);
|
||||
}
|
||||
config.setSketchbookdir(path);
|
||||
config.setDatadir(path);
|
||||
config.setDownloadsdir(path);
|
||||
|
||||
const initReq = new InitReq();
|
||||
initReq.setConfiguration(config);
|
||||
const initResp = await new Promise<InitResp>((resolve, reject) => client.init(initReq, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp)));
|
||||
const instance = initResp.getInstance();
|
||||
if (!instance) {
|
||||
throw new Error(`Could not retrieve instance from the initialize response.`);
|
||||
}
|
||||
const updateReq = new UpdateIndexReq();
|
||||
updateReq.setInstance(instance);
|
||||
const updateResp = client.updateIndex(updateReq);
|
||||
updateResp.on('data', (o: UpdateIndexResp) => {
|
||||
const progress = o.getDownloadProgress();
|
||||
if (progress) {
|
||||
if (progress.getCompleted()) {
|
||||
console.log(`Download${progress.getFile() ? ` of ${progress.getFile()}` : ''} completed.`);
|
||||
} else {
|
||||
console.log(`Downloading${progress.getFile() ? ` ${progress.getFile()}:` : ''} ${progress.getDownloaded()}.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
// TODO: revisit this!!!
|
||||
// `updateResp.on('data'` is called only when running, for instance, `compile`. It does not run eagerly.
|
||||
// await new Promise<void>((resolve, reject) => {
|
||||
// updateResp.on('close', resolve);
|
||||
// updateResp.on('error', reject);
|
||||
// });
|
||||
|
||||
const result = {
|
||||
client,
|
||||
instance
|
||||
}
|
||||
this.clients.set(rootUri, result);
|
||||
console.info(` <<< New client has been successfully created and cached for ${rootUri}.`);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
13
arduino-ide-extension/src/node/core-client-provider.ts
Normal file
13
arduino-ide-extension/src/node/core-client-provider.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Instance } from './cli-protocol/common_pb';
|
||||
import { ArduinoCoreClient } from './cli-protocol/commands_grpc_pb';
|
||||
|
||||
export const CoreClientProviderPath = '/services/core-client-provider';
|
||||
export const CoreClientProvider = Symbol('CoreClientProvider');
|
||||
export interface CoreClientProvider {
|
||||
getClient(workspaceRootOrResourceUri?: string): Promise<Client>;
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
readonly client: ArduinoCoreClient;
|
||||
readonly instance: Instance;
|
||||
}
|
||||
84
arduino-ide-extension/src/node/core-service-impl.ts
Normal file
84
arduino-ide-extension/src/node/core-service-impl.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { FileSystem } from '@theia/filesystem/lib/common/filesystem';
|
||||
import { CoreService } from '../common/protocol/core-service';
|
||||
import { CompileReq } from './cli-protocol/compile_pb';
|
||||
import { BoardsService } from '../common/protocol/boards-service';
|
||||
import { CoreClientProvider } from './core-client-provider';
|
||||
import { PlatformInstallReq } from './cli-protocol/core_pb';
|
||||
import { LibraryInstallReq } from './cli-protocol/lib_pb';
|
||||
|
||||
@injectable()
|
||||
export class CoreServiceImpl implements CoreService {
|
||||
|
||||
@inject(CoreClientProvider)
|
||||
protected readonly coreClientProvider: CoreClientProvider;
|
||||
|
||||
@inject(FileSystem)
|
||||
protected readonly fileSystem: FileSystem;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
async compile(options: CoreService.Compile.Options): Promise<string> {
|
||||
console.log('compile', options);
|
||||
const { uri } = options;
|
||||
const sketchpath = await this.fileSystem.getFsPath(options.uri);
|
||||
if (!sketchpath) {
|
||||
throw new Error(`Cannot resolve FS path for URI: ${uri}.`);
|
||||
}
|
||||
const { client, instance } = await this.coreClientProvider.getClient(uri);
|
||||
// const boards = await this.boardsService.connectedBoards();
|
||||
// if (!boards.current) {
|
||||
// throw new Error(`No selected board. The connected boards were: ${boards.boards}.`);
|
||||
// }
|
||||
// https://github.com/cmaglie/arduino-cli/blob/bd5e78701e7546787649d3cca6b21c5d22d0e438/cli/compile/compile.go#L78-L88
|
||||
|
||||
const installPlatformReq = new PlatformInstallReq();
|
||||
installPlatformReq.setArchitecture('samd');
|
||||
installPlatformReq.setVersion('1.6.0');
|
||||
installPlatformReq.setInstance(instance);
|
||||
const resp = client.platformInstall(installPlatformReq);
|
||||
console.log(resp);
|
||||
|
||||
|
||||
|
||||
const installLibReq = new LibraryInstallReq();
|
||||
installLibReq.setInstance(instance);
|
||||
installLibReq.setName('arduino:samd');
|
||||
const installResp = client.libraryInstall(installLibReq);
|
||||
const xxx = await new Promise<string>((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
installResp.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
installResp.on('error', error => reject(error));
|
||||
installResp.on('end', () => resolve(Buffer.concat(chunks).toString('utf8').trim()))
|
||||
});
|
||||
console.log('xxx', xxx);
|
||||
|
||||
const compilerReq = new CompileReq();
|
||||
compilerReq.setInstance(instance);
|
||||
compilerReq.setSketchpath(sketchpath);
|
||||
compilerReq.setFqbn('arduino:samd'/*boards.current.name*/);
|
||||
// request.setShowproperties(false);
|
||||
// request.setPreprocess(false);
|
||||
// request.setBuildcachepath('');
|
||||
// request.setBuildpath('');
|
||||
// request.setBuildpropertiesList([]);
|
||||
// request.setWarnings('none');
|
||||
// request.setVerbose(true);
|
||||
// request.setQuiet(false);
|
||||
// request.setVidpid('');
|
||||
// request.setExportfile('');
|
||||
const result = client.compile(compilerReq);
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
result.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
result.on('error', error => reject(error));
|
||||
result.on('end', () => resolve(Buffer.concat(chunks).toString('utf8').trim()))
|
||||
});
|
||||
}
|
||||
|
||||
upload(): Promise<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
}
|
||||
67
arduino-ide-extension/src/node/daemon-log.ts
Normal file
67
arduino-ide-extension/src/node/daemon-log.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { ILogger, LogLevel } from '@theia/core/lib/common/logger';
|
||||
|
||||
export interface DaemonLog {
|
||||
readonly time: string;
|
||||
readonly level: DaemonLog.Level;
|
||||
readonly msg: string;
|
||||
}
|
||||
|
||||
export namespace DaemonLog {
|
||||
|
||||
export type Level = 'info' | 'debug' | 'warning' | 'error';
|
||||
|
||||
export function is(arg: any | undefined): arg is DaemonLog {
|
||||
return !!arg
|
||||
&& typeof arg.time === 'string'
|
||||
&& typeof arg.level === 'string'
|
||||
&& typeof arg.msg === 'string'
|
||||
}
|
||||
|
||||
export function toLogLevel(log: DaemonLog): LogLevel {
|
||||
const { level } = log;
|
||||
switch (level) {
|
||||
case 'info': return LogLevel.INFO;
|
||||
case 'debug': return LogLevel.DEBUG;
|
||||
case 'error': return LogLevel.ERROR;
|
||||
case 'warning': return LogLevel.WARN;
|
||||
default: return LogLevel.INFO;
|
||||
}
|
||||
}
|
||||
|
||||
export function log(logger: ILogger, toLog: string): void {
|
||||
const segments = toLog.split('time').filter(s => s.trim().length > 0);
|
||||
for (const segment of segments) {
|
||||
const maybeDaemonLog = parse(`time${segment}`.trim());
|
||||
if (is(maybeDaemonLog)) {
|
||||
const { msg } = maybeDaemonLog;
|
||||
logger.log(toLogLevel(maybeDaemonLog), msg);
|
||||
} else {
|
||||
logger.info(toLog.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Super naive.
|
||||
function parse(toLog: string): string | DaemonLog {
|
||||
const rawSegments = toLog.split(/(\s+)/)
|
||||
.map(segment => segment.replace(/['"]+/g, ''))
|
||||
.map(segment => segment.trim())
|
||||
.filter(segment => segment.length > 0);
|
||||
|
||||
const timeIndex = rawSegments.findIndex(segment => segment.startsWith('time='));
|
||||
const levelIndex = rawSegments.findIndex(segment => segment.startsWith('level='));
|
||||
const msgIndex = rawSegments.findIndex(segment => segment.startsWith('msg='));
|
||||
if (rawSegments.length > 2
|
||||
&& timeIndex !== -1
|
||||
&& levelIndex !== -1
|
||||
&& msgIndex !== -1) {
|
||||
return {
|
||||
time: rawSegments[timeIndex].split('=')[1],
|
||||
level: rawSegments[levelIndex].split('=')[1] as Level,
|
||||
msg: [rawSegments[msgIndex].split('=')[1], ...rawSegments.slice(msgIndex + 1)].join(' ')
|
||||
}
|
||||
}
|
||||
// Otherwise, log the string as is.
|
||||
return toLog;
|
||||
}
|
||||
}
|
||||
46
arduino-ide-extension/src/node/library-service-impl.ts
Normal file
46
arduino-ide-extension/src/node/library-service-impl.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { Library, LibraryService } from '../common/protocol/library-service';
|
||||
|
||||
@injectable()
|
||||
export class LibraryServiceImpl implements LibraryService {
|
||||
|
||||
async search(options: { query?: string; }): Promise<{ items: Library[] }> {
|
||||
const { query } = options;
|
||||
const allItems: Library[] = [
|
||||
<Library>{
|
||||
name: 'Keyboard',
|
||||
availableVersions: ['1.0.0', '1.0.1', '1.02'],
|
||||
author: 'Arduino',
|
||||
summary: 'Allows an Arduino/Genuino board with USB capabilities to act as a Keyboard',
|
||||
description: 'This library plugs on the HID library. It can be used with or without other HIG-based libraries (Mouse, Gamepad etc)',
|
||||
installedVersion: '1.0.1',
|
||||
moreInfoLink: 'https://www.arduino.cc/reference/en/language/functions/usb/keyboard/',
|
||||
builtIn: true
|
||||
},
|
||||
<Library>{
|
||||
name: 'Mouse',
|
||||
availableVersions: ['1.0.0', '1.0.1'],
|
||||
author: 'Arduino',
|
||||
summary: 'Allows an Arduino board with USB capabilities to act as a Mouse. For Leonardo/Micro only',
|
||||
description: 'This library plugs on the HID library. Can be used with ot without other HID-based libraries (Keyboard, Gamepad etc)',
|
||||
installedVersion: '1.0.1',
|
||||
moreInfoLink: 'https://www.arduino.cc/reference/en/language/functions/usb/mouse/',
|
||||
builtIn: true
|
||||
},
|
||||
<Library>{
|
||||
name: 'USBHost',
|
||||
availableVersions: ['1.0.0', '1.0.1', '1.02', '1.0.3', '1.0.3', '1.0.4', '1.0.5'],
|
||||
author: 'Arduino',
|
||||
summary: 'Allows communication with USB peripherals like mice, keyboard, and thumbdrives.',
|
||||
// tslint:disable-next-line:max-line-length
|
||||
description: 'This USBHost library allows an Arduino Due board to appear as a USB host, enabling it to communicate with peripherals like USB mice and keyboards. USBHost does not support devices that ace corrected through USB hubs. This includes some keyboards that have an internal hub.',
|
||||
moreInfoLink: 'https://www.arduino.cc/en/Reference/USBHost',
|
||||
installable: true
|
||||
}
|
||||
];
|
||||
return {
|
||||
items: allItems.filter(item => !query || item.name.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) !== -1)
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
16
arduino-ide-extension/src/node/util.ts
Normal file
16
arduino-ide-extension/src/node/util.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as grpc from "grpc";
|
||||
import * as jspb from "google-protobuf";
|
||||
|
||||
export type GrpcMethod<Req, Resp> = (request: Req, callback: (error: grpc.ServiceError | null, response: Resp) => void) => grpc.ClientUnaryCall
|
||||
|
||||
export function promisify<M extends GrpcMethod<Req, Resp>, Req, Resp extends jspb.Message>(m: M, req: Req): Promise<Resp> {
|
||||
return new Promise<Resp>((resolve, reject) => {
|
||||
m(req, (err, resp) => {
|
||||
if (!!err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(resp);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
30
arduino-ide-extension/tsconfig.json
Normal file
30
arduino-ide-extension/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"noImplicitAny": true,
|
||||
"noEmitOnError": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"target": "es5",
|
||||
"outDir": "lib",
|
||||
"lib": [
|
||||
"es6",
|
||||
"dom"
|
||||
],
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"files": [
|
||||
"../node_modules/@theia/monaco/src/typings/monaco/index.d.ts"
|
||||
]
|
||||
}
|
||||
38
arduino-ide-extension/tslint.json
Normal file
38
arduino-ide-extension/tslint.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"rules": {
|
||||
"class-name": true,
|
||||
"comment-format": [true, "check-space"],
|
||||
"curly": false,
|
||||
"forin": false,
|
||||
"indent": [true, "spaces"],
|
||||
"max-line-length": [true, 180],
|
||||
"no-trailing-whitespace": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-var-keyword": true,
|
||||
"one-line": [true,
|
||||
"check-open-brace",
|
||||
"check-catch",
|
||||
"check-else",
|
||||
"check-whitespace"
|
||||
],
|
||||
"radix": true,
|
||||
"trailing-comma": [false],
|
||||
"triple-equals": [true, "allow-null-check"],
|
||||
"typedef-whitespace": [true, {
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
}],
|
||||
"variable-name": false,
|
||||
"whitespace": [true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user