mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-13 06:16:33 +00:00
fixed save-as. added sketchload
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
parent
528f4150d3
commit
8ab70f48f8
@ -260,13 +260,11 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
}
|
||||
|
||||
protected async openSketchFiles(uri: string): Promise<void> {
|
||||
const uris = await this.sketchService.getSketchFiles(uri);
|
||||
for (const uri of uris) {
|
||||
const sketch = await this.sketchService.loadSketch(uri);
|
||||
await this.editorManager.open(new URI(sketch.mainFileUri));
|
||||
for (const uri of [...sketch.otherSketchFileUris, ...sketch.additionalFileUris]) {
|
||||
await this.editorManager.open(new URI(uri));
|
||||
}
|
||||
if (uris.length) {
|
||||
await this.editorManager.open(new URI(uris[0])); // Make sure the sketch file has the focus.
|
||||
}
|
||||
}
|
||||
|
||||
registerColors(colors: ColorRegistry): void {
|
||||
@ -313,6 +311,24 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
hc: 'activityBar.inactiveForeground'
|
||||
},
|
||||
description: 'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.'
|
||||
},
|
||||
{
|
||||
id: 'arduino.output.background',
|
||||
defaults: {
|
||||
dark: 'editorWidget.background',
|
||||
light: 'editorWidget.background',
|
||||
hc: 'editorWidget.background'
|
||||
},
|
||||
description: 'Background color of the Output view.'
|
||||
},
|
||||
{
|
||||
id: 'arduino.output.foreground',
|
||||
defaults: {
|
||||
dark: 'editorWidget.foreground',
|
||||
light: 'editorWidget.foreground',
|
||||
hc: 'editorWidget.foreground'
|
||||
},
|
||||
description: 'Color of the text in the Output view.'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import { ArduinoLanguageGrammarContribution } from './language/arduino-language-
|
||||
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
|
||||
import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service';
|
||||
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
|
||||
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
|
||||
import { CoreService, CoreServicePath, CoreServiceClient } from '../common/protocol/core-service';
|
||||
import { BoardsListWidget } from './boards/boards-list-widget';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
|
||||
@ -151,6 +152,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
|
||||
// Sketch list service
|
||||
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
|
||||
bind(SketchesServiceClientImpl).toSelf().inSingletonScope();
|
||||
|
||||
// Config service
|
||||
bind(ConfigService).toDynamicValue(context => {
|
||||
|
@ -15,12 +15,12 @@ export class CloseSketch extends SketchContribution {
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(CloseSketch.Commands.CLOSE_SKETCH, {
|
||||
execute: async () => {
|
||||
const sketch = await this.currentSketch();
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!sketch) {
|
||||
return;
|
||||
}
|
||||
const isTemp = await this.sketchService.isTemp(sketch);
|
||||
const uri = await this.currentSketchFile();
|
||||
const uri = await this.sketchServiceClient.currentSketchFile();
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { inject, injectable, interfaces } from 'inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { notEmpty } from '@theia/core/lib/common/objects';
|
||||
import { FileSystem } from '@theia/filesystem/lib/common';
|
||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
@ -11,8 +10,9 @@ import { MenuModelRegistry, MenuContribution } from '@theia/core/lib/common/menu
|
||||
import { KeybindingRegistry, KeybindingContribution } from '@theia/core/lib/browser/keybinding';
|
||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { Command, CommandRegistry, CommandContribution, CommandService } from '@theia/core/lib/common/command';
|
||||
import { SketchesService, ConfigService, FileSystemExt, Sketch } from '../../common/protocol';
|
||||
import { EditorMode } from '../editor-mode';
|
||||
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { SketchesService, ConfigService, FileSystemExt, Sketch } from '../../common/protocol';
|
||||
|
||||
export { Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, URI, Sketch, open };
|
||||
|
||||
@ -69,30 +69,8 @@ export abstract class SketchContribution extends Contribution {
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
|
||||
protected async currentSketch(): Promise<Sketch | undefined> {
|
||||
const sketches = (await Promise.all(this.workspaceService.tryGetRoots().map(({ uri }) => this.sketchService.getSketchFolder(uri)))).filter(notEmpty);
|
||||
if (!sketches.length) {
|
||||
return;
|
||||
}
|
||||
if (sketches.length > 1) {
|
||||
console.log(`Multiple sketch folders were found in the workspace. Falling back to the first one. Sketch folders: ${JSON.stringify(sketches)}`);
|
||||
}
|
||||
return sketches[0];
|
||||
}
|
||||
|
||||
protected async currentSketchFile(): Promise<string | undefined> {
|
||||
const sketch = await this.currentSketch();
|
||||
if (sketch) {
|
||||
const uri = new URI(sketch.uri).resolve(`${sketch.name}.ino`).toString();
|
||||
const exists = await this.fileSystem.exists(uri);
|
||||
if (!exists) {
|
||||
this.messageService.warn(`Could not find sketch file: ${uri}`);
|
||||
return undefined;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
}
|
||||
|
||||
|
@ -151,33 +151,29 @@ ${value}
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.COPY_FOR_FORUM.id,
|
||||
keybinding: 'CtrlCmd+Shift+C'
|
||||
keybinding: 'CtrlCmd+Shift+C',
|
||||
when: 'editorFocus'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.COPY_FOR_GITHUB.id,
|
||||
keybinding: 'CtrlCmd+Alt+C'
|
||||
keybinding: 'CtrlCmd+Alt+C',
|
||||
when: 'editorFocus'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.GO_TO_LINE.id,
|
||||
keybinding: 'CtrlCmd+L'
|
||||
keybinding: 'CtrlCmd+L',
|
||||
when: 'editorFocus'
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.TOGGLE_COMMENT.id,
|
||||
keybinding: 'CtrlCmd+/'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.INDENT_LINES.id,
|
||||
keybinding: 'Tab'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.OUTDENT_LINES.id,
|
||||
keybinding: 'Shift+Tab'
|
||||
keybinding: 'CtrlCmd+/',
|
||||
when: 'editorFocus'
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+=' // TODO: compare with the Java IDE. It uses `⌘+`. There is no `+` on EN_US.
|
||||
keybinding: 'CtrlCmd+='
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
||||
@ -213,8 +209,15 @@ ${value}
|
||||
}
|
||||
|
||||
protected async currentValue(): Promise<string | undefined> {
|
||||
const currentEditor = await this.current()
|
||||
return currentEditor?.getValue();
|
||||
const currentEditor = await this.current();
|
||||
if (currentEditor) {
|
||||
const selection = currentEditor.getSelection();
|
||||
if (!selection || selection.isEmpty()) {
|
||||
return currentEditor.getValue();
|
||||
}
|
||||
return currentEditor.getModel()?.getValueInRange(selection);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected async run(commandId: string): Promise<any> {
|
||||
|
@ -28,7 +28,7 @@ export class OpenSketchExternal extends SketchContribution {
|
||||
}
|
||||
|
||||
protected async openExternal(): Promise<void> {
|
||||
const uri = await this.currentSketchFile();
|
||||
const uri = await this.sketchServiceClient.currentSketchFile();
|
||||
if (uri) {
|
||||
const exists = this.fileSystem.exists(uri);
|
||||
if (exists) {
|
||||
|
@ -32,7 +32,7 @@ export class SaveAsSketch extends SketchContribution {
|
||||
* Resolves `true` if the sketch was successfully saved as something.
|
||||
*/
|
||||
async saveAs({ execOnlyIfTemp, openAfterMove }: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT): Promise<boolean> {
|
||||
const sketch = await this.currentSketch();
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!sketch) {
|
||||
return false;
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export class SketchControl extends SketchContribution {
|
||||
isVisible: widget => this.shell.getWidgets('main').indexOf(widget) !== -1,
|
||||
execute: async () => {
|
||||
this.toDisposeBeforeCreateNewContextMenu.dispose();
|
||||
const sketch = await this.currentSketch();
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!sketch) {
|
||||
return;
|
||||
}
|
||||
@ -40,8 +40,8 @@ export class SketchControl extends SketchContribution {
|
||||
return;
|
||||
}
|
||||
|
||||
const uris = await this.sketchService.getSketchFiles(sketch.uri);
|
||||
// TODO: order them! The Java IDE orders them by tab index. Use the shell and the editor manager to achieve it.
|
||||
const { mainFileUri, otherSketchFileUris, additionalFileUris } = await this.sketchService.loadSketch(sketch.uri);
|
||||
const uris = [mainFileUri, ...otherSketchFileUris, ...additionalFileUris];
|
||||
for (let i = 0; i < uris.length; i++) {
|
||||
const uri = new URI(uris[i]);
|
||||
const command = { id: `arduino-focus-file--${uri.toString()}` };
|
||||
|
@ -73,7 +73,7 @@ export class UploadSketch extends SketchContribution {
|
||||
}
|
||||
|
||||
async uploadSketch(usingProgrammer: boolean = false): Promise<void> {
|
||||
const uri = await this.currentSketchFile();
|
||||
const uri = await this.sketchServiceClient.currentSketchFile();
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ export class VerifySketch extends SketchContribution {
|
||||
}
|
||||
|
||||
async verifySketch(): Promise<void> {
|
||||
const uri = await this.currentSketchFile();
|
||||
const uri = await this.sketchServiceClient.currentSketchFile();
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
|
@ -108,7 +108,9 @@
|
||||
"secondaryButton.hoverBackground": "#dae3e3",
|
||||
"arduino.branding.primary": "#00979d",
|
||||
"arduino.branding.secondary": "#b5c8c9",
|
||||
"arduino.foreground": "#edf1f1"
|
||||
"arduino.foreground": "#edf1f1",
|
||||
"arduino.output.background": "#000000",
|
||||
"arduino.output.foreground": "#ffffff"
|
||||
},
|
||||
"type": "light",
|
||||
"name": "Arduino"
|
||||
|
@ -155,3 +155,12 @@
|
||||
border: 1px solid var(--theia-arduino-toolbar-background);
|
||||
padding: 2px 0px 2px 9px;
|
||||
}
|
||||
|
||||
#outputView .monaco-editor .lines-content.monaco-editor-background {
|
||||
background-color: var(--theia-arduino-output-background);
|
||||
}
|
||||
|
||||
.monaco-editor {
|
||||
/* #outputView .monaco-editor .inputarea.ime-input { */
|
||||
color: var(--theia-arduino-output-foreground);
|
||||
}
|
||||
|
@ -17,7 +17,9 @@ export class EditorManager extends TheiaEditorManager {
|
||||
const { editor } = widget;
|
||||
if (editor instanceof MonacoEditor) {
|
||||
const codeEditor = editor.getControl();
|
||||
codeEditor.updateOptions({ readOnly });
|
||||
const lineNumbersMinChars = 2;
|
||||
const overviewRulerLanes = 0;
|
||||
codeEditor.updateOptions({ readOnly, lineNumbersMinChars, overviewRulerLanes });
|
||||
}
|
||||
}
|
||||
return widget;
|
||||
|
@ -7,7 +7,6 @@ import { ApplicationServer } from '@theia/core/lib/common/application-protocol';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { FocusTracker, Widget } from '@theia/core/lib/browser';
|
||||
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { EditorMode } from '../../editor-mode';
|
||||
import { ConfigService } from '../../../common/protocol/config-service';
|
||||
import { SketchesService } from '../../../common/protocol/sketches-service';
|
||||
import { ArduinoWorkspaceRootResolver } from '../../arduino-workspace-resolver';
|
||||
@ -24,9 +23,6 @@ export class WorkspaceService extends TheiaWorkspaceService {
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
@ -82,13 +78,7 @@ export class WorkspaceService extends TheiaWorkspaceService {
|
||||
if (!exists) {
|
||||
return false;
|
||||
}
|
||||
// The workspace root location must exist. However, when opening a workspace root in pro-mode,
|
||||
// the workspace root must not be a sketch folder. It can be the default sketch directory, or any other directories, for instance.
|
||||
if (this.editorMode.proMode) {
|
||||
return true;
|
||||
}
|
||||
const sketchFolder = await this.sketchService.isSketchFolder(uri);
|
||||
return sketchFolder;
|
||||
return this.sketchService.isSketchFolder(uri);
|
||||
}
|
||||
|
||||
protected onCurrentWidgetChange({ newValue }: FocusTracker.IChangedArgs<Widget>): void {
|
||||
|
@ -12,7 +12,7 @@ export class ToolOutputServiceClientImpl implements ToolOutputServiceClient {
|
||||
onNewOutput(tool: string, text: string): void {
|
||||
const name = `Arduino: ${tool}`;
|
||||
// Zen-mode: we do not reveal the output for daemon messages.
|
||||
const show = tool === 'daemon' ? Promise.resolve() : this.commandService.executeCommand(OutputCommands.SHOW.id, { name, options: { preserveFocus: false } });
|
||||
const show = tool === 'daemon22' ? Promise.resolve() : this.commandService.executeCommand(OutputCommands.SHOW.id, { name, options: { preserveFocus: false } });
|
||||
show.then(() => this.commandService.executeCommand(OutputCommands.APPEND.id, { name, text }));
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,49 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { notEmpty } from '@theia/core/lib/common/objects';
|
||||
import { FileSystem } from '@theia/filesystem/lib/common';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { Sketch, SketchesService } from '../../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class SketchesServiceClientImpl {
|
||||
|
||||
@inject(FileSystem)
|
||||
protected readonly fileSystem: FileSystem;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
@inject(SketchesService)
|
||||
protected readonly sketchService: SketchesService;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
async currentSketch(): Promise<Sketch | undefined> {
|
||||
const sketches = (await Promise.all(this.workspaceService.tryGetRoots().map(({ uri }) => this.sketchService.getSketchFolder(uri)))).filter(notEmpty);
|
||||
if (!sketches.length) {
|
||||
return undefined;
|
||||
}
|
||||
if (sketches.length > 1) {
|
||||
console.log(`Multiple sketch folders were found in the workspace. Falling back to the first one. Sketch folders: ${JSON.stringify(sketches)}`);
|
||||
}
|
||||
return sketches[0];
|
||||
}
|
||||
|
||||
async currentSketchFile(): Promise<string | undefined> {
|
||||
const sketch = await this.currentSketch();
|
||||
if (sketch) {
|
||||
const uri = new URI(sketch.uri).resolve(`${sketch.name}.ino`).toString();
|
||||
const exists = await this.fileSystem.exists(uri);
|
||||
if (!exists) {
|
||||
this.messageService.warn(`Could not find sketch file: ${uri}`);
|
||||
return undefined;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
@ -8,7 +8,12 @@ export interface SketchesService {
|
||||
*/
|
||||
getSketches(uri?: string): Promise<Sketch[]>;
|
||||
|
||||
getSketchFiles(uri: string): Promise<string[]>;
|
||||
/**
|
||||
* This is the TS implementation of `SketchLoad` from the CLI and should be replaced with a gRPC call eventually.
|
||||
* See: https://github.com/arduino/arduino-cli/issues/837
|
||||
* Based on: https://github.com/arduino/arduino-cli/blob/eef3705c4afcba4317ec38b803d9ffce5dd59a28/arduino/builder/sketch.go#L100-L215
|
||||
*/
|
||||
loadSketch(uri: string): Promise<Sketch>;
|
||||
|
||||
/**
|
||||
* Creates a new sketch folder in the temp location.
|
||||
@ -40,7 +45,10 @@ export interface SketchesService {
|
||||
|
||||
export interface Sketch {
|
||||
readonly name: string;
|
||||
readonly uri: string;
|
||||
readonly uri: string; // `LocationPath`
|
||||
readonly mainFileUri: string; // `MainFile`
|
||||
readonly otherSketchFileUris: string[]; // `OtherSketchFiles`
|
||||
readonly additionalFileUris: string[]; // `AdditionalFiles`
|
||||
}
|
||||
export namespace Sketch {
|
||||
export function is(arg: any): arg is Sketch {
|
||||
|
@ -1,12 +1,16 @@
|
||||
import * as fs from 'fs';
|
||||
import { promisify } from 'util';
|
||||
|
||||
export const constants = fs.constants;
|
||||
|
||||
export const existsSync = fs.existsSync;
|
||||
export const lstatSync = fs.lstatSync;
|
||||
export const readdirSync = fs.readdirSync;
|
||||
export const statSync = fs.statSync;
|
||||
export const writeFileSync = fs.writeFileSync;
|
||||
export const readFileSync = fs.readFileSync;
|
||||
export const accessSync = fs.accessSync;
|
||||
export const renameSync = fs.renameSync;
|
||||
|
||||
export const exists = promisify(fs.exists);
|
||||
export const lstat = promisify(fs.lstat);
|
||||
@ -14,6 +18,8 @@ export const readdir = promisify(fs.readdir);
|
||||
export const stat = promisify(fs.stat);
|
||||
export const writeFile = promisify(fs.writeFile);
|
||||
export const readFile = promisify(fs.readFile);
|
||||
export const access = promisify(fs.access);
|
||||
export const rename = promisify(fs.rename);
|
||||
|
||||
export const watchFile = fs.watchFile;
|
||||
export const unwatchFile = fs.unwatchFile;
|
||||
|
@ -2,14 +2,24 @@ import { injectable, inject } from 'inversify';
|
||||
import * as os from 'os';
|
||||
import * as temp from 'temp';
|
||||
import * as path from 'path';
|
||||
import * as fs from './fs-extra';
|
||||
import { ncp } from 'ncp';
|
||||
import { Stats } from 'fs';
|
||||
import * as fs from './fs-extra';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { FileUri, BackendApplicationContribution } from '@theia/core/lib/node';
|
||||
import { ConfigService } from '../common/protocol/config-service';
|
||||
import { SketchesService, Sketch } from '../common/protocol/sketches-service';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
|
||||
export const ALLOWED_FILE_EXTENSIONS = ['.c', '.cpp', '.h', '.hh', '.hpp', '.s', '.pde', '.ino'];
|
||||
|
||||
// As currently implemented on Linux,
|
||||
// the maximum number of symbolic links that will be followed while resolving a pathname is 40
|
||||
const MAX_FILESYSTEM_DEPTH = 40;
|
||||
|
||||
export namespace Extensions {
|
||||
export const MAIN = ['.ino', '.pde'];
|
||||
export const SOURCE = ['.c', '.cpp', '.s'];
|
||||
export const ADDITIONAL = ['.h', '.c', '.hpp', '.hh', '.cpp', '.s'];
|
||||
}
|
||||
|
||||
// TODO: `fs`: use async API
|
||||
@injectable()
|
||||
@ -43,46 +53,208 @@ export class SketchesServiceImpl implements SketchesService, BackendApplicationC
|
||||
for (const fileName of fileNames) {
|
||||
const filePath = path.join(fsPath, fileName);
|
||||
if (await this.isSketchFolder(FileUri.create(filePath).toString())) {
|
||||
const stat = await fs.stat(filePath);
|
||||
sketches.push({
|
||||
mtimeMs: stat.mtimeMs,
|
||||
name: fileName,
|
||||
uri: FileUri.create(filePath).toString()
|
||||
});
|
||||
try {
|
||||
const stat = await fs.stat(filePath);
|
||||
const sketch = await this.loadSketch(FileUri.create(filePath).toString());
|
||||
sketches.push({
|
||||
...sketch,
|
||||
mtimeMs: stat.mtimeMs
|
||||
});
|
||||
} catch {
|
||||
console.warn(`Could not load sketch from ${filePath}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sketches.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all allowed files.
|
||||
* File extensions: 'c', 'cpp', 'h', 'hh', 'hpp', 's', 'pde', 'ino'
|
||||
* This is the TS implementation of `SketchLoad` from the CLI.
|
||||
* See: https://github.com/arduino/arduino-cli/issues/837
|
||||
* Based on: https://github.com/arduino/arduino-cli/blob/eef3705c4afcba4317ec38b803d9ffce5dd59a28/arduino/builder/sketch.go#L100-L215
|
||||
*/
|
||||
async getSketchFiles(uri: string): Promise<string[]> {
|
||||
const uris: string[] = [];
|
||||
const fsPath = FileUri.fsPath(uri);
|
||||
if (fs.lstatSync(fsPath).isDirectory()) {
|
||||
if (await this.isSketchFolder(uri)) {
|
||||
const basename = path.basename(fsPath)
|
||||
const fileNames = await fs.readdir(fsPath);
|
||||
for (const fileName of fileNames) {
|
||||
const filePath = path.join(fsPath, fileName);
|
||||
if (ALLOWED_FILE_EXTENSIONS.indexOf(path.extname(filePath)) !== -1
|
||||
&& fs.existsSync(filePath)
|
||||
&& fs.lstatSync(filePath).isFile()) {
|
||||
const uri = FileUri.create(filePath).toString();
|
||||
if (fileName === basename + '.ino') {
|
||||
uris.unshift(uri); // The sketch file is the first.
|
||||
} else {
|
||||
uris.push(uri);
|
||||
}
|
||||
async loadSketch(uri: string): Promise<Sketch> {
|
||||
const sketchPath = FileUri.fsPath(uri);
|
||||
const exists = await fs.exists(sketchPath);
|
||||
if (!exists) {
|
||||
throw new Error(`${uri} does not exist.`);
|
||||
}
|
||||
const stat = await fs.lstat(sketchPath);
|
||||
let sketchFolder: string | undefined;
|
||||
let mainSketchFile: string | undefined;
|
||||
|
||||
// If a sketch folder was passed, save the parent and point sketchPath to the main sketch file
|
||||
if (stat.isDirectory()) {
|
||||
sketchFolder = sketchPath;
|
||||
// Allowed extensions are .ino and .pde (but not both)
|
||||
for (const extension of Extensions.MAIN) {
|
||||
const candidateSketchFile = path.join(sketchPath, `${path.basename(sketchPath)}${extension}`);
|
||||
const candidateExists = await fs.exists(candidateSketchFile);
|
||||
if (candidateExists) {
|
||||
if (!mainSketchFile) {
|
||||
mainSketchFile = candidateSketchFile;
|
||||
} else {
|
||||
throw new Error(`Multiple main sketch files found (${path.basename(mainSketchFile)}, ${path.basename(candidateSketchFile)})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return uris;
|
||||
|
||||
// Check main file was found.
|
||||
if (!mainSketchFile) {
|
||||
throw new Error(`Unable to find a sketch file in directory ${sketchFolder}`);
|
||||
}
|
||||
|
||||
// Check main file is readable.
|
||||
try {
|
||||
await fs.access(mainSketchFile, fs.constants.R_OK);
|
||||
} catch {
|
||||
throw new Error('Unable to open the main sketch file.');
|
||||
}
|
||||
|
||||
const mainSketchFileStat = await fs.lstat(mainSketchFile);
|
||||
if (mainSketchFileStat.isDirectory()) {
|
||||
throw new Error(`Sketch must not be a directory.`);
|
||||
}
|
||||
} else {
|
||||
sketchFolder = path.dirname(sketchPath);
|
||||
mainSketchFile = sketchPath;
|
||||
}
|
||||
|
||||
const files: string[] = [];
|
||||
let rootVisited = false;
|
||||
const err = await this.simpleLocalWalk(sketchFolder, MAX_FILESYSTEM_DEPTH, async (fsPath: string, info: Stats, error: Error | undefined) => {
|
||||
if (error) {
|
||||
console.log(`Error during sketch processing: ${error}`);
|
||||
return error;
|
||||
}
|
||||
const name = path.basename(fsPath);
|
||||
if (info.isDirectory()) {
|
||||
if (rootVisited) {
|
||||
if (name.startsWith('.') || name === 'CVS' || name === 'RCS') {
|
||||
return new SkipDir();
|
||||
}
|
||||
} else {
|
||||
rootVisited = true
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (name.startsWith('.')) {
|
||||
return undefined;
|
||||
}
|
||||
const ext = path.extname(fsPath);
|
||||
const isMain = Extensions.MAIN.indexOf(ext) !== -1;
|
||||
const isAdditional = Extensions.ADDITIONAL.indexOf(ext) !== -1;
|
||||
if (!isMain && !isAdditional) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(fsPath, fs.constants.R_OK);
|
||||
files.push(fsPath);
|
||||
} catch { }
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (err) {
|
||||
console.error(`There was an error while collecting the sketch files: ${sketchPath}`)
|
||||
throw err;
|
||||
}
|
||||
|
||||
return this.newSketch(sketchFolder, mainSketchFile, files);
|
||||
|
||||
}
|
||||
|
||||
private newSketch(sketchFolderPath: string, mainFilePath: string, allFilesPaths: string[]): Sketch {
|
||||
let mainFile: string | undefined;
|
||||
const paths = new Set<string>();
|
||||
for (const p of allFilesPaths) {
|
||||
if (p === mainFilePath) {
|
||||
mainFile = p;
|
||||
} else {
|
||||
paths.add(p);
|
||||
}
|
||||
}
|
||||
if (!mainFile) {
|
||||
throw new Error('Could not locate main sketch file.');
|
||||
}
|
||||
const additionalFiles: string[] = [];
|
||||
const otherSketchFiles: string[] = [];
|
||||
for (const p of Array.from(paths)) {
|
||||
const ext = path.extname(p);
|
||||
if (Extensions.MAIN.indexOf(ext) !== -1) {
|
||||
if (path.dirname(p) === sketchFolderPath) {
|
||||
otherSketchFiles.push(p);
|
||||
}
|
||||
} else if (Extensions.ADDITIONAL.indexOf(ext) !== -1) {
|
||||
// XXX: this is a caveat with the CLI, we do not know the `buildPath`.
|
||||
// https://github.com/arduino/arduino-cli/blob/0483882b4f370c288d5318913657bbaa0325f534/arduino/sketch/sketch.go#L108-L110
|
||||
additionalFiles.push(p);
|
||||
} else {
|
||||
throw new Error(`Unknown sketch file extension '${ext}'.`);
|
||||
}
|
||||
}
|
||||
additionalFiles.sort();
|
||||
otherSketchFiles.sort();
|
||||
|
||||
return {
|
||||
uri: FileUri.create(sketchFolderPath).toString(),
|
||||
mainFileUri: FileUri.create(mainFile).toString(),
|
||||
name: path.basename(sketchFolderPath),
|
||||
additionalFileUris: additionalFiles.map(p => FileUri.create(p).toString()),
|
||||
otherSketchFileUris: otherSketchFiles.map(p => FileUri.create(p).toString())
|
||||
}
|
||||
}
|
||||
|
||||
protected async simpleLocalWalk(
|
||||
root: string,
|
||||
maxDepth: number,
|
||||
walk: (fsPath: string, info: Stats | undefined, err: Error | undefined) => Promise<Error | undefined>): Promise<Error | undefined> {
|
||||
|
||||
let { info, err } = await this.lstat(root);
|
||||
if (err) {
|
||||
return walk(root, undefined, err);
|
||||
}
|
||||
if (!info) {
|
||||
return new Error(`Could not stat file: ${root}.`);
|
||||
}
|
||||
err = await walk(root, info, err);
|
||||
if (err instanceof SkipDir) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (info.isDirectory()) {
|
||||
if (maxDepth <= 0) {
|
||||
return walk(root, info, new Error(`Filesystem bottom is too deep (directory recursion or filesystem really deep): ${root}`));
|
||||
}
|
||||
maxDepth--;
|
||||
const files: string[] = [];
|
||||
try {
|
||||
files.push(...await fs.readdir(root));
|
||||
} catch { }
|
||||
for (const file of files) {
|
||||
err = await this.simpleLocalWalk(path.join(root, file), maxDepth, walk);
|
||||
if (err instanceof SkipDir) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async lstat(fsPath: string): Promise<{ info: Stats, err: undefined } | { info: undefined, err: Error }> {
|
||||
const exists = await fs.exists(fsPath);
|
||||
if (!exists) {
|
||||
return { info: undefined, err: new Error(`${fsPath} does not exist`) };
|
||||
}
|
||||
try {
|
||||
const info = await fs.lstat(fsPath);
|
||||
return { info, err: undefined };
|
||||
} catch (err) {
|
||||
return { info: undefined, err };
|
||||
}
|
||||
const sketchDir = path.dirname(fsPath);
|
||||
return this.getSketchFiles(FileUri.create(sketchDir).toString());
|
||||
}
|
||||
|
||||
async createNewSketch(): Promise<Sketch> {
|
||||
@ -129,10 +301,7 @@ void loop() {
|
||||
|
||||
}
|
||||
`, { encoding: 'utf8' });
|
||||
return {
|
||||
name: sketchName,
|
||||
uri: FileUri.create(sketchDir).toString()
|
||||
}
|
||||
return this.loadSketch(FileUri.create(sketchDir).toString());
|
||||
}
|
||||
|
||||
async getSketchFolder(uri: string): Promise<Sketch | undefined> {
|
||||
@ -142,10 +311,7 @@ void loop() {
|
||||
let currentUri = new URI(uri);
|
||||
while (currentUri && !currentUri.path.isRoot) {
|
||||
if (await this.isSketchFolder(currentUri.toString())) {
|
||||
return {
|
||||
name: currentUri.path.base,
|
||||
uri: currentUri.toString()
|
||||
};
|
||||
return this.loadSketch(currentUri.toString());
|
||||
}
|
||||
currentUri = currentUri.parent;
|
||||
}
|
||||
@ -173,20 +339,35 @@ void loop() {
|
||||
|
||||
async copy(sketch: Sketch, { destinationUri }: { destinationUri: string }): Promise<string> {
|
||||
const source = FileUri.fsPath(sketch.uri);
|
||||
if (await !fs.exists(source)) {
|
||||
const exists = await fs.exists(source);
|
||||
if (!exists) {
|
||||
throw new Error(`Sketch does not exist: ${sketch}`);
|
||||
}
|
||||
const destination = FileUri.fsPath(destinationUri);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
ncp.ncp(source, destination, error => {
|
||||
ncp.ncp(source, destination, async error => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
const newName = path.basename(destination);
|
||||
try {
|
||||
await fs.rename(path.join(destination, new URI(sketch.mainFileUri).path.base), path.join(destination, `${newName}.ino`));
|
||||
await this.loadSketch(destinationUri); // Sanity check.
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
return FileUri.create(destination).toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SkipDir extends Error {
|
||||
constructor() {
|
||||
super('skip this directory');
|
||||
Object.setPrototypeOf(this, SkipDir.prototype);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user