diff --git a/BRANCHNOTES.md b/BRANCHNOTES.md new file mode 100644 index 00000000..1beab149 --- /dev/null +++ b/BRANCHNOTES.md @@ -0,0 +1,456 @@ +# IntelliSense Autoconfiguration Branch + +**TL;DR:** Alpha versions of this extension of this branch with the latest features and fixes are available from the following [Dropbox folder](https://www.dropbox.com/sh/whmcdt26chyjgby/AAB1Ld2fzZ9Z_NfM3CRay17wa). + +## Problem +This branch more or less addresses the following issues: +| # | Issue | Title | Comment | +|--:|:------|:------------|:--------| +| 1| [#438](https://github.com/microsoft/vscode-arduino/issues/438) | **The Extension should automagically fill the `c_cpp_properties.json`, so intellisense works out of the box** | This is the issue to which I usually report news concerning the progress of this project | +| 2| [#969](https://github.com/microsoft/vscode-arduino/issues/969) | **INO Files defines undefined but can jump to definition** | | +| 3| [#959](https://github.com/microsoft/vscode-arduino/issues/959) | **Update board type command does not update intellisense config automatically** | | +| 4| [#892](https://github.com/microsoft/vscode-arduino/issues/892) | **Default IntelliSense config** | | +| 5| [#876](https://github.com/microsoft/vscode-arduino/issues/876) | **Missing #define ARDUINO 10808 logic?** | Marked as bug but it's just the same problem again | +| 6| [#850](https://github.com/microsoft/vscode-arduino/issues/850) | **How to prevent modifications of `c_cpp_properties.json` by the extension?** | Asks if the current implementation can be turned off, because it overwrites a user's config with non working IS config -- this is sad. | +| 7| [#833](https://github.com/microsoft/vscode-arduino/issues/833) | **Allow C_Cpp.intelliSenseEngine to be set to "Default" instead of "Tag Parser" for better code completion/suggestions** | | +| 8| [#831](https://github.com/microsoft/vscode-arduino/issues/831) | **IntelliSenseEngine doesn't work as default** | | +| 9| [#829](https://github.com/microsoft/vscode-arduino/issues/829) | **`Arduino.h` and ESP8266 includes have squiggles** | Stale issue | +| 10| [#823](https://github.com/microsoft/vscode-arduino/issues/823) | **Intellisense is not highlighting code** | Stale issue | +| 11| [#818](https://github.com/microsoft/vscode-arduino/issues/818) | **Warning with default includePath after initialize, cannot open source file `avr/pgmspace.h`** | Stale issue | +| 12| [#808](https://github.com/microsoft/vscode-arduino/issues/808) | **Identifier `Serial` is undefined** | | +| 13| [#776](https://github.com/microsoft/vscode-arduino/issues/776) | **Can not open source file `omp.h` (dependency of `ESP8266WiFi.h`** | | +| 14| [#772](https://github.com/microsoft/vscode-arduino/issues/772) | **How to fix red squiggles under constants like D2 (upload works fine)** | | +| 15| [#761](https://github.com/microsoft/vscode-arduino/issues/761) | **When creating the `c_cpp_properties.json` it should include the libraries folder as well as all of the other folders.** | | +| 16| [#749](https://github.com/microsoft/vscode-arduino/issues/749) | **Non-fatal error on Adafruit Feather M0: cannot open source file `sam.h`** | Stale issue | +| 17| [#727](https://github.com/microsoft/vscode-arduino/issues/727) | **Intellisense for Arduino Tabs** | Stale issue | +| 18| [#684](https://github.com/microsoft/vscode-arduino/issues/684) | **Default C/C++ configuration uses MSVC for IntelliSense rather than GCC** | Stale issue | +| 19| [#678](https://github.com/microsoft/vscode-arduino/issues/678) | **Dependency error `avr32/io.h`** | Stale issue | +| 20| [#645](https://github.com/microsoft/vscode-arduino/issues/645) | **Should IntelliSense suggest only built in function?** | Stale issue | +| 21| [#613](https://github.com/microsoft/vscode-arduino/issues/613) | **Read content of `keywords.txt` for text highlights** | This will become obsolete as well since `keywords.txt` is a dirty workaround for the arduino IDE which doesn't have IntelliSense at all. | +| 22| [#563](https://github.com/microsoft/vscode-arduino/issues/563) | **Support of intellisence for library Wire(sam)** | | +| 23| [#525](https://github.com/microsoft/vscode-arduino/issues/525) | **`#include` errors detected - Tag Parser.** | Closed but still not fixed properly | +| 24| [#474](https://github.com/microsoft/vscode-arduino/issues/474) | **Enrich device develop experience** | | +| 25| [#127](https://github.com/microsoft/vscode-arduino/issues/127) | **Syntax highlighting for some of the Classes/Instances is missing** | Closed but still not fixed properly | +| 26| [#126](https://github.com/microsoft/vscode-arduino/issues/126) | **Syntax highlighting for some of the Macros is missing** | Closed but still not fixed properly | +| 27| [#125](https://github.com/microsoft/vscode-arduino/issues/125) | **Missing syntax highlighting for partial arduino constants** | Closed but still not fixed properly | +| 28| [#115](https://github.com/microsoft/vscode-arduino/issues/115) | **Dot prompting the methods doesn't work on Mac** | Closed but still not fixed properly | + + + + + + +-- the list is probably incomplete - I didn't search exhaustively and didn't consider closed issues as long as I didn't stumble upon one. New duplicates are popping up at a rate of about one per week. + +Some issue searches and other related issues +* [vscode-arduino issue search for IntelliSense](https://github.com/microsoft/vscode-arduino/issues?utf8=%E2%9C%93&q=intellisense+is%3Aopen) +* [vscode-arduino issue search for intellisense label](https://github.com/microsoft/vscode-arduino/issues?utf8=%E2%9C%93&q=label%3Aintellisense) +* [Wrongly attributed to vscode instead of vscode-arduino](https://github.com/Microsoft/vscode-cpptools/issues/1750) +* [Problems with IntelliSense itself](https://github.com/microsoft/vscode-cpptools/issues/1034) + +## Solution +Implement and add a a parser which parses the output from Arduino's build process and generate a very precise `c_cpp_properties.json` which in turn hopefully renders any user interaction with this file obsolete. + +This mechanism should try it's best to detect situations in which the setup changes (board changed, sketch changed and so forth) and re-generate this configuration. For all other situations I'll provide a command to manually re-generate it. + +## Branch Goals +### Build Output Parser +The parser which identifies the main compilation command from Arduino's build output. It then parses the relevant includes, defines, compiler paths and flags from it. + +### `c_cpp_properties.json` Generator +The generator takes the parser's output and transforms it into a valid `c_cpp_properties.json` file. It merges the generated configuration with the existing (e.g. user-) configurations and writes it back to the configuration file. + +### Configuration Flags +Provide a global configuration flag which allows the user to turn this feature off. A project- (sketch-) specific override will be provided which allows the user to turn it off or on - regardless of the global setting. +This is useful for the rare cases for which this magic should fail or the user has a very exotic setup. This branch tries to eliminate most of the latter cases though. Especially it will always write to the `Arduino` configuration. If the user sets up a custom configuration she/he must simply name it differently, e.g. `John's Custom Config`, and the generator won't touch it. + +### Global Tasks in vscode-arduino +* Integrate it into vscode-arduino's build mechanics +* Install event trigger generation/handling to run the analysis as soon as something changes +* Remove the current implementation +For more details see table below. + +## Branch Log +**2020 02 05** Currently I'm able to generate error free IntelliSense setups for AVR and ESP32 using the preliminary implementation. For ESP32 I just had to add the intrinsic compiler paths manually. A solution has to be found for these ... which there is, see [here](https://stackoverflow.com/a/6666338) +**2020 02 06** Got it fully working (with built-in include directories) for AVR, ESP32, ESP8266. Rewrote the backend to facilitate writing of further parser engines in the future. +**2020 02 07** Wrote compiler command parser npm package [cocopa](https://www.npmjs.com/package/cocopa) and began writing a test framework for it. Added a global configuration switch which allows the IntelliSense configuration generation to be turned off. +**2020 02 08** Integrated `cocopa` into vscode-arduino. Added project configuration flag which can override the global flag in both ways (forced off, forced on). Made code tslint compliant. Began some documentation in [README.md](README.md). vscode-arduino now tries to generate an IntelliSense configuration even if compilation (verify) should fail. vscode-arduino now tries to generate a IntelliSense configuration even if Arduino's verify failed (if the main sketch compilation was invoked before anything failed) +**2020 02 09** Moved vscode-arduino specific from cocopa over (to keep cocopa as generic as possible). More unit testing within cocopa. Some research regarding future serial monitor implementation. Implemented c_cpp_properties merging -> compiler analysis results are merged into existing configuration and will preserve configurations of different name than the vscode-studio default configuration name (currently "Arduino"). This opens up the possibility for users to write their own configurations without having to disable the autogeneration. Implemented "write on change" - `c_cpp_properties.json` will only be written if a new configuration has been detected. Now loads of tests have to be written for cocopa. +**2020 02 10-12** Worked primarily on cocopa and test cases, fixed some npm and build errors on vscode-arduino within my setup. +**2020 02 15** Merged `upload` `uploadUsingProgrammer` and `verify` into a single function since they shared mostly the same code +* Better readability +* Better maintainability +* Less code redundancy -> less code -> less bugs +* Keeps the calls to the Arduino build CLI at a single location + +During merging I found some bugs within those functions - mainly due to the above problem. The most notable were: +* The serial monitor state wasn't restored when something went wrong +* In one of the `upload` functions the original authors forgot to invoke the "pre build command" +* Error message formatting was fixed within `verify` only +* No consistent return values within `verify` (when it bailed out early it returned `void`) + +**2020 02 17** Disabled and marked all previous implementations of IntelliSense support for later removal using `IS-REMOVE`. Pulled changes from upstream and merged them into the intellisense feature branch. Began to work on event handling/generation: vscode-arduino should detect when sketch/board/configuration and so on has changed, then re-analyze the current setup and set the IntelliSense configuration accordingly. This works more or less but there's a lot to fix in the current implementation which kept me busy till late today (I need some sleep now). Cleanup and commits follow tomorrow. Approaching alpha version for curious testers. OSX and Linux comes first, Windows will follow later. +**2020 02 18** Finished basic event triggering. Rewrote `DeviceContext` for proper settings modification detection (trigger events only on actual change) and generation of setting specific events (e.g. board changed) instead of one global event (aka. "something in the settings changed"). +**2020 02 19** Implemented proper build scheduling for analysis build by writing an `AnalysisManager` class. This class collects multiple changes (e.g. board and configuration, which often are changed shortly after another) before running an analysis. In case another build or analysis is in progress it postpones newly filed analysis requests until the other build has completed. Updated and completed the documentation for the IntelliSense usage within [README](README.md). Alpha test builds of the extension containing the latest implemented features and fixes are now available from the following [Dropbox folder](https://www.dropbox.com/sh/whmcdt26chyjgby/AAB1Ld2fzZ9Z_NfM3CRay17wa). Please note, that Windows is currently not supported yet. Reviewed, documented/commented all changes and committed the automatic analysis integration changes. +**2020 02 20** Windows support - what a PITA. This OS is so foobar'ed... The only positive outcome from this experience: I found some substantial bugs in the parser which - of course (Murphy) - didn't affect me up to now. The parser should be much more resistant against strange paths and escapes now: Added proper command line lexer to cocopa and worked around several ridiculous Windows shortcomings (Microsoft owes me at least 50 crates of beer). The whole mess is not cleaned up and committed yet so please don't build from the repository and use the alpha release packages as outlined above. +**2020 02 21** Discovered problems and bugs in the current official release from Microsoft: Fixed event handling within board manager. Added validity checks when loading board configurations from arduino.json. Better error handling and code locality, for details see commit. +**2020 02 22** Worked on cocopa unit tests: restored broken tests and added platform test for built-in parser. Added path normalizing for include paths for both cocopa and vscode-arduino. Verified correct behaviour on Windows with latest release (alpha tester claimed it not to be working) +**2020 02 23** Several tests, fixes and improvements within cocopa. `Arduino.h` now added to forced includes. Fixed code which wasn't linted up to now. Added hint to end of build how to rebuild the IntelliSense configuration. Added missing version field to `c_cpp_properties.json`. +**2020 02 25** Implemented and tested support for `.cpp`-sketches. Done implementing the most relevant unit tests in cocopa. Two independent alpha testers confirm proper working of the previous alpha releases. Updated to the latest revision of cocopa. +**2020 02 26** Preparing for pull request: Removed all code marked `IS-REMOVE` and dead code which I was able to identify. Reviewed all TODOs and resolved those which can be fixed without having to bother the maintainers. Left the unresolved on grounds of documentation. +**2020 02 27** Integrated analysis into all builds. Fixed bug in board management (board configuration couldn't be set anymore). Added logging filter for non-verbose builds and uploads to remove spurious messages. Added post-build script support. +**2020 02 28** Moved some code to cocopa. Cocopa now tries to parse the compiler arguments to set the C++ standard according to the C++ standard set by the individual board package. Better error handling for pre-/post-build commands. Error reporting for exceptions during build cleanup. Full `cmd` and `bash` support within pre-/post-build commands. Environment variables for pre-/post-build commands. +**2020 02 29** Added support for Arduino preferences within `arduino.json`. Fixed bug which surfaced when no sketch was set. Fixed security issue reported by Mathieu. + +## Status +| | Tasks | +|-----:|:--------| +| **Build output parser** | :heavy_check_mark: Basic parser working | +| | :heavy_check_mark: Support for different boards (done for AVR, ESP32, ESP8266) -- The code has been designed such that it is easy to write/add new parser engines (for non gcc compilers for instance) | +| | :heavy_check_mark: Getting intrinsic gcc include paths | +| | :heavy_check_mark: Handling quoted arguments | +| | :heavy_check_mark: X-platform support | +| **`c_cpp_properties.json` generator** | :heavy_check_mark: Basic objects | +| | :heavy_check_mark: Basic setting of parsing result | +| | :heavy_check_mark: Basic file input | +| | :heavy_check_mark: Basic file output | +| | :heavy_check_mark: Merging of parsing result and existing file content | +| | :heavy_check_mark: Handling inexistent files and folders | +| | :heavy_check_mark: Write configuration on change only | +| | :heavy_check_mark: Parse C++ standard from compiler flags | +| **Configuration flags** | :heavy_check_mark: Provide global disable flag for IntelliSense auto-config | +| | :heavy_check_mark: Provide project specific override for the global flag - most users will likely use the default setup and disable auto-generation for very specific projects | +| **Unit tests** | :heavy_check_mark: Basic parser (known boards, match/no match)| +| | :heavy_check_mark: All unit tests in cocopa | +| | :heavy_check_mark: Test with cpp sketches | +| **General** | :heavy_check_mark: Review and remove previous attempts messing with `c_cpp_properties.json` or IntelliSense (documented in the [General Tasks](#General-Tasks) section) | +| | :heavy_check_mark: *Auto-run verify when* | +| |     :heavy_check_mark: a) setting a board | +| |     :heavy_check_mark: b) changing the board's configuration | +| |     :heavy_check_mark: c) selecting another sketch | +| |     :heavy_check_mark: d) ~~workbench initialized and no `c_cpp_properties.json` found~~ obsolete: when board and board configuration is loaded on start up the analysis is triggered anyways | +| |     :white_check_mark: e) Identify other occasions where this applies (usually when adding new libraries) -- any suggestions? | +| | :heavy_check_mark: Hint the user to run *Arduino: Rebuild IntelliSense Configuration* -> printing message after each build (verify, upload, ...) | +| | :heavy_check_mark: Better build management such that regular builds and analyze builds do not interfere (done, 2020-02-19) | +| | :heavy_check_mark: Analyze task queue which fits in the latter (done, 2020-02-19) | +| | :heavy_check_mark: Document configuration settings in [README.md](README.md) | +| | :heavy_check_mark: Document features in [README.md](README.md) | +| | :heavy_check_mark: Try to auto-generate even if verify (i.e. compilation) fails | +| | :heavy_check_mark: Extract compiler command parser from vscode-arduino and [publish](https://itnext.io/step-by-step-building-and-publishing-an-npm-typescript-package-44fe7164964c) it as a separate package which will allow reusage and easy testing without heavy vscode-arduino rucksack -- done, see [cocopa](https://www.npmjs.com/package/cocopa) | +| | :heavy_check_mark: Parser only works when arduino is set to `verbose`, since this is the only way we get the compiler invocation command - this has to be fixed (done, see next item) | +| | :heavy_check_mark: Implement a *Rebuild IntelliSense Configuration* command which runs verify verbosely internally and therefore allows us to find and parse the compiler command | +| | :heavy_check_mark: Implement proper event generation for `DeviceContext`. a) Events should be issued only when something actually changes, b) Events should be issued for each setting separately | +| | :heavy_check_mark: Finally: go through my code and look for TODOs | + +`*` not committed to branch yet +`>` most of the actual parsing and configuration generation is part of [cocopa](https://github.com/elektronikworkshop/cocopa/) ([here](https://www.npmjs.com/package/cocopa)'s the npm package) + +## Additional Stuff this Branch addresses +* Support for `postbuild` commands, see [#786](https://github.com/microsoft/vscode-arduino/issues/786) + * Full `cmd` and `bash` syntax support for pre-/post-build commands + * Environment variables for pre-/post-build commands +* Support for Arduino preferences to be set during build, see [#975](https://github.com/microsoft/vscode-arduino/issues/975#issuecomment-592931155) +* Fixed security issue [#966](https://github.com/microsoft/vscode-arduino/issues/966): Web-server was listening on machine's main interface (0.0.0.0) instead of `localhost`. + +## Motivation +I write a lot of code for Arduino, especially libraries. The Arduino IDE is not suited for more complex projects and I tried several alternatives: +* The old and dysfunctional Arduino CDT extension for eclipse somehow stalled (even if it was promising) +* Sloeber could be an option but the maintainer is disillusioned and the project is more or less dead. Furthermore Eclipse is pretty heavy and less accessible to beginners +* Platform IO IDE's license is very [restrictive](https://community.platformio.org/t/what-part-of-platformio-is-open-source-licenced/1447/2). + +Then remains vscode-arduino. It seems that it isn't completely dead - but almost. Most of the core functionality seems to work (I used it a few days now). But the biggest show stopper is the bad IntelliSense support -- which I'll address here now. + +## Beer Money :beers: -- Support +You can chip in some beer money to keep me motivated - this is *really* appreciated. + +[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=PVLCSRZHBJ28G&source=url) + + +I will list every supporter here, thanks! + +### Supporters +5$ -> 1 :beer: +1h coding -> 20$ -> 4 :beers: (very moderate wage though) +2020-02-04 EW: 32 :beers: (8h coding) +2020-02-05 EW: 40 :beers: (10h coding) +2020-02-06 EW: 36 :beers: (9h coding) +2020-02-07 EW: 48 :beers: (12h coding) +2020-02-08 EW: 52 :beers: (13h coding) +2020-02-09 EW: 40 :beers: (10h coding) +2020-02-10 EW: 32 :beers: (8h coding) +2020-02-11 EW: 16 :beers: (4h coding) +2020-02-12 EW: 32 :beers: (8h coding) +2020-02-15 T.D.: 4 :beers: (20$ - Thanks a lot!) +2020-02-15 EW: 28 :beers: (7h coding) +2020-02-17 EW: 52 :beers: (13h coding) +2020-02-18 EW: 36 :beers: (9h coding) +2020-02-19 EW: 48 :beers: (12h coding) +2020-02-20 EW: 56 :beers: (14h coding) +2020-02-21 EW: 48 :beers: (12h coding) +2020-02-22 EW: 44 :beers: (11h coding) +2020-02-23 EW: 20 :beers: (5h coding) +2020-02-25 EW: 12 :beers: (3h coding) +2020-02-26 EW: 16 :beers: (4h coding) +2020-02-27 EW: 48 :beers: (12h coding) +2020-02-28 EW: 52 :beers: (13h coding) +2020-02-29 EW: 28 :beers: (7h coding) + + + + +## Useful Links +* [`c_cpp_properties.json` reference](https://code.visualstudio.com/docs/cpp/c-cpp-properties-schema-reference) +* [Interactive regex debugger](https://regex101.com/) +* [Git branch management](https://blog.scottlowe.org/2015/01/27/using-fork-branch-git-workflow/) +* [Collapsible Markdown](https://gist.githubusercontent.com/joyrexus/16041f2426450e73f5df9391f7f7ae5f/raw/f774f242feff6bae4a5be7d6c71aa5df2e3fcb0e/README.md) +* [Arduino CLI manpage](https://github.com/arduino/Arduino/blob/master/build/shared/manpage.adoc) +* [Install extensions from file](https://vscode-docs.readthedocs.io/en/stable/extensions/install-extension/) +* [Publish vscode extensions](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) +* [Debug logging for IntelliSense](https://code.visualstudio.com/docs/cpp/enable-logging-cpp) +* [Arduino Dev Tools](https://playground.arduino.cc/Main/DevelopmentTools/) (obsolete/outdated) + +## Future Work +* Proper interactive serial terminal (this is the second major show stopper in my opinion) + * Command history option + * From https://github.com/microsoft/vscode-arduino/issues/463#issuecomment-583846263 and following: + * Allow input on the serial monitor in a convenient way - ie just type and hit return, just like the Arduino IDE + * Have the serial monitor window NOT keep turning off autoscroll (there is a separate ticket for this) + * Have the option of the serial monitor and/or compile window auto clear each time the sketch is compiled + * There is the annoying default where the compile runs in verbose mode and we have to manually edit config files to turn off the trace output + * Is there a way to automatically select the right serial port? + * Oh and one more. I want the serial output and perhaps compile windows to be undocked or at least I want them to sit to the right of my code window but they seem rigidly stuck at the bottom of the screen. + * And I would probably prioritize ease of use over better editing/intelligence. + * Being able to set baud rate within monitor + * Possible implementation hooks + * run node program in native terminal and connect it to extension + * https://github.com/serialport/node-serialport + * [General](https://serialport.io/docs/guide-about) + * [CLI](https://serialport.io/docs/guide-cli) + * [API](https://serialport.io/docs/guide-usage) + * write a [debugger extension](https://code.visualstudio.com/api/extension-guides/debugger-extension) with a [mock](https://github.com/Microsoft/vscode-mock-debug) which communicates with the serial +* Lots of redundant code + * e.g. "upload is a superset of "verify" + * general lack of modularity - the above is the result +* It seems that this extension is pretty chaotic. Most probably some refactoring is necessary. +* Possibility to jump to compilation errors from compiler output and highlight compiler errors +* Further IntelliSense enhancements/features: + * When having adding a library folder to the workspace IntelliSense should use the same configuration for it to enable library navigation and code completion. + * Optimization: Abort analysis build as soon as compiler statement has been found +* Non-IDE unit testing - to eliminate dependency injection use ts-mock-imports for instance +* Remove dead code +* Identify bad code and rework it + * all those auxiliary functions in `src/common/util.ts` should be reviewed as they violate every guideline of good coding (try-catch clauses without error handling etc.) +* Hardcoded and scattered constants: + * Load package.json and use values from therein instead of hard coding redundant values like shortcuts (like I did for the IntelliSense message in `arduino.ts`) + * Line splitting and other regexes + * Scan code for other hard coded stuff and take appropriate countermeasures +* Write good documentation/manual + * Integration of Arduino + * Project structure + * How does the compiling work + * Build/Arduino Backend + * Include paths + * Relation of IntelliSense and build + +## Non-categorized Notes +### Integrate upstream changes into fork +```bash +git remote add upstream https://github.com/microsoft/vscode-arduino.git +git remote -v +git fetch upstream +# make sure your working directory is clean, then +git checkout master +git merge upstream/master +git push origin master +# to pull the changes into you feature branch: +git checkout intellisense-autoconfig +git merge master +git push origin intellisense-autoconfig +``` +---- + +## How to beta test cutting edge code from the repo + +For development and testing of this fork/branch I've set up a dedicated chat room: + +[![Gitter](https://badges.gitter.im/vscode-arduino-ew-fork/community.svg)](https://gitter.im/vscode-arduino-ew-fork/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +**Note:** the topic is strictly development and testing of this feature enhancement branch. It's not a place where you can ask for help on your Arduino problems. There are dedicated forums for that and I'll delete any question of this type. + + +*I wrote the follwing for @iFreilicht, if anyone has some additional findings please let me know to keep this documentation up to date* + + > I'd love to try this out and give feedback. It would be a huge quality of life improvement if this was to work. Could you point me to a resource that helps me install your version of the extension? + +that's the reason I embarked on this project. Great if you'd like to help out - this comes in really handy. Getting the stuff rock solid is one of my central objectives and testing is one cornerstones here. + +Currently my work is at the following state: +* Parsing works +* Enable/disable via global or project flags +* You can have personal configurations which aren't overwritten + +But I'm not fully there yet. Things which are yet to be done are: + +* Move the parser/configuration-generator out of the "verify" code, since it requires the build to be run with `--verbose` flag enabled which most probably few like (except for me :) I always switch on all warnings and set it to verbose). + This is something I can implement pretty fast +* I have not removed the original "IntelliSense configurator" which ruins the `c_cpp_properties.json` every now and then but that's one of my next tasks as well. And you can simply delete the file as you can re-generate the perfect version with my analyzer/generator by simply verifying your sketch +* My development system runs on Ubuntu-GNU/linux and I haven't done any work for Windows yet but that's one of the next steps. The chances that it works on OSX "out of the box" are pretty good + +To run the development version clone my [repository](https://github.com/elektronikworkshop/vscode-arduino) and checkout the `intellisense-autoconfig` branch + +The following steps requires you to have `git`, `vscode`, `npm` and `nodejs` at recent versions. On my Ubuntu system I don't use the versions supplied by my package manager (`apt`) as they are usually pretty outdated - I usually install `nodejs` and `npm` from their respective/official websites. If you're on Windows you'll have to be a bit more patient since I haven't set up a virtual machine yet for testing and Windows is definitely not my domain. But I'll document the setup process as soon as I'll get to it. + +```bash +git clone https://github.com/elektronikworkshop/vscode-arduino +cd vscode-arduino +# switch to the feature branch (not necessary probably because this branch is set to default) +git checkout intellisense-autoconfig +# check if you're on the right branch +git status +# install module dependencies +npm install +# install gulp builder globally to make it available to the path (requires relaunching your shell) +# there's another option below, see ./launchcode.sh +npm install -g gulp +# to make sure that gulp is actually working type +gulp --tasks +# if not -> configure your $PATH and open a new terminal to make sure it's added, then +# open vscode +code . +``` +Making sure that gulp is on your `$PATH` is essential. As long this isn't the case the following steps must not be carried out. + +Another option to launch code with gulp on your path is (within bash or similar) +```bash +# create launch script +echo "PATH=./node_modules/.bin:$PATH code ." > launchcode +# make it executable +chmod +x launchcode +# now you can launch vscode like this +./launchcode +``` +This way you don't have to install gulp globally anymore (no `npm install -g gulp`). The path to the vscode dependency module binary is set as temporary environment variable when launching vscode. + +When everything's fine and vscode running, hit F5 to debug or select it from the *Debug* menu. vscode will then complain that there's *No task defined* and you let it generate the configuration for you by clicking the button *Configure Task*. After configuring the tasks debug (`F5`) or build (`Ctrl + Shift + B`) should work. + +As soon as you've got it up and running (`F5` spawns a new window), just navigate to your Arduino project. Configure in the vscode-arduino global settings the build output to `verbose` and run verify (`Ctrl + Alt + R`) as you know it. This will then generate a fresh `c_cpp_properties.json`. As long as I haven't removed the generator from the current maintainers you'll have to regenerate it as soon as you see those double asterisk-paths like `whatever/path/**` - if I get to it today, I'll give it a try and will remove/disable it for testing. You can then pull my changes in by running +```bash +git pull +``` +from within your terminal inside vscode (and your `vscode-arduino` folder). + +Different IntelliSense configurations can be selected in vscode in the lower right. The configuration my branch generates is called *Arduino* as depicted here: + +![74001156-cfce8280-496a-11ea-9b9d-7d30c83765c1](https://user-images.githubusercontent.com/21954933/74351237-2696ea80-4db7-11ea-9f7a-1bfc652ad5f5.png) + +Sometimes IntelliSense has problems within the extension host (which you're running when launching it with `F5`) and fails to resolve some header paths. This is a problem of vscode and not the extension. Then just restart. The extension host is a bit buggy if you ask me, so I hope I can provide some development snapshots in the future which will render all the steps above superfluous and let you run the latest development version with your regular vscode. + +---- + +# Implementation +Here are some implementation notes. Probably only of interest to me. + +## Build Output Parser +### Intrinsic Include Paths +Some include paths are built into gcc and don't have to be specified on the command line. Just searching the compiler installation directory with something like +```bash +find ~/.arduino15/packages/esp32/tools/xtensa-esp32-elf-gcc/1.22.0-80-g6c4433a-5.2.0/ -name "include*" +``` +won't do since not all include directories are named `include`. Fortunately gcc can be queried about its configuration ([source](https://stackoverflow.com/a/6666338)) -- built-in include paths are part of it: +```bash +# generally for C++ +gcc -xc++ -E -v - +# for esp32 +~/.arduino15/packages/esp32/tools/xtensa-esp32-elf-gcc/1.22.0-80-g6c4433a-5.2.0/bin/xtensa-esp32-elf-gcc -xc++ -E -v - < /dev/null > xtensa-esp32-elf-gcc_built_in_specs.txt 2>&1 +# avr +~/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino5/bin/avr-gcc -xc++ -E -v - < /dev/null > avr-gcc_built_in_specs.txt 2>&1 +``` +The result can be inspected here: +* [xtensa-esp32-elf-gcc_built_in_specs.txt](doc/intellisense/compilerinfo/xtensa-esp32-elf-gcc_built_in_specs.txt) +* [avr-gcc_built_in_specs.txt](doc/intellisense/compilerinfo/avr-gcc_built_in_specs.txt) + +To show the most interesting section in the output of the above commands, for ESP32: +``` +#include "..." search starts here: +#include <...> search starts here: + /home/uli/.arduino15/packages/esp32/tools/xtensa-esp32-elf-gcc/1.22.0-80-g6c4433a-5.2.0/bin/../lib/gcc/xtensa-esp32-elf/5.2.0/../../../../xtensa-esp32-elf/include/c++/5.2.0 + /home/uli/.arduino15/packages/esp32/tools/xtensa-esp32-elf-gcc/1.22.0-80-g6c4433a-5.2.0/bin/../lib/gcc/xtensa-esp32-elf/5.2.0/../../../../xtensa-esp32-elf/include/c++/5.2.0/xtensa-esp32-elf + /home/uli/.arduino15/packages/esp32/tools/xtensa-esp32-elf-gcc/1.22.0-80-g6c4433a-5.2.0/bin/../lib/gcc/xtensa-esp32-elf/5.2.0/../../../../xtensa-esp32-elf/include/c++/5.2.0/backward + /home/uli/.arduino15/packages/esp32/tools/xtensa-esp32-elf-gcc/1.22.0-80-g6c4433a-5.2.0/bin/../lib/gcc/xtensa-esp32-elf/5.2.0/include + /home/uli/.arduino15/packages/esp32/tools/xtensa-esp32-elf-gcc/1.22.0-80-g6c4433a-5.2.0/bin/../lib/gcc/xtensa-esp32-elf/5.2.0/include-fixed + /home/uli/.arduino15/packages/esp32/tools/xtensa-esp32-elf-gcc/1.22.0-80-g6c4433a-5.2.0/bin/../lib/gcc/xtensa-esp32-elf/5.2.0/../../../../xtensa-esp32-elf/include + /home/uli/.arduino15/packages/esp32/tools/xtensa-esp32-elf-gcc/1.22.0-80-g6c4433a-5.2.0/bin/../xtensa-esp32-elf/sysroot/usr/include +End of search list. +``` +for AVR: +``` +#include "..." search starts here: +#include <...> search starts here: + /home/uli/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino5/bin/../lib/gcc/avr/7.3.0/include + /home/uli/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino5/bin/../lib/gcc/avr/7.3.0/include-fixed + /home/uli/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino5/bin/../lib/gcc/avr/7.3.0/../../../../avr/include +End of search list. +``` +As one can see with the ESP32-gcc not all include directories are named `include`. Parsing of this output is pretty trivial though. + +## `c_cpp_properties.json` Generator + + +## Settings +### Global Settings +Under linux at `~/.config/Code/User/settings.json`, for instance: +```json +{ + "arduino.additionalUrls": "", + "arduino.logLevel": "verbose", + "arduino.disableTestingOpen": true, + "workbench.editor.enablePreview": false +} +``` +Code: [src/arduino/arduinoSettings.ts](src/arduino/arduinoSettings.ts) +Code: [src/arduino/vscodeSettings.ts](src/arduino/vscodeSettings.ts) +Validator: [package.json](package.json) + +### Project Settings +Path in project `.vscode/arduino.json` +```json +{ + "board": "arduino:avr:nano", + "configuration": "cpu=atmega328old", + "sketch": "examples/lcdpong-butenc/lcdpong-butenc.ino", + "port": "/dev/ttyUSB0" +} +``` +Code: [src/deviceContext.ts](src/deviceContext.ts) +Validator: [misc/arduinoValidator.json](misc/arduinoValidator.json) + +## General Tasks +### Removing existing Attempts which mess with c_cpp_properties.json or Intellisense + +Remove these as they are helpless attempts to get IntelliSense working: +```ts +//src/arduino/arduino.ts + tryToUpdateIncludePaths() + addLibPath(libraryPath: string) + getDefaultForcedIncludeFiles() + // parts in + openExample() + + //probably not needed anymore: + getDefaultPackageLibPaths() + +``` +Remove this as this messes in an unpredictable and helpless way with Intellisense +[src/langService/completionProvider.ts](src/langService/completionProvider.ts) + +Review this folder as some of this is probably obsolete when Intellisense works properly: +``` +syntaxes/arduino.configuration.json +syntaxes/arduino.tmLanguage +# Within package.json + { + "language": "cpp", + "path": "./syntaxes/arduino.tmLanguage", + "scopeName": "source.cpp.arduino" + }, +``` diff --git a/README.md b/README.md index 4b7af0fa..87c65c99 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,12 @@ This extension provides several commands in the Command Palette (F1 o - **Arduino: Upload**: Build sketch and upload to Arduino board. - **Arduino: Upload Using Programmer**: Upload using an external programmer. - **Arduino: Verify**: Build sketch. +- **Arduino: Rebuild IntelliSense Configuration**: Forced/manual rebuild of the IntelliSense configuration. The extension analyzes Arduino's build output and sets the Intellisense include paths, defines, compiler arguments accordingly. ## Keybindings - **Arduino: Upload** Alt + Cmd + U *or* Alt + Ctrl + U - **Arduino: Verify** Alt + Cmd + R *or* Alt + Ctrl + R +- **Arduino: Rebuild IntelliSense Configuration** Alt + Cmd + I *or* Alt + Ctrl + I ## Options | Option | Description | @@ -67,6 +69,7 @@ This extension provides several commands in the Command Palette (F1 o | `arduino.disableTestingOpen` | Enable/disable automatic sending of a test message to the serial port for checking the open status. The default value is `false` (a test message will be sent). | | `arduino.skipHeaderProvider` | Enable/disable the extension providing completion items for headers. This functionality is included in newer versions of the C++ extension. The default value is `false`.| | `arduino.defaultBaudRate` | Default baud rate for the serial port monitor. The default value is 115200. Supported values are 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 74880, 115200, 230400 and 250000 | +| `arduino.disableIntelliSenseAutoGen` | When `true` vscode-arduino will not auto-generate an IntelliSense configuration (i.e. `.vscode/c_cpp_properties.json`) by analyzing Arduino's compiler output. | The following Visual Studio Code settings are available for the Arduino extension. These can be set in global user preferences Ctrl + , or workspace settings (`.vscode/settings.json`). The latter overrides the former. @@ -98,7 +101,9 @@ The following settings are as per sketch settings of the Arduino extension. You "board": "adafruit:samd:adafruit_feather_m0", "output": "../build", "debugger": "jlink", - "prebuild": "bash prebuild.sh" + "prebuild": "./prebuild.sh", + "postbuild": "./postbuild.sh", + "intelliSenseGen": "global" } ``` - `sketch` - The main sketch file name of Arduino. @@ -106,7 +111,68 @@ The following settings are as per sketch settings of the Arduino extension. You - `board` - Currently selected Arduino board alias. Can be set by the `Arduino: Change Board Type` command. Also, you can find the board list there. - `output` - Arduino build output path. If not set, Arduino will create a new temporary output folder each time, which means it cannot reuse the intermediate result of the previous build leading to long verify/upload time, so it is recommended to set the field. Arduino requires that the output path should not be the workspace itself or in a subfolder of the workspace, otherwise, it may not work correctly. By default, this option is not set. It's worth noting that the contents of this file could be deleted during the build process, so pick (or create) a directory that will not store files you want to keep. - `debugger` - The short name of the debugger that will be used when the board itself does not have a debugger and there is more than one debugger available. You can find the list of debuggers [here](https://github.com/Microsoft/vscode-arduino/blob/master/misc/debuggerUsbMapping.json). By default, this option is not set. -- `prebuild` - External command before building the sketch file. You should only set one `prebuild` command. `command1 && command2` does not work. If you need to run multiple commands before the build, then create a script. +- `prebuild` - External command which will be invoked before any sketch build (verify, upload, ...). For details see the [Pre- and Post-Build Commands](#Pre--and-Post-Build-Commands) section. +- `postbuild` - External command to be run after the sketch has been built successfully. See the afore mentioned section for more details. +- `intelliSenseGen` - Override the global setting for auto-generation of the IntelliSense configuration (i.e. `.vscode/c_cpp_properties.json`). Three options are available: + - `"global"`: Use the global settings (default) + - `"disable"`: Disable the auto-generation even if globally enabled + - `"enable"`: Enable the auto-generation even if globally disabled +- `buildPreferences` - Set Arduino preferences which then are used during any build (verify, upload, ...). This allows for extra defines, compiler options or includes. The preference key-value pairs must be set as follows: +```json + "buildPreferences": [ + ["build.extra_flags", "-DMY_DEFINE=666 -DANOTHER_DEFINE=3.14 -Wall"], + ["compiler.cpp.extra_flags", "-DYET_ANOTER=\"hello\""] + ] +} +``` + +## Pre- and Post-Build Commands +On Windows the commands run within a `cmd`-, on Linux and OSX within a `bash`-instance. Therefore your command can be anything what you can run within those shells. Instead of running a command you can invoke a script. This makes writing more complex pre-/post-build mechanisms much easier and opens up the possibility to run python or other scripting languages. +The commands run within the workspace root directory and vscode-arduino sets the following environment variables: +**`VSCA_BUILD_MODE`** The current build mode, one of `Verifying`, `Uploading`, `Uploading (programmer)` or `Analyzing`. This allows you to run your script on certain build modes only. +**`VSCA_SKETCH`** The sketch file relative to your workspace root directory. +**`VSCA_BOARD`** Your board and configuration, e.g. `arduino:avr:nano:cpu=atmega328`. +**`VSCA_WORKSPACE_DIR`** The absolute path of your workspace root directory. +**`VSCA_LOG_LEVEL`** The current log level. This allows you to control the verbosity of your scripts. +**`VSCA_SERIAL`** The serial port used for uploading. Not set if you haven't set one in your `arduino.json`. +**`VSCA_BUILD_DIR`** The build directory. Not set if you haven't set one in your `arduino.json`. + +For example under Windows the following `arduino.json` setup +```json +{ + "board": "arduino:avr:nano", + "sketch": "test.ino", + "configuration": "cpu=atmega328", + "prebuild": "IF \"%VSCA_BUILD_MODE%\"==\"Verifying\" (echo VSCA_BUILD_MODE=%VSCA_BUILD_MODE% && echo VSCA_BOARD=%VSCA_BOARD%)" +} +``` +will produce +``` +[Starting] Verifying sketch 'test.ino' +Running pre-build command: "IF "%VSCA_BUILD_MODE%"=="Verifying" (echo VSCA_BUILD_MODE=%VSCA_BUILD_MODE% && echo VSCA_BOARD=%VSCA_BOARD%)" +VSCA_BUILD_MODE=Verifying +VSCA_BOARD=arduino:avr:nano:cpu=atmega328 +Loading configuration... +<...> +``` +when verifying. + +## IntelliSense +vscode-arduino auto-configures IntelliSense by default. vscode-arduino analyzes Arduino's compiler output by running a separate build and generates the corresponding configuration file at `.vscode/c_cpp_properties.json`. vscode-arduino tries as hard as possible to keep things up to date, e.g. it runs the analysis when switching the board or the sketch. + +It doesn't makes sense though to run the analysis repeatedly. Therefore if the workspace reports problems ("squiggles") - for instance after adding new includes from a new library - run the analysis manually: + +Manual rebuild: **Arduino: Rebuild IntelliSense Configuration**, +Keybindings: Alt + Cmd + I *or* Alt + Ctrl + I + +When the analysis is invoked manually it ignores any global and project specific disable. + +### IntelliSense Configurations +vscode-arduino's analysis stores the result as a dedicated IntelliSense-configuration named `Arduino`. You have to select it from the far right of the status bar when you're in one of your source files as shown here: + +![74001156-cfce8280-496a-11ea-9b9d-7d30c83765c1](https://user-images.githubusercontent.com/21954933/74351237-2696ea80-4db7-11ea-9f7a-1bfc652ad5f5.png) + +This system allows you to setup and use own IntelliSense configurations in parallel to the automatically generated configurations provided through vscode-arduino. Just add your configuration to `c_cpp_properties.json` and name it differently from the default configuration (`Arduino`), e.g. `My awesome configuration` and select it from the status bar or via the command palette command **C/C++: Select a Configuration...** ## Debugging Arduino Code preview Before you start to debug your Arduino code, please read [this document](https://code.visualstudio.com/docs/editor/debugging) to learn about the basic mechanisms of debugging in Visual Studio Code. Also see [debugging for C++ in VSCode](https://code.visualstudio.com/docs/languages/cpp#_debugging) for further reference. diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index fdacfadc..95505d66 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -25,7 +25,7 @@ Visual Studio Code Extension for Arduino incorporates third party material from 18. vscode-extension-telemetry (https://github.com/Microsoft/vscode-extension-telemetry) 19. winreg (https://github.com/fresc81/node-winreg) 20. Winston (https://github.com/winstonjs/winston) - +21. cocopa (https://github.com/elektronikworkshop/cocopa) %% body-parser NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= @@ -532,4 +532,31 @@ THE SOFTWARE. ========================================= END OF Winston NOTICES, INFORMATION, AND LICENSE +%% cocopa NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +Copyright (C) 2020 Uli Franke - Elektronik Workshop + +All rights reserved. + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF cocopa NOTICES, INFORMATION, AND LICENSE diff --git a/misc/arduinoValidator.json b/misc/arduinoValidator.json index 73baff4f..fc6b10d0 100644 --- a/misc/arduinoValidator.json +++ b/misc/arduinoValidator.json @@ -32,6 +32,38 @@ "description": "Arduino Debugger Settings", "type": "string", "minLength": 1 + }, + "intelliSenseGen": { + "description": "Disable/enable the automatic generation of the IntelliSense configuration file (c_cpp_properties.json) for this project (overrides the global setting). When set to \"global\" the global extension settings will be used.", + "type": "string", + "default": "global", + "enum": [ + "global", + "disable", + "enable" + ] + }, + "prebuild": { + "description": "Command to be run before every build", + "type": "string", + "minLength": 1 + }, + "postbuild": { + "description": "Command to be run after every build", + "type": "string", + "minLength": 1 + }, + "buildPreferences": { + "description": "Arduino preferences which are passed to the Arduino back-end during build", + "type": "array", + "items": { + "type":"array", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "string" + } + } } } } \ No newline at end of file diff --git a/package.json b/package.json index 8c3b9587..8b09036b 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "homepage": "https://github.com/Microsoft/vscode-arduino/blob/master/README.md", "categories": [ - "Languages", + "Programming Languages", "Debuggers", "Snippets", "Formatters" @@ -37,10 +37,10 @@ "onCommand:arduino.verify", "onCommand:arduino.upload", "onCommand:arduino.uploadUsingProgrammer", + "onCommand:arduino.rebuildIntelliSenseConfig", "onCommand:arduino.selectProgrammer", "onCommand:arduino.selectSerialPort", "onCommand:arduino.changeBaudRate", - "onCommand:arduino.addLibPath", "onCommand:arduino.openSerialMonitor", "onCommand:arduino.sendMessageToSerialPort", "onCommand:arduino.closeSerialMonitor", @@ -89,6 +89,10 @@ "command": "arduino.uploadUsingProgrammer", "title": "Arduino: Upload Using Programmer" }, + { + "command": "arduino.rebuildIntelliSenseConfig", + "title": "Arduino: Rebuild IntelliSense Configuration" + }, { "command": "arduino.selectProgrammer", "title": "Arduino: Select Programmer" @@ -233,7 +237,7 @@ }, "targetArchitecture": { "type": "string", - "description": "The architecture of the debuggee.", + "description": "The architecture of the debugger.", "default": "arm" }, "cwd": { @@ -434,6 +438,11 @@ "command": "arduino.upload", "key": "ctrl+alt+u", "mac": "cmd+alt+u" + }, + { + "command": "arduino.rebuildIntelliSenseConfig", + "key": "ctrl+alt+i", + "mac": "cmd+alt+i" } ], "configuration": { @@ -489,6 +498,11 @@ "arduino.defaultBaudRate": { "type": "number", "default": 115200 + }, + "arduino.disableIntelliSenseAutoGen": { + "type": "boolean", + "default": false, + "description": "When disabled vscode-arduino will not auto-generate an IntelliSense configuration (i.e. c_cpp_properties.json) by analyzing the compiler output." } } }, @@ -544,8 +558,9 @@ "devDependencies": { "@types/compare-versions": "^3.0.0", "@types/mocha": "^2.2.32", - "@types/node": "^6.0.40", + "@types/node": "^6.14.9", "@types/winreg": "^1.2.30", + "ajv": "^5.0.0", "del": "^2.2.2", "eslint": "^4.18.2", "eslint-config-standard": "^10.2.1", @@ -565,12 +580,13 @@ "plugin-error": "^1.0.1", "tslint": "^5.20.1", "typemoq": "^1.6.0", - "typescript": "^2.2.1", + "typescript": "^3.7.5", "vscode": "^1.1.36", "webpack": "^4.41.2" }, "dependencies": { "body-parser": "^1.16.1", + "cocopa": "0.0.13", "compare-versions": "^3.4.0", "eventemitter2": "^4.1.0", "express": "^4.14.1", diff --git a/src/arduino/arduino.ts b/src/arduino/arduino.ts index 73619a7b..732643fb 100644 --- a/src/arduino/arduino.ts +++ b/src/arduino/arduino.ts @@ -9,20 +9,35 @@ import * as vscode from "vscode"; import * as constants from "../common/constants"; import * as util from "../common/util"; -import * as Logger from "../logger/logger"; +import * as logger from "../logger/logger"; import { DeviceContext } from "../deviceContext"; import { IArduinoSettings } from "./arduinoSettings"; import { BoardManager } from "./boardManager"; import { ExampleManager } from "./exampleManager"; +import { AnalysisManager, + isCompilerParserEnabled, + makeCompilerParserContext } from "./intellisense"; import { LibraryManager } from "./libraryManager"; +import { ProgrammerManager } from "./programmerManager"; import { VscodeSettings } from "./vscodeSettings"; import { arduinoChannel } from "../common/outputChannel"; import { ArduinoWorkspace } from "../common/workspace"; import { SerialMonitor } from "../serialmonitor/serialMonitor"; import { UsbDetector } from "../serialmonitor/usbDetector"; -import { ProgrammerManager } from "./programmerManager"; + +/** + * Supported build modes. For further explanation see the documentation + * of ArduinoApp.build(). + * The strings are used for status reporting within the above function. + */ +export enum BuildMode { + Verify = "Verifying", + Analyze = "Analyzing", + Upload = "Uploading", + UploadProgrammer = "Uploading (programmer)", +}; /** * Represent an Arduino application based on the official Arduino IDE. @@ -37,10 +52,30 @@ export class ArduinoApp { private _programmerManager: ProgrammerManager; + /** + * IntelliSense analysis manager. + * Makes sure that analysis builds and regular builds go along + * and that multiple subsequent analysis requests - as triggered + * by board/board-configuration changes - are bundled to a single + * analysis build run. + */ + private _analysisManager: AnalysisManager; + + /** + * Indicates if a build is currently in progress. + * If so any call to this.build() will return false immediately. + */ + private _building: boolean = false; + /** * @param {IArduinoSettings} _settings ArduinoSetting object. */ constructor(private _settings: IArduinoSettings) { + const analysisDelayMs = 1000 * 3; + this._analysisManager = new AnalysisManager( + () => this._building, + async () => { await this.build(BuildMode.Analyze); }, + analysisDelayMs); } /** @@ -63,6 +98,17 @@ export class ArduinoApp { } catch (ex) { } } + + // set up event handling for IntelliSense analysis + const requestAnalysis = async () => { + if (isCompilerParserEnabled()) { + await this._analysisManager.requestAnalysis(); + } + }; + const dc = DeviceContext.getInstance(); + dc.onChangeBoard(requestAnalysis); + dc.onChangeConfiguration(requestAnalysis); + dc.onChangeSketch(requestAnalysis); } /** @@ -87,364 +133,59 @@ export class ArduinoApp { public async setPref(key, value) { try { await util.spawn(this._settings.commandPath, - null, - ["--pref", `${key}=${value}`, "--save-prefs"]); + ["--pref", `${key}=${value}`, "--save-prefs"]); } catch (ex) { } } - public async upload() { - const dc = DeviceContext.getInstance(); - const boardDescriptor = this.getBoardBuildString(); - if (!boardDescriptor) { - return; - } - - if (!ArduinoWorkspace.rootPath) { - vscode.window.showWarningMessage("Cannot find the sketch file."); - return; - } - - if (!dc.sketch || !util.fileExistsSync(path.join(ArduinoWorkspace.rootPath, dc.sketch))) { - await this.getMainSketch(dc); - } - - if ((!dc.configuration || !/upload_method=[^=,]*st[^,]*link/i.test(dc.configuration)) && !dc.port) { - const choice = await vscode.window.showInformationMessage( - "Serial port is not specified. Do you want to select a serial port for uploading?", - "Yes", "No"); - if (choice === "Yes") { - vscode.commands.executeCommand("arduino.selectSerialPort"); - } - return; - } - - arduinoChannel.show(); - arduinoChannel.start(`Upload sketch - ${dc.sketch}`); - - const serialMonitor = SerialMonitor.getInstance(); - - const needRestore = await serialMonitor.closeSerialMonitor(dc.port); - UsbDetector.getInstance().pauseListening(); - await vscode.workspace.saveAll(false); - - if (dc.prebuild) { - arduinoChannel.info(`Run prebuild command: ${dc.prebuild}`); - const prebuildargs = dc.prebuild.split(" "); - const prebuildCommand = prebuildargs.shift(); - try { - await util.spawn(prebuildCommand, arduinoChannel.channel, prebuildargs, { shell: true, cwd: ArduinoWorkspace.rootPath }); - } catch (ex) { - arduinoChannel.error(`Run prebuild failed: \n${ex.error}`); - return; - } - } - - const appPath = path.join(ArduinoWorkspace.rootPath, dc.sketch); - const args = ["--upload", "--board", boardDescriptor]; - if (dc.port) { - args.push("--port", dc.port); - } - args.push(appPath); - if (VscodeSettings.getInstance().logLevel === "verbose") { - args.push("--verbose"); - } - if (dc.output) { - const outputPath = path.resolve(ArduinoWorkspace.rootPath, dc.output); - const dirPath = path.dirname(outputPath); - if (!util.directoryExistsSync(dirPath)) { - Logger.notifyUserError("InvalidOutPutPath", new Error(constants.messages.INVALID_OUTPUT_PATH + outputPath)); - return; - } - - args.push("--pref", `build.path=${outputPath}`); - arduinoChannel.info(`Please see the build logs in Output path: ${outputPath}`); - } else { - const msg = "Output path is not specified. Unable to reuse previously compiled files. Upload could be slow. See README."; - arduinoChannel.warning(msg); - } - await util.spawn(this._settings.commandPath, arduinoChannel.channel, args).then(async () => { - UsbDetector.getInstance().resumeListening(); - if (needRestore) { - await serialMonitor.openSerialMonitor(); - } - arduinoChannel.end(`Uploaded the sketch: ${dc.sketch}${os.EOL}`); - }, (reason) => { - arduinoChannel.error(`Exit with code=${reason.code}${os.EOL}`); - }); - } - - public async uploadUsingProgrammer() { - const dc = DeviceContext.getInstance(); - const boardDescriptor = this.getBoardBuildString(); - if (!boardDescriptor) { - return; - } - - const selectProgrammer = this.getProgrammerString(); - if (!selectProgrammer) { - return; - } - - if (!ArduinoWorkspace.rootPath) { - vscode.window.showWarningMessage("Cannot find the sketch file."); - return; - } - - if (!dc.sketch || !util.fileExistsSync(path.join(ArduinoWorkspace.rootPath, dc.sketch))) { - await this.getMainSketch(dc); - } - if (!dc.port) { - const choice = await vscode.window.showInformationMessage( - "Serial port is not specified. Do you want to select a serial port for uploading?", - "Yes", "No"); - if (choice === "Yes") { - vscode.commands.executeCommand("arduino.selectSerialPort"); - } - return; - } - - arduinoChannel.show(); - arduinoChannel.start(`Upload sketch - ${dc.sketch}`); - - const serialMonitor = SerialMonitor.getInstance(); - - const needRestore = await serialMonitor.closeSerialMonitor(dc.port); - UsbDetector.getInstance().pauseListening(); - await vscode.workspace.saveAll(false); - - const appPath = path.join(ArduinoWorkspace.rootPath, dc.sketch); - const args = ["--upload", "--board", boardDescriptor, "--port", dc.port, "--useprogrammer", - "--pref", "programmer=" + selectProgrammer, appPath]; - if (VscodeSettings.getInstance().logLevel === "verbose") { - args.push("--verbose"); - } - if (dc.output) { - const outputPath = path.resolve(ArduinoWorkspace.rootPath, dc.output); - const dirPath = path.dirname(outputPath); - if (!util.directoryExistsSync(dirPath)) { - Logger.notifyUserError("InvalidOutPutPath", new Error(constants.messages.INVALID_OUTPUT_PATH + outputPath)); - return; - } - - args.push("--pref", `build.path=${outputPath}`); - arduinoChannel.info(`Please see the build logs in Output path: ${outputPath}`); - } else { - const msg = "Output path is not specified. Unable to reuse previously compiled files. Upload could be slow. See README."; - arduinoChannel.warning(msg); - } - await util.spawn(this._settings.commandPath, arduinoChannel.channel, args).then(async () => { - UsbDetector.getInstance().resumeListening(); - if (needRestore) { - await serialMonitor.openSerialMonitor(); - } - arduinoChannel.end(`Uploaded the sketch: ${dc.sketch}${os.EOL}`); - }, (reason) => { - arduinoChannel.error(`Exit with code=${reason.code}${os.EOL}`); - }); + /** + * Returns true if a build is currently in progress. + */ + public get building() { + return this._building; } - public async verify(output: string = "") { - const dc = DeviceContext.getInstance(); - const boardDescriptor = this.getBoardBuildString(); - if (!boardDescriptor) { - return; - } - - if (!ArduinoWorkspace.rootPath) { - vscode.window.showWarningMessage("Cannot find the sketch file."); - return; - } - - if (!dc.sketch || !util.fileExistsSync(path.join(ArduinoWorkspace.rootPath, dc.sketch))) { - await this.getMainSketch(dc); - } - - await vscode.workspace.saveAll(false); - - arduinoChannel.start(`Verify sketch - ${dc.sketch}`); - - if (dc.prebuild) { - arduinoChannel.info(`Run prebuild command: ${dc.prebuild}`); - const prebuildargs = dc.prebuild.split(" "); - const prebuildCommand = prebuildargs.shift(); - try { - await util.spawn(prebuildCommand, arduinoChannel.channel, prebuildargs, { shell: true, cwd: ArduinoWorkspace.rootPath }); - } catch (ex) { - arduinoChannel.error(`Run prebuild failed: \n${ex.error}`); - return; - } - } - - const appPath = path.join(ArduinoWorkspace.rootPath, dc.sketch); - const args = ["--verify", "--board", boardDescriptor, appPath]; - if (VscodeSettings.getInstance().logLevel === "verbose") { - args.push("--verbose"); - } - if (output || dc.output) { - const outputPath = path.resolve(ArduinoWorkspace.rootPath, output || dc.output); - const dirPath = path.dirname(outputPath); - if (!util.directoryExistsSync(dirPath)) { - Logger.notifyUserError("InvalidOutPutPath", new Error(constants.messages.INVALID_OUTPUT_PATH + outputPath)); - return; - } - - args.push("--pref", `build.path=${outputPath}`); - arduinoChannel.info(`Please see the build logs in Output path: ${outputPath}`); - } else { - const msg = "Output path is not specified. Unable to reuse previously compiled files. Verify could be slow. See README."; - arduinoChannel.warning(msg); - } + /** + * Runs the arduino builder to build/compile and - if necessary - upload + * the current sketch. + * @param mode Build mode. + * * BuildMode.Upload: Compile and upload + * * BuildMode.UploadProgrammer: Compile and upload using the user + * selectable programmer + * * BuildMode.Analyze: Compile, analyze the output and generate + * IntelliSense configuration from it. + * * BuildMode.Verify: Just compile. + * All build modes except for BuildMode.Analyze run interactively, i.e. if + * something is missing, it tries to query the user for the missing piece + * of information (sketch, board, etc.). Analyze runs non interactively and + * just returns false. + * @param buildDir Override the build directory set by the project settings + * with the given directory. + * @returns true on success, false if + * * another build is currently in progress + * * board- or programmer-manager aren't initialized yet + * * or something went wrong during the build + */ + public async build(mode: BuildMode, buildDir?: string) { - arduinoChannel.show(); - // we need to return the result of verify - try { - await util.spawn(this._settings.commandPath, arduinoChannel.channel, args); - arduinoChannel.end(`Finished verify sketch - ${dc.sketch}${os.EOL}`); - return true; - } catch (reason) { - const msg = reason.code ? - `Exit with code=${reason.code}${os.EOL}` : - reason.message ? - reason.message : - JSON.stringify(reason); - arduinoChannel.error(msg); + if (!this._boardManager || !this._programmerManager || this._building) { return false; } - } - - public tryToUpdateIncludePaths() { - const configFilePath = path.join(ArduinoWorkspace.rootPath, constants.CPP_CONFIG_FILE); - if (!fs.existsSync(configFilePath)) { - return; - } - const cppConfigFile = fs.readFileSync(configFilePath, "utf8"); - const cppConfig = JSON.parse(cppConfigFile) as { configurations: Array<{ includePath: string[], forcedInclude: string[] }> }; - const libPaths = this.getDefaultPackageLibPaths(); - const defaultForcedInclude = this.getDefaultForcedIncludeFiles(); - const configuration = cppConfig.configurations[0]; - - let cppConfigFileUpdated = false; - // cpp exntension changes \\ to \\\\ in paths in JSON string, revert them first - configuration.includePath = configuration.includePath.map((path) => path.replace(/\\\\/g, "\\")); - configuration.forcedInclude = configuration.forcedInclude.map((path) => path.replace(/\\\\/g, "\\")); - - for (const libPath of libPaths) { - if (configuration.includePath.indexOf(libPath) === -1) { - cppConfigFileUpdated = true; - configuration.includePath.push(libPath); - } - } - for (const forcedIncludePath of defaultForcedInclude) { - if (configuration.forcedInclude.indexOf(forcedIncludePath) === -1) { - cppConfigFileUpdated = true; - configuration.forcedInclude.push(forcedIncludePath); - } - } - - // remove all unexisting paths - // concern mistake removal, comment temporary - // for (let pathIndex = 0; pathIndex < configuration.includePath.length; pathIndex++) { - // let libPath = configuration.includePath[pathIndex]; - // if (libPath.indexOf("${workspaceFolder}") !== -1) { - // continue; - // } - // if (/\*$/.test(libPath)) { - // libPath = libPath.match(/^[^\*]*/)[0]; - // } - // if (!fs.existsSync(libPath)) { - // cppConfigFileUpdated = true; - // configuration.includePath.splice(pathIndex, 1); - // pathIndex--; - // } - // } - // for (let pathIndex = 0; pathIndex < configuration.forcedInclude.length; pathIndex++) { - // const forcedIncludePath = configuration.forcedInclude[pathIndex]; - // if (forcedIncludePath.indexOf("${workspaceFolder}") !== -1) { - // continue; - // } - // if (!fs.existsSync(forcedIncludePath)) { - // cppConfigFileUpdated = true; - // configuration.forcedInclude.splice(pathIndex, 1); - // pathIndex--; - // } - // } - - if (cppConfigFileUpdated) { - fs.writeFileSync(configFilePath, JSON.stringify(cppConfig, null, 4)); - } - } - - // Add selected library path to the intellisense search path. - public addLibPath(libraryPath: string) { - let libPaths; - if (libraryPath) { - libPaths = [libraryPath]; - } else { - libPaths = this.getDefaultPackageLibPaths(); - } - - const defaultForcedInclude = this.getDefaultForcedIncludeFiles(); - - if (!ArduinoWorkspace.rootPath) { - return; - } - const configFilePath = path.join(ArduinoWorkspace.rootPath, constants.CPP_CONFIG_FILE); - let deviceContext = null; - if (!util.fileExistsSync(configFilePath)) { - util.mkdirRecursivelySync(path.dirname(configFilePath)); - deviceContext = {}; - } else { - deviceContext = util.tryParseJSON(fs.readFileSync(configFilePath, "utf8")); - } - if (!deviceContext) { - Logger.notifyAndThrowUserError("arduinoFileError", new Error(constants.messages.ARDUINO_FILE_ERROR)); - } - - deviceContext.configurations = deviceContext.configurations || []; - let configSection = null; - deviceContext.configurations.forEach((section) => { - if (section.name === util.getCppConfigPlatform()) { - configSection = section; - } - }); - - if (!configSection) { - configSection = { - name: util.getCppConfigPlatform(), - includePath: [], - }; - deviceContext.configurations.push(configSection); - } + this._building = true; - libPaths.forEach((childLibPath) => { - childLibPath = path.resolve(path.normalize(childLibPath)); - if (configSection.includePath && configSection.includePath.length) { - for (const existingPath of configSection.includePath) { - if (childLibPath === path.resolve(path.normalize(existingPath))) { - return; - } - } - } else { - configSection.includePath = []; - } - configSection.includePath.unshift(childLibPath); + return await this._build(mode, buildDir) + .then((ret) => { + this._building = false; + return ret; + }) + .catch((reason) => { + this._building = false; + logger.notifyUserError("ArduinoApp.build", + reason, + `Unhandled exception when cleaning up build "${mode}": ${JSON.stringify(reason)}`); + return false; }); - - if (!configSection.forcedInclude) { - configSection.forcedInclude = defaultForcedInclude; - } else { - for (let i = 0; i < configSection.forcedInclude.length; i++) { - if (/arduino\.h$/i.test(configSection.forcedInclude[i])) { - configSection.forcedInclude.splice(i, 1); - i--; - } - } - configSection.forcedInclude = defaultForcedInclude.concat(configSection.forcedInclude); - } - - fs.writeFileSync(configFilePath, JSON.stringify(deviceContext, null, 4)); } // Include the *.h header files from selected library to the arduino sketch. @@ -503,9 +244,9 @@ Please make sure the folder is not occupied by other procedures .`); } try { await util.spawn(this._settings.commandPath, - showOutput ? arduinoChannel.channel : null, - ["--install-boards", `${packageName}${arch && ":" + arch}${version && ":" + version}`]); - + ["--install-boards", `${packageName}${arch && ":" + arch}${version && ":" + version}`], + undefined, + { channel: showOutput ? arduinoChannel.channel : null }); if (updatingIndex) { arduinoChannel.end("Updated package index files."); } else { @@ -541,9 +282,9 @@ Please make sure the folder is not occupied by other procedures .`); } try { await util.spawn(this._settings.commandPath, - showOutput ? arduinoChannel.channel : null, - ["--install-library", `${libName}${version && ":" + version}`]); - + ["--install-library", `${libName}${version && ":" + version}`], + undefined, + { channel: showOutput ? arduinoChannel.channel : undefined }); if (updatingIndex) { arduinoChannel.end("Updated library index files."); } else { @@ -569,45 +310,6 @@ Please make sure the folder is not occupied by other procedures .`); arduinoChannel.end(`Removed library - ${libName}${os.EOL}`); } - public getDefaultPackageLibPaths(): string[] { - const result = []; - const boardDescriptor = this._boardManager.currentBoard; - if (!boardDescriptor) { - return result; - } - const toolsPath = boardDescriptor.platform.rootBoardPath; - result.push(path.normalize(path.join(toolsPath, "**"))); - // if (util.directoryExistsSync(path.join(toolsPath, "cores"))) { - // const coreLibs = fs.readdirSync(path.join(toolsPath, "cores")); - // if (coreLibs && coreLibs.length > 0) { - // coreLibs.forEach((coreLib) => { - // result.push(path.normalize(path.join(toolsPath, "cores", coreLib))); - // }); - // } - // } - // return result; - - // /hardware// -> /tools - const toolPath = path.join(toolsPath, "..", "..", "..", "tools"); - if (fs.existsSync(toolPath)) { - result.push(path.normalize(path.join(toolPath, "**"))); - } - return result; - } - - public getDefaultForcedIncludeFiles(): string[] { - const result = []; - const boardDescriptor = this._boardManager.currentBoard; - if (!boardDescriptor) { - return result; - } - const arduinoHeadFilePath = path.normalize(path.join(boardDescriptor.platform.rootBoardPath, "cores", "arduino", "Arduino.h")); - if (fs.existsSync(arduinoHeadFilePath)) { - result.push(arduinoHeadFilePath); - } - return result; - } - public openExample(example) { function tmpName(name) { let counter = 0; @@ -647,6 +349,7 @@ Please make sure the folder is not occupied by other procedures .`); const dc = DeviceContext.getInstance(); const arduinoJson = { sketch: sketchFile, + // TODO EW, 2020-02-18: COM1 is Windows specific - what about OSX and Linux users? port: dc.port || "COM1", board: dc.board, configuration: dc.configuration, @@ -654,43 +357,6 @@ Please make sure the folder is not occupied by other procedures .`); const arduinoConfigFilePath = path.join(destExample, constants.ARDUINO_CONFIG_FILE); util.mkdirRecursivelySync(path.dirname(arduinoConfigFilePath)); fs.writeFileSync(arduinoConfigFilePath, JSON.stringify(arduinoJson, null, 4)); - - // Generate cpptools intellisense config - const cppConfigFilePath = path.join(destExample, constants.CPP_CONFIG_FILE); - - // Current workspace - let includePath = ["${workspaceRoot}"]; - // Defaut package for this board - const defaultPackageLibPaths = this.getDefaultPackageLibPaths(); - includePath = includePath.concat(defaultPackageLibPaths); - // Arduino built-in package tools - includePath.push(path.join(this._settings.arduinoPath, "hardware", "tools", "**")); - // Arduino built-in libraries - includePath.push(path.join(this._settings.arduinoPath, "libraries", "**")); - // Arduino custom package tools - includePath.push(path.join(this._settings.sketchbookPath, "hardware", "tools", "**")); - // Arduino custom libraries - includePath.push(path.join(this._settings.sketchbookPath, "libraries", "**")); - - const forcedInclude = this.getDefaultForcedIncludeFiles(); - - const defines = [ - "ARDUINO=10800", - ]; - const cppConfig = { - configurations: [{ - name: util.getCppConfigPlatform(), - defines, - includePath, - forcedInclude, - intelliSenseMode: "clang-x64", - cStandard: "c11", - cppStandard: "c++17", - }], - version: 3, - }; - util.mkdirRecursivelySync(path.dirname(cppConfigFilePath)); - fs.writeFileSync(cppConfigFilePath, JSON.stringify(cppConfig, null, 4)); } // Step 3: Open the arduino project at a new vscode window. @@ -735,29 +401,272 @@ Please make sure the folder is not occupied by other procedures .`); this._programmerManager = value; } - private getProgrammerString(): string { - const selectProgrammer = this.programmerManager.currentProgrammer; - if (!selectProgrammer) { - Logger.notifyUserError("getProgrammerString", new Error(constants.messages.NO_PROGRAMMMER_SELECTED)); - return; + /** + * Runs the pre or post build command. + * Usually before one of + * * verify + * * upload + * * upload using programmer + * @param dc Device context prepared during one of the above actions + * @param what "pre" if the pre-build command should be run, "post" if the + * post-build command should be run. + * @returns True if successful, false on error. + */ + protected async runPrePostBuildCommand(dc: DeviceContext, + environment: any, + what: "pre" | "post"): Promise { + const cmdline = what === "pre" + ? dc.prebuild + : dc.postbuild; + + if (cmdline) { + arduinoChannel.info(`Running ${what}-build command: "${cmdline}"`); + let cmd: string; + let args: string[]; + // pre-/post-build commands feature full bash support on UNIX systems. + // On Windows you have full cmd support. + if (os.platform() === "win32") { + args = []; + cmd = cmdline; + } else { + args = ["-c", cmdline]; + cmd = "bash"; + } + try { + await util.spawn(cmd, + args, + { + shell: os.platform() === "win32", + cwd: ArduinoWorkspace.rootPath, + env: {...environment}, + }, + { channel: arduinoChannel.channel }); + } catch (ex) { + const msg = ex.error + ? `${ex.error}` + : ex.code + ? `Exit code = ${ex.code}` + : JSON.stringify(ex); + arduinoChannel.error(`Running ${what}-build command failed: ${os.EOL}${msg}`); + return false; + } } - return selectProgrammer; + return true; } - private getBoardBuildString(): string { - const selectedBoard = this.boardManager.currentBoard; - if (!selectedBoard) { - Logger.notifyUserError("getBoardBuildString", new Error(constants.messages.NO_BOARD_SELECTED)); - return; + /** + * Private implementation. Not to be called directly. The wrapper build() + * manages the build state. + * @param mode See build() + * @param buildDir See build() + * @see https://github.com/arduino/Arduino/blob/master/build/shared/manpage.adoc + */ + private async _build(mode: BuildMode, buildDir?: string): Promise { + const dc = DeviceContext.getInstance(); + const args: string[] = []; + let restoreSerialMonitor: boolean = false; + const verbose = VscodeSettings.getInstance().logLevel === constants.LogLevel.Verbose; + + if (!this.boardManager.currentBoard) { + if (mode !== BuildMode.Analyze) { + logger.notifyUserError("boardManager.currentBoard", new Error(constants.messages.NO_BOARD_SELECTED)); + } + return false; } - return selectedBoard.getBuildConfig(); - } + const boardDescriptor = this.boardManager.currentBoard.getBuildConfig(); - private async getMainSketch(dc: DeviceContext) { - await dc.resolveMainSketch(); - if (!dc.sketch) { - vscode.window.showErrorMessage("No sketch file was found. Please specify the sketch in the arduino.json file"); - throw new Error("No sketch file was found."); + args.push("--board", boardDescriptor); + + if (!ArduinoWorkspace.rootPath) { + vscode.window.showWarningMessage("Workspace doesn't seem to have a folder added to it yet."); + return false; + } + + if (!dc.sketch || !util.fileExistsSync(path.join(ArduinoWorkspace.rootPath, dc.sketch))) { + if (mode === BuildMode.Analyze) { + // Analyze runs non interactively + return false; + } + if (!await dc.resolveMainSketch()) { + vscode.window.showErrorMessage("No sketch file was found. Please specify the sketch in the arduino.json file"); + return false; + } + } + + const selectSerial = async () => { + const choice = await vscode.window.showInformationMessage( + "Serial port is not specified. Do you want to select a serial port for uploading?", + "Yes", "No"); + if (choice === "Yes") { + vscode.commands.executeCommand("arduino.selectSerialPort"); + } + } + + if (mode === BuildMode.Upload) { + if ((!dc.configuration || !/upload_method=[^=,]*st[^,]*link/i.test(dc.configuration)) && !dc.port) { + await selectSerial(); + return false; + } + args.push("--upload"); + if (dc.port) { + args.push("--port", dc.port); + } + } else if (mode === BuildMode.UploadProgrammer) { + const programmer = this.programmerManager.currentProgrammer; + if (!programmer) { + logger.notifyUserError("getProgrammerString", new Error(constants.messages.NO_PROGRAMMMER_SELECTED)); + return false; + } + if (!dc.port) { + await selectSerial(); + return false; + } + args.push("--upload", + "--port", dc.port, + "--useprogrammer", + "--pref", `programmer=${programmer}`); + } else { + args.push("--verify"); + } + + if (dc.buildPreferences) { + for (const pref of dc.buildPreferences) { + // Note: BuildPrefSetting makes sure that each preference + // value consists of exactly two items (key and value). + args.push("--pref", `${pref[0]}=${pref[1]}`); + } } + + // We always build verbosely but filter the output based on the settings + args.push("--verbose-build"); + if (verbose) { + args.push("--verbose-upload"); + } + + await vscode.workspace.saveAll(false); + + // we prepare the channel here since all following code will + // or at leas can possibly output to it + arduinoChannel.show(); + arduinoChannel.start(`${mode} sketch '${dc.sketch}'`); + + if (buildDir || dc.output) { + // 2020-02-29, EW: This whole code appears a bit wonky to me. + // What if the user specifies an output directory "../builds/my project" + buildDir = path.resolve(ArduinoWorkspace.rootPath, buildDir || dc.output); + const dirPath = path.dirname(buildDir); + if (!util.directoryExistsSync(dirPath)) { + logger.notifyUserError("InvalidOutPutPath", new Error(constants.messages.INVALID_OUTPUT_PATH + buildDir)); + return false; + } + args.push("--pref", `build.path=${buildDir}`); + arduinoChannel.info(`Please see the build logs in output path: ${buildDir}`); + } else { + const msg = "Output path is not specified. Unable to reuse previously compiled files. Build will be slower. See README."; + arduinoChannel.warning(msg); + } + + // Environment variables passed to pre- and post-build commands + const env = { + VSCA_BUILD_MODE: mode, + VSCA_SKETCH: dc.sketch, + VSCA_BOARD: boardDescriptor, + VSCA_WORKSPACE_DIR: ArduinoWorkspace.rootPath, + VSCA_LOG_LEVEL: verbose ? constants.LogLevel.Verbose : constants.LogLevel.Info, + }; + if (dc.port) { + env["VSCA_SERIAL"] = dc.port; + } + if (buildDir) { + env["VSCA_BUILD_DIR"] = buildDir; + } + + // TODO EW: What should we do with pre-/post build commands when running + // analysis? Some could use it to generate/manipulate code which could + // be a prerequisite for a successful build + if (!await this.runPrePostBuildCommand(dc, env, "pre")) { + return false; + } + + // stop serial monitor when everything is prepared and good + // what makes restoring of its previous state easier + if (mode === BuildMode.Upload || mode === BuildMode.UploadProgrammer) { + restoreSerialMonitor = await SerialMonitor.getInstance().closeSerialMonitor(dc.port); + UsbDetector.getInstance().pauseListening(); + } + + // Push sketch as last argument + args.push(path.join(ArduinoWorkspace.rootPath, dc.sketch)); + + const cocopa = makeCompilerParserContext(dc); + + const cleanup = async (result: "ok" | "error") => { + let ret = true; + if (result === "ok") { + ret = await this.runPrePostBuildCommand(dc, env, "post"); + } + await cocopa.conclude(); + if (mode === BuildMode.Upload || mode === BuildMode.UploadProgrammer) { + UsbDetector.getInstance().resumeListening(); + if (restoreSerialMonitor) { + await SerialMonitor.getInstance().openSerialMonitor(); + } + } + return ret; + } + const stdoutcb = (line: string) => { + if (cocopa.callback) { + cocopa.callback(line); + } + if (verbose) { + arduinoChannel.channel.append(line); + } + } + const stderrcb = (line: string) => { + if (os.platform() === "win32") { + line = line.trim(); + if (line.length <= 0) { + return; + } + line = line.replace(/(?:\r|\r\n|\n)+/g, os.EOL); + line = `${line}${os.EOL}`; + } + if (!verbose) { + // Don't spill log with spurious info from the backend. This + // list could be fetched from a config file to accommodate + // messages of unknown board packages, newer backend revisions + const filters = [ + /^Picked\sup\sJAVA_TOOL_OPTIONS:\s+/, + /^\d+\d+-\d+-\d+T\d+:\d+:\d+.\d+Z\s(?:INFO|WARN)\s/, + /^(?:DEBUG|TRACE|INFO)\s+/, + ]; + for (const f of filters) { + if (line.match(f)) { + return; + } + } + } + arduinoChannel.channel.append(line); + } + + return await util.spawn( + this._settings.commandPath, + args, + undefined, + { stdout: stdoutcb, stderr: stderrcb }, + ).then(async () => { + const ret = await cleanup("ok"); + if (ret) { + arduinoChannel.end(`${mode} sketch '${dc.sketch}'${os.EOL}`); + } + return ret; + }, async (reason) => { + await cleanup("error"); + const msg = reason.code + ? `Exit with code=${reason.code}` + : JSON.stringify(reason); + arduinoChannel.error(`${mode} sketch '${dc.sketch}': ${msg}${os.EOL}`); + return false; + }); } } diff --git a/src/arduino/arduinoContentProvider.ts b/src/arduino/arduinoContentProvider.ts index 6c893eb2..e3dcce7f 100644 --- a/src/arduino/arduinoContentProvider.ts +++ b/src/arduino/arduinoContentProvider.ts @@ -8,20 +8,17 @@ import ArduinoActivator from "../arduinoActivator"; import ArduinoContext from "../arduinoContext"; import * as Constants from "../common/constants"; import * as JSONHelper from "../common/cycle"; +import { DeviceContext } from "../deviceContext"; import * as Logger from "../logger/logger"; import LocalWebServer from "./localWebServer"; -import { VscodeSettings } from "./vscodeSettings"; export class ArduinoContentProvider implements vscode.TextDocumentContentProvider { private _webserver: LocalWebServer; private _onDidChange = new vscode.EventEmitter(); - constructor( - private _extensionPath: string) { - this.initialize(); - } + constructor(private _extensionPath: string) { } - public initialize() { + public async initialize() { this._webserver = new LocalWebServer(this._extensionPath); // Arduino Boards Manager this.addHandlerWithLogger("show-boardmanager", "/boardmanager", (req, res) => this.getHtmlView(req, res)); @@ -50,7 +47,7 @@ export class ArduinoContentProvider implements vscode.TextDocumentContentProvide this.addHandlerWithLogger("load-examples", "/api/examples", async (req, res) => await this.getExamples(req, res)); this.addHandlerWithLogger("open-example", "/api/openexample", (req, res) => this.openExample(req, res), true); - this._webserver.start(); + await this._webserver.start(); } public async provideTextDocumentContent(uri: vscode.Uri): Promise { @@ -207,7 +204,6 @@ export class ArduinoContentProvider implements vscode.TextDocumentContentProvide return res.status(400).send("BAD Request! Missing { libraryPath } parameters!"); } else { try { - await ArduinoContext.arduinoApp.addLibPath(req.body.libraryPath); await ArduinoContext.arduinoApp.includeLibrary(req.body.libraryPath); return res.json({ status: "OK", @@ -262,6 +258,8 @@ export class ArduinoContentProvider implements vscode.TextDocumentContentProvide } else { try { ArduinoContext.boardManager.currentBoard.updateConfig(req.body.configId, req.body.optionId); + const dc = DeviceContext.getInstance(); + dc.configuration = ArduinoContext.boardManager.currentBoard.customConfig; return res.json({ status: "OK", }); diff --git a/src/arduino/board.ts b/src/arduino/board.ts index 39908022..294ce7cf 100644 --- a/src/arduino/board.ts +++ b/src/arduino/board.ts @@ -1,14 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -import { DeviceContext } from "../deviceContext"; -import { IBoard, IBoardConfigItem, IPlatform } from "./package"; +import { BoardConfigResult, IBoard, IBoardConfigItem, IPlatform } from "./package"; export function parseBoardDescriptor(boardDescriptor: string, plat: IPlatform): Map { const boardLineRegex = /([^\.]+)\.(\S+)=(.+)/; const result = new Map(); - const lines = boardDescriptor.split(/[\r|\r\n|\n]/); + const lines = boardDescriptor.split(/(?:\r|\r\n|\n)/); const menuMap = new Map(); lines.forEach((line) => { @@ -18,7 +17,7 @@ export function parseBoardDescriptor(boardDescriptor: string, plat: IPlatform): } const match = boardLineRegex.exec(line); - if (match && match.length > 3) { + if (match) { if (line.startsWith("menu.")) { menuMap.set(match[2], match[3]); return; @@ -52,15 +51,15 @@ export class Board implements IBoard { this._configItems = []; } - public get board(): string { + public get board() { return this._board; } - public get platform(): IPlatform { + public get platform() { return this._platform; } - public addParameter(key: string, value: string): void { + public addParameter(key: string, value: string) { const match = key.match(MENU_REGEX); if (match) { const existingItem = this._configItems.find((item) => item.id === match[1]); @@ -82,8 +81,7 @@ export class Board implements IBoard { } } } - - public getBuildConfig(): string { + public getBuildConfig() { return `${this.getPackageName()}:${this.platform.architecture}:${this.board}${this.customConfig ? ":" + this.customConfig : ""}`; } @@ -100,36 +98,84 @@ export class Board implements IBoard { } } - public get configItems(): IBoardConfigItem[] { + public get configItems() { return this._configItems; } - public loadConfig(configString: string): void { + public loadConfig(configString: string) { + // An empty or undefined config string resets the configuration + if (!configString) { + this.resetConfig(); + return BoardConfigResult.Success; + } const configSections = configString.split(","); const keyValueRegex = /(\S+)=(\S+)/; - configSections.forEach((configSection) => { - const match = configSection.match(keyValueRegex); - if (match && match.length >= 2) { - this.updateConfig(match[1], match[2]); + let result = BoardConfigResult.Success; + for (const section of configSections) { + const match = section.match(keyValueRegex); + if (!match) { + return BoardConfigResult.InvalidFormat; + } + const r = this.updateConfig(match[1], match[2]); + switch (r) { + case BoardConfigResult.SuccessNoChange: + result = r; + break; + case BoardConfigResult.Success: + break; + default: + return r; } - }); + }; + return result; } - public updateConfig(configId: string, optionId: string): boolean { + /** + * For documentation see the documentation on IBoard.updateConfig(). + */ + public updateConfig(configId: string, optionId: string) { const targetConfig = this._configItems.find((config) => config.id === configId); if (!targetConfig) { - return false; + return BoardConfigResult.InvalidConfigID; + } + // Iterate through all options and ... + for (const o of targetConfig.options) { + // Make sure that we only set valid options, e.g. when loading + // from config files. + if (o.id === optionId) { + if (targetConfig.selectedOption !== optionId) { + targetConfig.selectedOption = optionId; + return BoardConfigResult.Success; + } + return BoardConfigResult.SuccessNoChange; + } } - if (targetConfig.selectedOption !== optionId) { - targetConfig.selectedOption = optionId; - const dc = DeviceContext.getInstance(); - dc.configuration = this.customConfig; - return true; + return BoardConfigResult.InvalidOptionID; + } + + public resetConfig() { + for (const c of this._configItems) { + c.selectedOption = c.options[0].id; } - return false; } public getPackageName() { return this.platform.packageName ? this.platform.packageName : this.platform.package.name; } } + +/** + * Test if two boards are of the same type, i.e. have the same key. + * @param {IBoard | undefined} a A board. + * @param {IBoard | undefined} b And another board. + * @returns {boolean} true if two boards are of the same type, else false. + */ +export function boardEqual(a: IBoard | undefined, + b: IBoard | undefined) { + if (a && b) { + return a.key === b.key; + } else if (a || b) { + return false; + } + return true; +} diff --git a/src/arduino/boardManager.ts b/src/arduino/boardManager.ts index d8267afa..a2f6d481 100644 --- a/src/arduino/boardManager.ts +++ b/src/arduino/boardManager.ts @@ -12,8 +12,8 @@ import { arduinoChannel } from "../common/outputChannel"; import { DeviceContext } from "../deviceContext"; import { ArduinoApp } from "./arduino"; import { IArduinoSettings } from "./arduinoSettings"; -import { parseBoardDescriptor } from "./board"; -import { IBoard, IPackage, IPlatform } from "./package"; +import { boardEqual, parseBoardDescriptor } from "./board"; +import { BoardConfigResult, IBoard, IPackage, IPlatform } from "./package"; import { VscodeSettings } from "./vscodeSettings"; export class BoardManager { @@ -67,15 +67,17 @@ export class BoardManager { // Load default platforms from arduino installation directory and user manually installed platforms. this.loadInstalledPlatforms(); - // Load all supported boards type. + // Load all supported board types this.loadInstalledBoards(); - this.updateStatusBar(); - this._boardConfigStatusBar.show(); const dc = DeviceContext.getInstance(); - dc.onDidChange(() => { - this.updateStatusBar(); - }); + dc.onChangeBoard(() => this.onDeviceContextBoardChange()); + dc.onChangeConfiguration(() => this.onDeviceContextConfigurationChange()); + + // load initial board from DeviceContext by emulating + // a board change event. + this.onDeviceContextBoardChange(); + this.updateStatusBar(true); } public async changeBoardType() { @@ -119,13 +121,21 @@ export class BoardManager { public doChangeBoardType(targetBoard: IBoard) { const dc = DeviceContext.getInstance(); - dc.board = targetBoard.key; - this._currentBoard = targetBoard; - dc.configuration = this._currentBoard.customConfig; - this._boardConfigStatusBar.text = targetBoard.name; - this._arduinoApp.addLibPath(null); - this._onBoardTypeChanged.fire(); + if (dc.board === targetBoard.key) { + return; + } + + // Resetting the board first that we don't overwrite the configuration + // of the previous board. + this._currentBoard = null; + // This will cause a configuration changed event which will have no + // effect because no current board is set. + dc.configuration = targetBoard.customConfig; + // This will generate a device context board event which will set the + // correct board and configuration. We know that it will trigger - we + // made sure above that the boards actually differ + dc.board = targetBoard.key; } public get packages(): IPackage[] { @@ -247,19 +257,12 @@ export class BoardManager { } } - public updateStatusBar(show: boolean = true): void { + private updateStatusBar(show: boolean = true): void { if (show) { this._boardConfigStatusBar.show(); - const dc = DeviceContext.getInstance(); - const selectedBoard = this._boards.get(dc.board); - if (selectedBoard) { - this._currentBoard = selectedBoard; - this._boardConfigStatusBar.text = selectedBoard.name; - if (dc.configuration) { - this._currentBoard.loadConfig(dc.configuration); - } + if (this._currentBoard) { + this._boardConfigStatusBar.text = this._currentBoard.name; } else { - this._currentBoard = null; this._boardConfigStatusBar.text = "