├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── SERVICES.md ├── keymaps └── atom-haxe.cson ├── lib ├── build │ └── build.js ├── completion │ ├── debug.js │ ├── file.js │ ├── provider.js │ ├── query.js │ └── server.js ├── haxe-call.js ├── haxe-config.js ├── haxe-state.js ├── haxe.js ├── linting │ └── lint.js ├── parsing │ ├── compiler.js │ └── signatures.js ├── reflect │ └── declaration.js └── utils │ ├── escape-html.js │ ├── exec.js │ ├── extend.js │ ├── haxe-code.js │ ├── hxml.js │ ├── log.js │ └── uuid.js ├── menus └── atom-haxe.cson ├── package.json ├── settings └── haxe.cson └── styles └── atom-haxe.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | npm-debug.log 4 | node_modules 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.9.0 - Dev 2 | 3 | * Working on shared haxe code implementation (see readme Future Plans - [tides](https://github.com/snowkit/tides)) 4 | 5 | ## 0.8.14 6 | 7 | * Handle both source.haxe and source.hx extensions for compatility with unfixed/fixed language-haxe plugins (see [jeremyfa/language-haxe#3](https://github.com/jeremyfa/language-haxe/issues/3#issuecomment-214175415)) 8 | 9 | ## 0.8.13 10 | 11 | * Add save all files on build option, thanks @DjPale 12 | 13 | ## 0.8.12 14 | 15 | * Improve El Capitan default PATH fix 16 | 17 | ## 0.8.11 18 | 19 | * Display completion return type on the right. Add an option to put it on the left 20 | * Minor fixes on toplevel completion display 21 | * Optimize and fix haxe server not running after closing the window that started it 22 | * Display haxe language snippets in addition to the compiler completion 23 | * Fix code completion failing after `'\''` 24 | * Fix decreased indent on `case` and `default` [](https://github.com/snowkit/atom-haxe/pull/53) 25 | 26 | ## 0.8.10 27 | 28 | * Fix missing path_set to true 29 | 30 | ## 0.8.9 31 | 32 | * Minor fixes for El Capitan path fix 33 | 34 | ## 0.8.8 35 | 36 | * Fix El Capitan default PATH for the immediate term 37 | * Completion fixes and tweaks to display 38 | 39 | ## 0.8.7 40 | 41 | * Fix context commands pointing at flow by mistake 42 | 43 | ## 0.8.6 44 | 45 | * Fix context menus not working with Atom 1.0.7 46 | * Add context menu to hxml files as well 47 | 48 | ## 0.8.5 49 | 50 | * Improve code completion: disable it in various unexpected cases (fixes `#38`) 51 | * Ensure type hints are compatible with the latest autocomplete-plus package (version `2.19.0`) 52 | 53 | ## 0.8.4 54 | 55 | * Fix linter handling of multiline errors 56 | 57 | ## 0.8.3 58 | 59 | * Fix empty buffer causing errors `#33` 60 | * Ensure custom haxe path is used `#37` and logged 61 | 62 | ## 0.8.2 63 | 64 | * Fix linter deprecations `#32` 65 | * Implement project wide linting from new Linter API 66 | 67 | ## 0.8.1 68 | 69 | * Add SERVICES.md documentation 70 | * Update changelogs 71 | 72 | ## 0.8.0 73 | 74 | * completion; Fix truncated type hint and return type on newer autocomplete-plus version 75 | * completion; Exclude haxe completion providers with priority lower than 2 instead of 1 76 | * NOTE: The version bump from 0.6.2 => 0.8.0 was an error, trying to correct it created unavailable downloads from the atom API so it was pushed up one to continue working. 77 | 78 | ## 0.6.1 79 | 80 | * tidy up logging from build consumers, and so on to debug logging system/flag 81 | * hxml state parsing handles spaces and parses more smartly to avoid problems from macro calls etc 82 | * completion: various parsing improvements 83 | * go to definition : various improvements and additions 84 | 85 | ## 0.6.0 86 | 87 | * Update readme requirements to mention Haxe requirement 88 | * Add initial go to definition (requires haxe 3.2.0+) 89 | * Add initial top level completion (requires haxe 3.2.0+) 90 | * fix line-comment command putting `/* */` instead of `//` 91 | * fixes to tmp_path experimental option 92 | 93 | ## 0.5.1~0.5.3 94 | 95 | * minor clean ups 96 | 97 | ## 0.5.0 98 | 99 | * ensure completion is more stable with autocomplete-plus `2.6.0` 100 | * many fixes in type parsing and display 101 | * many fixes in experimental tmp file option 102 | * fix byte offset with unicode 103 | * lint shares code completion cache 104 | * refactor for separating the future shared code 105 | 106 | ## 0.4.0 ~ 0.4.3 107 | 108 | * add all dependencies to dependency check 109 | * show more info on type hints 110 | * more robust completion state detection 111 | * add consumer build workflow 112 | * fix bugs about tree view assumptions 113 | * implement build system 114 | * added unset project option 115 | * make menus useful 116 | * fix bug in hxml formatting 117 | * completion improvements (filter suggestions, type hints, etc) 118 | * port type parsing to new package 119 | * linting fixes 120 | 121 | 122 | ## 0.3.0 123 | 124 | * implemented consumer state handling 125 | - active consumer will be notified when it's being made inactive 126 | - prevents clobbering of state from multiple consumers 127 | * implemented error linting 128 | * implemented dependency check with notifications 129 | - package will abort until installed 130 | - restart after installing if needed 131 | * implemented prelim completion 132 | - still very wip, but just about usable 133 | - completes every char typed atm 134 | * fix scrolling on completion debug views 135 | * fix deactivation errors 136 | - resolves server being weird 137 | - resolves cleanup 138 | 139 | ## 0.1.0 ~ 0.2.0 140 | 141 | * Renamed to haxe instead of atom-haxe 142 | * Initial completion implementation 143 | * Initial completion service provider 144 | * Initial structure implemented 145 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 snõwkit atom-haxe contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atom-haxe 2 | atom.io haxe plugin, includes completion, building, error checking and more. 3 | 4 | ## Active development 5 | Please note that this package is in active development, you're bound to find 6 | some rough edges and pain points. Issues, feedback and suggestions are welcome. 7 | 8 | A full rewrite in Haxe language is in progress on [haxe-rewrite](https://github.com/snowkit/atom-haxe/tree/haxe-rewrite) branch (only compatible with Haxe 3.3+). 9 | 10 | Thanks! 11 | 12 | ## Goals 13 | 14 | - Provide a definitive plugin for all Haxe features 15 | - No library/framework specific code but 16 | - Flexible to libraries/frameworks (offering completion + build provider) 17 | 18 | ## Requirements 19 | 20 | **Haxe** is required, and can be installed from http://haxe.org 21 | 22 | By default, the plugin will use the `haxe` executable from your path/environment, 23 | this can be configured in the settings, but is not usually required. 24 | 25 | **Required Atom Packages** 26 | 27 | Install these either from Atom -> Settings -> Install, or run `apm install [package-name]` on your CLI if you have atom shell commands installed. 28 | 29 | - requires `linter` package 30 | - requires `language-haxe` package 31 | - requires `autocomplete-plus` package (already included in Atom by default) 32 | 33 | ## Usage 34 | 35 | This usage applies only if using hxml only build workflow. 36 | If you are using a package that handles this for you (like [flow](https://github.com/snowkit/atom-flow/)) then 37 | this does not apply, and you should read the documentation for that package. 38 | 39 | - Right click a HXML file in the tree view 40 | - Set as active HXML file 41 | 42 | This will activate the hxml file, and use it for completion + builds. 43 | You can unset the project state from the Packages -> Haxe menu. 44 | 45 | **completion** 46 | Completion happens as you type. 47 | For now, you might add "dot files" to Settings -> ignored Names, 48 | For example adding `.*` would ignore the .tmp file generated for 49 | completion. We are working on a more flexible solution. 50 | 51 | 52 | **linting** 53 | Linting only happens when you save the file. 54 | 55 | ## Issues / feedback 56 | 57 | Please file issues at https://github.com/snowkit/atom-haxe ! 58 | 59 | ## Features 60 | 61 | #### code completion 62 | ![completion](http://i.imgur.com/OzN25ii.gif) 63 | ![typedefcompletion](http://i.imgur.com/7kDqcID.gif) 64 | 65 | #### code linting 66 | ![linting](http://i.imgur.com/okGD6Ue.gif) 67 | 68 | #### build workflow 69 | ![building](http://i.imgur.com/3Ldo6hJ.gif) 70 | 71 | 72 | **future features** 73 | 74 | - code / doc hinting 75 | 76 | ## Troubleshooting 77 | 78 | Use the Packages -> Haxe -> Menu options to open various debug views. 79 | 80 | **general** 81 | - Enable debug logging in the settings 82 | - Toggle log view 83 | 84 | **completion debugging** 85 | - Toggle Completion Debug 86 | - The top area shows the queries to the completion cache server 87 | - The bottom area shows the server process log 88 | 89 | 90 | ## Future plans 91 | 92 | Currently the shared IDE code is in progress. [Visit the tides repo for info.](https://github.com/snowkit/tides) 93 | This repo will migrate to being written in Haxe and using tides, in a branch. When it's ready for testers and usage we'll let you know! 94 | -------------------------------------------------------------------------------- /SERVICES.md: -------------------------------------------------------------------------------- 1 | ## Haxe package services 2 | 3 | For implementing framework specific build and completion workflow. 4 | 5 | --- 6 | 7 | This package is a central package for Haxe user packages to share the implementation of code completion, build workflow and other features. 8 | This means that in order for a framework to be supported, it must be implemented in its own package - and consume the services implemented by this package. 9 | 10 | You can read about the Atom services here : http://blog.atom.io/2015/03/25/new-services-API.html 11 | 12 | #### Configuring your package as a service consumer 13 | 14 | Inside your `package.json` add a `consumedServices` node, which tells your package two things. 15 | One is _which function to call when the service is consumed_ and the other is the exact version of the service to use. 16 | 17 | Currently the only service endpoint to implement is called `haxe-completion.provider` and works by handing you a reference to the haxe package endpoint. In other words, it gives you an object with a function available to call, to configure the packages. This may change in the coming updates due to Atom API stabilizing, as this service was created before that. 18 | 19 | ``` 20 | "consumedServices": { 21 | "haxe-completion.provider": { 22 | "versions": { 23 | "1.0.0": "completion_hook" 24 | } 25 | } 26 | } 27 | ``` 28 | 29 | In this consumer, the service will look for a function inside your _main_ package code called `completion_hook`. 30 | 31 | #### Implement the callback function in your package main code 32 | 33 | Your main package is defined in the `package.json` already, as `"main": "./lib/main.js"` or whatever you want it to be. 34 | The function endpoint for the services go inside of this file, at the root module level. 35 | 36 | Since currently the service will pass you a reference to the Haxe service itself, you would hang onto that reference and call functions on it. 37 | 38 | An example implementation would look like this: 39 | 40 | ``` 41 | //Haxe package services consumer 42 | 43 | completion_hook: function(haxe_package) { 44 | this.haxe_package = haxe_package; 45 | } 46 | 47 | ``` 48 | 49 | Now you can make calls to the Haxe service directly, via `this.haxe_package.somethingHere()`. 50 | As mentioned above, this is likely to change in an upcoming version due to the services API being finalized. 51 | 52 | #### The Haxe package services only one active consumer at a time 53 | 54 | Because of the way packages are singular, and would contend each other for the same resource, a single consumer is _set as current_ in the Haxe package at a time. 55 | 56 | For example, if you're working with just haxe and a hxml file, that is the default consumer. If you then wanted to use a framework specific project (like flow or openfl or nme) you would set the active consumer to the package in use, each time you switch. 57 | 58 | **Setting the active consumer via the service** 59 | 60 | To change the consumer to your plugin you call the `set_completion_consumer` function on the Haxe package. 61 | Note that this should only be done via user interaction - Do not set the consumer without the user intent. 62 | The Haxe package will remember the last active consumer and it will retain it's active state across sessions. 63 | 64 | The argument for the completion consumer function is a single object, with the following properties: 65 | 66 | - `name` 67 | - Required. 68 | - The name of the consumer. 69 | - `hxml_content` 70 | - Required. 71 | - Populate this with the HXML content for your project. 72 | - `hxml_cwd` 73 | - Required. 74 | - The current working directory for the hxml content. 75 | - `onConsumerLost` 76 | - Required. 77 | - The callback to notify you when the consumer was switched to another. 78 | - Disable features when not the active consumer to avoid conflicts when possible 79 | - `does_build` 80 | - If your package handles the build, not relying on the Haxe default hxml build, set this to true. 81 | - `onRunBuild` 82 | - The callback to notify you a build was triggered by the user. 83 | - `onBuildSelectorQuery` 84 | - The callback to query _selectors_ that allow a build to run 85 | - These are additional to `source.haxe, source.hx` which is the default 86 | - Example: `return ['source.json', 'source.xml']` would allow builds to run from these file types 87 | - This is a list of atom _selectors_ to trigger the build command 88 | - If the selector doesn't match these and the default, the build command will be ignored 89 | 90 | An example below shows the completion consumer being configured for a package to handle the endpoints where appropriate. 91 | As you can see above the process is very simple - and is all that is required at the moment. 92 | 93 | ``` 94 | this.haxe_package.set_completion_consumer({ 95 | name: 'mypackage', 96 | hxml_cwd: this.project_dir, 97 | hxml_content: this.project_hxml, 98 | does_build: true, 99 | onRunBuild: this.on_runbuild.bind(this), 100 | onBuildSelectorQuery: this.on_buildselectorquery.bind(this), 101 | onConsumerLost: this.on_consumerlost.bind(this), 102 | }); 103 | ``` 104 | 105 | Once the hxml data is correctly configured, all the haxe features should work as intended, because the haxe compiler is used directly. 106 | 107 | #### Updating the hxml once set 108 | 109 | Every time your package has updated hxml data and directory to run from, it should call this function again with updated information. 110 | This would happen for example if they updated their project file, and the hxml was generated. 111 | 112 | ## Example implementation 113 | 114 | The [flow](http://snowkit.org/flow) package implements the consumer model and build workflow of its own. It can be viewed in full at the [atom-flow](https://github.com/snowkit/atom-flow) repository for reference. 115 | 116 | ## Important Notes 117 | 118 | There is often an "Atom way", and it's a good idea to be good to the users. 119 | The same is probably true for framework specifics, try to respect the user workflow. 120 | 121 | On top of that, the following guidelines are suggested: 122 | 123 | - **Suggestions and feedback welcome** 124 | - As this is a community package to serve the Haxe users in the best way possible, we definitely welcome input. 125 | - **Pay attention to the atom community packages** 126 | - There are many great libraries and packages already available for use with atom, so there is a lot of strong, consistently maintained work already done that you shouldn't redo unless absolutely necessary. Often times their work is that good, that it becomes a core package for Atom itself. Try not to cobble together things that have already been done in a way true to the Atom user space. 127 | - **Keep an eye on version releases of this and related packages** 128 | - As the readme states the active development of the atom-haxe plugin and Future Plans section describe things are coming that may shake up the service API slightly. We will notify packages we are aware of when they land in master and are released. 129 | - **Use the atom reporting features** 130 | - They are great, and ensure everyone using the packages have a consistent way to get problems addressed and in front of the correct eyes. Encourage users to use that when problems arise. 131 | - **Don't create monolith packages.** 132 | - Create one package for a specific framework, and have it do ONLY the work needed for that package and no more. A package should only ever cater to one framework and do that well. 133 | - **Don't duplicate functionality.** 134 | - Inside your framework package, don't recreate the Haxe (or other) package features. For example hxml only builds are handled by this package. Don't implement that inside your consumer, because the default consumer already has this feature. This allows every Haxe user using the Atom packages to share the same reliable workflow and consistency, and allows the community to adapt quickly by maintaining a single package. 135 | 136 | 137 | ## Conclusion 138 | 139 | Don't forget to browse the [Atom documentation](https://atom.io/docs) and blogs! 140 | -------------------------------------------------------------------------------- /keymaps/atom-haxe.cson: -------------------------------------------------------------------------------- 1 | 2 | '.platform-darwin atom-workspace': 3 | 'cmd-b': 'haxe:build' 4 | 5 | 'atom-workspace': 6 | 'ctrl-b': 'haxe:build' 7 | -------------------------------------------------------------------------------- /lib/build/build.js: -------------------------------------------------------------------------------- 1 | 2 | var state = require('../haxe-state') 3 | , run = require('../haxe-call') 4 | , log = require('../utils/log') 5 | 6 | module.exports = { 7 | 8 | run_build: function() { 9 | 10 | if(state.valid) { 11 | 12 | var build_autosave = atom.config.get('haxe.build_autosave') || false; 13 | 14 | if (build_autosave) { 15 | log.debug('saving all files'); 16 | atom.workspace.saveAll(); 17 | } 18 | 19 | // - I want to handle the build 20 | // - I don't handle the build, you run it 21 | // - I don't handle the build, 22 | // but don't run haxe on the hxml 23 | if(state.consumer) { 24 | if(state.consumer.does_build) { 25 | log.debug('consumer handling build: ' + state.consumer.name); 26 | this.run_consumer_build(); 27 | } else if(!state.consumer.no_build) { 28 | log.debug('consumer build pass, run default hxml'); 29 | this.run_hxml_build(); 30 | } else { 31 | console.log('consumer configured no build'); 32 | } 33 | } else { 34 | this.run_hxml_build(); 35 | } 36 | 37 | } else { 38 | 39 | atom.notifications.addWarning('Haxe: No project state set. No build possible.'); 40 | 41 | } 42 | 43 | }, //run_build 44 | 45 | 46 | run_consumer_build: function() { 47 | 48 | if(state.consumer.onRunBuild) { 49 | state.consumer.onRunBuild({ 50 | haxe_args: state.as_args(), 51 | run: { 52 | haxe:function(){ run.haxe.apply(run, arguments); }, 53 | haxelib:function(){ run.haxelib.apply(run, arguments); } 54 | } 55 | }); 56 | } else { 57 | var info = 'Haxe: A package trying to build is misconfigured.
'; 58 | info += 'Please report this to the following package:
'; 59 | info += '- ' + state.consumer.name; 60 | console.log('haxe: misconfigured build consumer: ', state.consumer); 61 | atom.notifications.addWarning(info, {dismissable:true}); 62 | } 63 | 64 | }, //run_consumer_build 65 | 66 | run_hxml_build: function() { 67 | 68 | atom.notifications.addInfo('Haxe: Running build...'); 69 | 70 | //we can assume since we did state.valid above 71 | var args = state.as_args(); 72 | var build = run.haxe(args, this._logi, this._loge); 73 | log.info('Running build...', true) 74 | log.debug('haxe ' + args.join(' ')); 75 | build.then(function(res){ 76 | if(res.code) { 77 | atom.notifications.addWarning('Haxe: Build failed. check log.'); 78 | log.error('Build failed', false, true); 79 | } else { 80 | atom.notifications.addSuccess('Haxe: Build succeeded'); 81 | log.success('Build succeeded'); 82 | } 83 | }); 84 | 85 | }, //run_hxml_build 86 | 87 | _logi: function(s) { 88 | log.msg(s, false, true); 89 | }, // 90 | 91 | _loge: function(s) { 92 | log.error(s, false, true); 93 | }, // 94 | 95 | } //module.exports 96 | -------------------------------------------------------------------------------- /lib/completion/debug.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | module.exports = { 5 | 6 | //Lifecycle 7 | 8 | init:function() { 9 | 10 | this.element = document.createElement('div'); 11 | this.element.classList.add('haxe-completion-log'); 12 | 13 | // Element for query logs 14 | this.content_query = document.createElement('div'); 15 | this.content_query.innerHTML = "haxe / completion / query / log\n\n"; 16 | this.content_query.classList.add('haxe-completion-log-query-content'); 17 | 18 | // Element for server logs 19 | this.content_server = document.createElement('div'); 20 | this.content_server.innerHTML = "haxe / completion / server / log\n\n"; 21 | this.content_server.classList.add('haxe-completion-log-server-content'); 22 | 23 | this.element.appendChild(this.content_query); 24 | this.element.appendChild(this.content_server); 25 | 26 | this.panel = atom.workspace.addRightPanel({ 27 | visible: false, 28 | item: this.element 29 | }); 30 | 31 | }, //init 32 | 33 | dispose:function() { 34 | this.element.remove(); 35 | this.panel.destroy(); 36 | }, 37 | 38 | //Public API 39 | 40 | set:function(val) { 41 | this.content.innerHTML = val; 42 | this.scroll_to_bottom(); 43 | }, 44 | 45 | query:function(val) { 46 | val = 'haxe / completion / query / ' + val; 47 | this.content_query.textContent += this._pre(val); 48 | this.scroll_to_bottom(this.content_query); 49 | }, 50 | 51 | server:function(val) { 52 | val = 'haxe / completion / server / ' + val; 53 | this.content_server.textContent += this._pre(val); 54 | this.scroll_to_bottom(this.content_server); 55 | }, 56 | 57 | scroll_to_bottom:function(el) { 58 | setImmediate(function(){ 59 | el.scrollTop = el.scrollHeight; 60 | }); 61 | }, 62 | 63 | //Commands (hooked in main) 64 | 65 | toggle:function() { 66 | if(this.panel.isVisible()) { 67 | this.panel.hide(); 68 | } else { 69 | this.panel.show(); 70 | } 71 | }, 72 | 73 | //Internal helpers 74 | 75 | _pre:function(val) { 76 | val += '\n'; 77 | return val; 78 | }, 79 | 80 | } //exports 81 | -------------------------------------------------------------------------------- /lib/completion/file.js: -------------------------------------------------------------------------------- 1 | 2 | // lib code 3 | var state = require('../haxe-state') 4 | , uuid = require('../utils/uuid') 5 | , log = require('../utils/log') 6 | , code = require('../utils/haxe-code') 7 | // node built in 8 | , path = require('path') 9 | // dep code 10 | , fs = require('fs-extra') 11 | 12 | // Save temporary files to run haxe autocomplete server over it 13 | // and extract completion lists and types. 14 | // This allows us to use haxe to extract informations from the project without 15 | // Having to parse them ourself, and with the `more forgiving` compilation 16 | // process used by the haxe server when performing completion/display 17 | module.exports = { 18 | 19 | // Save file to get completion list of a variable of specific type 20 | // A key path (array of keys) can be added to get the completion list of a property inside the instance type. 21 | save_tmp_file_for_completion_list_of_instance_type: function(type, key_path, imports) { 22 | // Setup 23 | var key_path_str; 24 | var import_str = ''; 25 | if (key_path == null || key_path.length == 0) { 26 | key_path_str = ''; 27 | } else { 28 | key_path_str = key_path.join('.') + '.'; 29 | } 30 | 31 | if (imports != null && imports.length > 0) { 32 | for (var i = 0; i < imports.length; i++) { 33 | import_str += 'import ' + imports[i] + '; '; 34 | } 35 | } 36 | 37 | var file_contents = import_str + 'class AtomHaxeTempClass__ { public static function main():Void { var atomHaxeTempVar__:' + type + '; atomHaxeTempVar__.' + key_path_str; 38 | var file_name = 'AtomHaxeTempClass__.hx'; 39 | 40 | // Save and return result 41 | return this.save_file_contents_in_temporary_path(file_contents, file_name); 42 | }, 43 | 44 | // Save file to get completion list of package (to list sub-packages). 45 | save_tmp_file_for_completion_list_of_package: function(package_name) { 46 | 47 | // Setup 48 | var file_contents = 'import ' + package_name + '.'; 49 | var file_name = 'AtomHaxeTempImport__.hx'; 50 | 51 | // Save and return result 52 | return this.save_file_contents_in_temporary_path(file_contents, file_name); 53 | }, 54 | 55 | save_tmp_file_for_completion_of_original_file: function(original_file_path, file_contents) 56 | { 57 | // Setup 58 | var cwd = state.hxml_cwd; 59 | 60 | // Extract package from contents, generate a sub-package name and compute relative path 61 | var package_name = code.extract_package(file_contents); 62 | var new_package_name = 'atom_tempfile__'; 63 | if (package_name.length > 0) { 64 | new_package_name = package_name + '.' + new_package_name; 65 | } 66 | 67 | // Replace package in contents with a sub-package (that will still have access to the parent package) 68 | file_contents = code.replace_package(file_contents, new_package_name); 69 | 70 | var base_name = path.basename(original_file_path); 71 | var relative_file_path = path.join(new_package_name.split('.').join(path.sep), base_name); 72 | 73 | // Save and return result 74 | return this.save_file_contents_in_temporary_path(file_contents, relative_file_path); 75 | }, 76 | 77 | // Save the given file contents with the requested file name and return info to use it 78 | save_file_contents_in_temporary_path: function(file_contents, relative_file_path) { 79 | 80 | var tmp_path = state.tmp_path; 81 | 82 | var hash = uuid.v1(); 83 | 84 | // cp_path can be used to add the path to haxe server options 85 | var cp_path = path.join(tmp_path, hash); 86 | 87 | // We need to add an intermediate directory (here we name it 'haxe') 88 | // For the -cp to work fine 89 | var temporary_file_path = path.join(cp_path, relative_file_path); 90 | 91 | // Remove trailing slash on tmp_path if any 92 | if (tmp_path.charAt(tmp_path.length - 1) === path.sep) { 93 | tmp_path = tmp_path.slice(0, tmp_path.length - 1); 94 | } 95 | 96 | // Ensure this path is inside the temporary directory 97 | if (temporary_file_path.slice(0, tmp_path.length + 1) !== tmp_path + path.sep) { 98 | // No? Then let's not do something silly 99 | log.error('saving temporary file outside of tmp_path is forbidden: ' + temporary_file_path); 100 | return null; 101 | } 102 | 103 | // Perform save to disk 104 | fs.outputFileSync(temporary_file_path, file_contents, 'utf8'); 105 | 106 | // Return info 107 | return { 108 | file_path: temporary_file_path, 109 | cp_path: cp_path, 110 | contents: file_contents 111 | }; 112 | }, 113 | 114 | // Remove the temporary file related to the given file info 115 | // file_info is expected to be an object previously returned 116 | // by one the the save_* methods of this module. 117 | remove_tmp_file: function(file_info) { 118 | var to_remove; 119 | var tmp_path = state.tmp_path; 120 | 121 | // Don't try anything if tmp_path is not set 122 | if (!tmp_path) { 123 | log.error('cannot remove temporary file because tmp_path does\'t exist'); 124 | return; 125 | } 126 | 127 | // Remove trailing slash on tmp_path if any 128 | if (tmp_path.charAt(tmp_path.length - 1) === path.sep) { 129 | tmp_path = tmp_path.slice(0, tmp_path.length - 1); 130 | } 131 | 132 | // Remove the cp directory, itself inside the temporary directory 133 | to_remove = path.normalize(file_info.cp_path); 134 | 135 | if (to_remove != null) { 136 | // Ensure this path is inside the temporary directory 137 | if (to_remove.slice(0, tmp_path.length + 1) === tmp_path + path.sep) { 138 | // Remove it 139 | fs.removeSync(to_remove); 140 | } else { 141 | log.error('removing file outside of tmp_path is forbidden: ' + to_remove); 142 | } 143 | } 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /lib/completion/provider.js: -------------------------------------------------------------------------------- 1 | 2 | // node built in 3 | var path = require('path') 4 | , crypto = require('crypto') 5 | // lib code 6 | , query = require('./query') 7 | , debug = require('./debug') 8 | , file = require('./file') 9 | , state = require('../haxe-state') 10 | , signatures = require('../parsing/signatures') 11 | , escape = require('../utils/escape-html') 12 | , code = require('../utils/haxe-code') 13 | , compiler = require('../parsing/compiler') 14 | // dep code 15 | , xml2js = require('xml2js') 16 | , fs = require('fs-extra') 17 | , filter = require('fuzzaldrin').filter 18 | 19 | 20 | var REGEX_ENDS_WITH_DOT_IDENTIFIER = /\.([a-zA-Z_0-9]*)$/; 21 | var REGEX_ENDS_WITH_DOT_NUMBER = /[^a-zA-Z0-9_\]\)]([\.0-9]+)$/; 22 | var REGEX_ENDS_WITH_PARTIAL_PACKAGE_DECL = /[^a-zA-Z0-9_]package\s+([a-zA-Z_0-9]+(\.[a-zA-Z_0-9]+)*)\.([a-zA-Z_0-9]*)$/; 23 | var REGEX_BEGINS_WITH_KEY = /^([a-zA-Z0-9_]+)\s*\:/; 24 | var REGEX_ENDS_WITH_ALPHANUMERIC = /([A-Za-z0-9_]+)$/; 25 | 26 | 27 | module.exports = { 28 | 29 | selector: '.source.haxe, .source.hx', 30 | disableForSelector: '.source.haxe .comment, .source.hx .comment', 31 | inclusionPriority: 2, 32 | excludeLowerPriority: false, 33 | prefixes:['.','('], 34 | 35 | last_completion_index: null, 36 | last_key_path: null, 37 | last_has_partial_key: false, 38 | last_suggestions: null, 39 | last_suggestions_kind: null, 40 | last_pretext: null, 41 | last_mode: null, 42 | 43 | // Set to true to use extended completion 44 | // on anonymous structures as argument 45 | use_extended_completion: true, 46 | 47 | getSuggestions : function(opt) { 48 | 49 | if(!state.hxml_cwd) return []; 50 | if(!state.hxml_content && !state.hxml_file) return []; 51 | 52 | var _buffer_file = opt.editor.buffer.file; 53 | if(!_buffer_file) return []; 54 | 55 | var _file = _buffer_file.path; 56 | if(!_file) return []; 57 | 58 | return new Promise( function(resolve, reject) { 59 | 60 | var buffer_pos = opt.bufferPosition.toArray(); 61 | var pretext = opt.editor.getTextInBufferRange( [ [0,0], buffer_pos] ); 62 | var text = opt.editor.getText(); 63 | var index = pretext.length; 64 | var mode = null; 65 | 66 | var position_info = this._position_info_from_text_and_index(pretext, index); 67 | 68 | // If a new index is provided, use it 69 | if (position_info != null) { 70 | index = position_info.index; 71 | 72 | if (position_info.mode != null) { 73 | mode = position_info.mode; 74 | } 75 | 76 | //if (position_info.call != null) { 77 | // console.log(position_info.call); 78 | //} 79 | } else { 80 | // Nothing to query from the current index 81 | return resolve([]); 82 | } 83 | 84 | // Get current key_path and partial_key 85 | var key_path = null, has_partial_key = false; 86 | if (position_info != null && position_info.call != null) { 87 | key_path = position_info.call.key_path; 88 | has_partial_key = (position_info.call.partial_key != null); 89 | } 90 | 91 | // Check if we should run haxe autocomplete again 92 | if (this.last_completion_index != null) { 93 | if (this.last_completion_index === index && this.last_pretext.slice(0, index) === pretext.slice(0, index) && this.last_mode === mode) { 94 | var should_recompute_completion = true; 95 | 96 | // Compare last key path/partial key with the current one 97 | if (((key_path != null && this.last_key_path != null && key_path.join(',') === this.last_key_path.join(',')) 98 | || (key_path == null && this.last_key_path == null)) && this.last_has_partial_key == has_partial_key) { 99 | // Key path is the same as before 100 | should_recompute_completion = false; 101 | } 102 | 103 | if (!should_recompute_completion) { 104 | // No need to recompute haxe completion 105 | // Just filter and resolve 106 | var filtered = this._filter_suggestions(this.last_suggestions, this.last_suggestions_kind, opt, position_info); 107 | return resolve(filtered); 108 | } 109 | } 110 | } 111 | 112 | // Keep useful values for later completion 113 | this.last_pretext = pretext; 114 | this.last_completion_index = index; 115 | this.last_key_path = key_path; 116 | this.last_has_partial_key = has_partial_key; 117 | this.last_mode = mode; 118 | 119 | // We need to perform a new haxe query 120 | // Save file first 121 | var fetch, save_info; 122 | var use_external_file = atom.config.get('haxe.completion_avoid_saving_original_file'); 123 | if (use_external_file) { 124 | 125 | // Completion using an external file 126 | save_info = file.save_tmp_file_for_completion_of_original_file(opt.editor.buffer.file.path, text); 127 | 128 | fetch = query.get({ 129 | file: save_info.file_path, 130 | byte: Buffer.byteLength(save_info.contents.slice(0, index + save_info.contents.length - text.length), 'utf8'), 131 | mode: mode, 132 | add_args: ['-cp', save_info.cp_path] 133 | }); 134 | } 135 | else { 136 | 137 | // `classic` way of saving the file 138 | save_info = this._save_for_completion(opt.editor, _file); 139 | 140 | fetch = query.get({ 141 | file: save_info.file, 142 | byte: Buffer.byteLength(pretext.slice(0, index), 'utf8'), 143 | mode: mode, 144 | add_args:[] 145 | }); 146 | } 147 | 148 | fetch.then(function(data) { 149 | 150 | var parse = this._parse_suggestions(opt, data, position_info); 151 | 152 | parse.then(function(result) { 153 | resolve(result); 154 | }).catch(function(e) { 155 | reject(e); 156 | }); 157 | 158 | }.bind(this)).catch(reject).then(function() { 159 | 160 | if (use_external_file) { 161 | file.remove_tmp_file(save_info); 162 | } 163 | else { 164 | this._restore_post_completion(save_info); 165 | } 166 | 167 | }.bind(this)); //then 168 | 169 | }.bind(this)); //promise 170 | 171 | }, //getSuggestions 172 | 173 | onDidInsertSuggestion: function(options) { 174 | // Get editor state 175 | var editor = options.editor; 176 | var position = options.triggerPosition; 177 | var buffer_pos = editor.getLastCursor().getBufferPosition().toArray(); 178 | var suggestion = options.suggestion; 179 | 180 | if (this.use_extended_completion && (this.last_mode !== 'toplevel' && this.last_key_path != null || this.last_has_partial_key) && suggestion.className != 'haxe-autocomplete-suggestion-type-hint') { 181 | // When inserting a structure key, add a colon, unless it already exists 182 | var following_text = editor.getTextInBufferRange([ [position.row,position.column], editor.getBuffer().getEndPosition().toArray() ]); 183 | REGEX_BEGINS_WITH_KEY.lastIndex = -1; 184 | if (!REGEX_BEGINS_WITH_KEY.test(following_text)) { 185 | editor.insertText(': '); 186 | } 187 | 188 | } else { 189 | // When inserting a signature as snippet, remove the contents of the signature 190 | // because it is usually annoying, especially with optional arguments. 191 | // The type hinting should be enough to know what to type next 192 | // And in case the developer really wants to restore the full snippet, 193 | // ctrl/cmd + z shortcut will do this job 194 | var range = [ [position.row,position.column], buffer_pos ]; 195 | var inserted_text = editor.getTextInBufferRange(range); 196 | var sig_start = inserted_text.indexOf('('); 197 | if (sig_start > -1) { 198 | var following_text = editor.getTextInBufferRange([ [position.row,position.column], editor.getBuffer().getEndPosition().toArray() ]) 199 | var sig_end = following_text.indexOf(')') 200 | if (sig_end != -1) { 201 | inserted_text = following_text.slice(sig_start + 1, sig_end); 202 | editor.setTextInBufferRange([ [position.row,position.column+sig_start+1], [position.row,position.column+sig_start+inserted_text.length+1] ], ''); 203 | 204 | // Ensure the cursor is inside the parenthesis 205 | editor.setCursorBufferPosition([position.row,position.column+sig_start+1]); 206 | } 207 | } 208 | } 209 | }, //onDidInsertSuggestion 210 | 211 | // Compute and return the position info from the current text and index 212 | // such as the current replacement prefix 213 | _position_info_from_text_and_index: function(text, index) { 214 | REGEX_ENDS_WITH_DOT_IDENTIFIER.lastIndex = -1; 215 | var m, call_info; 216 | 217 | // We only care about the text before index 218 | text = text.slice(0, index); 219 | 220 | // Don't provide suggestions if inside a string or comment 221 | text = code.code_with_empty_comments_and_strings(text); 222 | 223 | // Look for a dot 224 | if (m = text.match(REGEX_ENDS_WITH_DOT_IDENTIFIER)) { 225 | 226 | // Don't query haxe when writing a number containing dots 227 | REGEX_ENDS_WITH_DOT_NUMBER.lastIndex = -1; 228 | if ((' '+text).match(REGEX_ENDS_WITH_DOT_NUMBER)) { 229 | return null; 230 | } 231 | 232 | // Don't query haxe when writing a package declaration 233 | REGEX_ENDS_WITH_PARTIAL_PACKAGE_DECL.lastIndex = -1; 234 | if ((' '+text).match(REGEX_ENDS_WITH_PARTIAL_PACKAGE_DECL)) { 235 | return null; 236 | } 237 | 238 | return { 239 | index: (index - m[1].length), 240 | prefix: m[1] 241 | }; 242 | } 243 | 244 | // Look for parens open 245 | else if (call_info = code.parse_partial_signature(text, index)) { 246 | var prefix = ''; 247 | 248 | if (call_info.partial_key != null) { 249 | prefix = call_info.partial_key; 250 | // Don't provide completion right after comma in this case 251 | // because it looks confusing 252 | if (call_info.partial_key.length == 0) { 253 | var c = text.charAt(index - 1); 254 | if (c == ',') { 255 | return null; 256 | } 257 | } 258 | } 259 | else { 260 | if (call_info.key_path != null && call_info.key_path.length > 0) { 261 | 262 | REGEX_ENDS_WITH_ALPHANUMERIC.lastIndex = -1; 263 | if (m = text.match(REGEX_ENDS_WITH_ALPHANUMERIC)) { 264 | prefix = m[1]; 265 | } else { 266 | return null; 267 | } 268 | 269 | return { 270 | index: (call_info.signature_start + 1), 271 | prefix: prefix, 272 | mode: 'toplevel' 273 | }; 274 | } 275 | else if (call_info.key_path == null && call_info.partial_arg != null) { 276 | 277 | REGEX_ENDS_WITH_ALPHANUMERIC.lastIndex = -1; 278 | if (m = text.match(REGEX_ENDS_WITH_ALPHANUMERIC)) { 279 | prefix = m[1]; 280 | } else { 281 | return null; 282 | } 283 | 284 | return { 285 | index: index - prefix.length, 286 | prefix: prefix, 287 | mode: 'toplevel', 288 | call: call_info 289 | }; 290 | } 291 | } 292 | 293 | return { 294 | index: (call_info.signature_start + 1), 295 | prefix: prefix, 296 | call: call_info 297 | }; 298 | } 299 | 300 | else { 301 | REGEX_ENDS_WITH_ALPHANUMERIC.lastIndex = -1; 302 | if (m = text.match(REGEX_ENDS_WITH_ALPHANUMERIC)) { 303 | prefix = m[1]; 304 | } else { 305 | return null; 306 | } 307 | 308 | return { 309 | index: index - prefix.length, 310 | prefix: prefix, 311 | mode: 'toplevel' 312 | }; 313 | } 314 | }, //_position_info_from_text_and_index 315 | 316 | _save_for_completion: function(_editor, _file) { 317 | 318 | var filepath = path.dirname(_file); 319 | var filename = path.basename(_file); 320 | var tmpname = '.' + filename; 321 | var tempfile = path.join(filepath, tmpname); 322 | 323 | fs.copySync(_file, tempfile); 324 | 325 | var _code = _editor.getText(); 326 | var b = new Buffer(_code, 'utf8'); 327 | var freal = fs.openSync(_file, 'w'); 328 | fs.writeSync(freal, b, 0, b.length, 0); 329 | fs.closeSync(freal); 330 | freal = null; 331 | 332 | return { 333 | tempfile: tempfile, 334 | file: _file 335 | } 336 | 337 | }, //_save_for_completion 338 | 339 | _restore_post_completion: function(save_info) { 340 | 341 | debug.query('remove ' + save_info.tempfile); 342 | 343 | if(fs.existsSync(save_info.tempfile)) { 344 | fs.deleteSync(save_info.tempfile); 345 | } 346 | 347 | }, //_restore_post_completion 348 | 349 | _parse_suggestions: function(opt, content, position_info) { 350 | 351 | return new Promise(function(resolve,reject) { 352 | 353 | var has_list = content.indexOf('') != -1; 354 | var has_type = content.indexOf('') != -1; 355 | var has_toplevel_list = content.indexOf('') != -1; 356 | if(!has_list && !has_type && !has_toplevel_list) { 357 | // Apparently no completion from this point 358 | var suggestions = []; 359 | 360 | if (position_info == null || position_info.mode !== 'toplevel') { 361 | // Try to extract haxe errors from output 362 | var haxe_errors = compiler.parse_output(content); 363 | 364 | if (haxe_errors.length > 0) { 365 | var message = haxe_errors[0].message; 366 | var message_words = message.split(' '); 367 | if (message_words.length != 2 || message_words[0] != 'Unexpected') { 368 | // Don't display errors like `Unexpected (` because they are happening only 369 | // because we are in the middle of typing an expression. 370 | // However we want to keep the other errors as they can tell the user 371 | // why he doesn't get autocomplete suggestion. 372 | suggestions.push({ 373 | rightLabelHTML: escape(message), 374 | text: '', // No text as we will perform no replacemennt and only label will be visible 375 | replacementPrefix: '', 376 | className: 'haxe-autocomplete-suggestion-error' 377 | }); 378 | } 379 | } 380 | } 381 | 382 | resolve(suggestions); 383 | 384 | debug.query('autocomplete response: ' + content); 385 | 386 | // because the error could come from the server not being ready 387 | // or other temporary state, reset completion info to be clean during next call 388 | this.last_completion_index = null; 389 | this.last_suggestions = null; 390 | this.last_suggestions_kind = null; 391 | 392 | } else { 393 | 394 | xml2js.parseString(content, function (err, json) { 395 | // TODO care about err 396 | 397 | var info = this._parse_haxe_completion_json(json); 398 | 399 | if (this.use_extended_completion && position_info != null && position_info.call != null && position_info.call.key_path != null) { 400 | // Extended completion with key path 401 | 402 | if (info.type != null && info.type.args != null) { 403 | 404 | // Extract the type we want to use to get key path completion 405 | var structure_type = info.type.args[position_info.call.number_of_args - 1]; 406 | if (structure_type == null) { 407 | return resolve([]); 408 | } 409 | if (typeof(structure_type) == 'object' && structure_type.type != null) { 410 | structure_type = structure_type.type; 411 | } 412 | structure_type = code.string_from_parsed_type(structure_type); 413 | 414 | // When having no partial key, we just want 415 | // to hint the current type 416 | var used_key_path = [].concat(position_info.call.key_path); 417 | if (position_info.call.partial_key == null) { 418 | used_key_path.pop(); 419 | } 420 | 421 | // Fetch instance completion list 422 | var fetch_instance_list = function(type, key_path, imports) { 423 | 424 | // Create a temporary file to get completion list 425 | var save_info = file.save_tmp_file_for_completion_list_of_instance_type(type, key_path, imports); 426 | 427 | // Query completion server 428 | var fetch = query.get({ 429 | file: save_info.file_path, 430 | byte: Buffer.byteLength(save_info.contents, 'utf8'), 431 | add_args: ['-cp', save_info.cp_path] 432 | }); 433 | 434 | fetch.then(function(content) { 435 | 436 | // Remove temporary file 437 | file.remove_tmp_file(save_info); 438 | 439 | var has_list = content.indexOf('') != -1; 440 | if (!has_list) { 441 | return resolve([]); 442 | } 443 | 444 | xml2js.parseString(content, function(err, json) { 445 | // TODO care about err 446 | 447 | var info = this._parse_haxe_completion_json(json); 448 | if (info.list != null) { 449 | var suggestions = []; 450 | 451 | // Fill suggestions 452 | this._add_suggestions_with_list(info.list, suggestions, {ignore_methods: true}); 453 | this.last_suggestions_kind = 'list'; 454 | 455 | // Keep computed suggestions for later use 456 | this.last_suggestions = suggestions; 457 | 458 | var filtered = this._filter_suggestions(this.last_suggestions, this.last_suggestions_kind, opt, position_info); 459 | resolve(filtered); 460 | 461 | } else { 462 | resolve([]); 463 | } 464 | }.bind(this)); 465 | 466 | }.bind(this)).catch(reject); 467 | 468 | }.bind(this); 469 | 470 | var last_dot_index = structure_type.lastIndexOf('.'); 471 | if (last_dot_index != -1 && structure_type.trim().charAt(0) !== '{') { 472 | // Because haxe server gives the package, not the module as response 473 | // We need to import sub-packages and then query completion on the type 474 | // This wouldn't be required if the completion response was giving us the full type path 475 | // At the moment, better have something that works with one more query than something not working. 476 | var package_name = structure_type.slice(0, last_dot_index); 477 | var base_type_name = structure_type.slice(last_dot_index + 1); 478 | 479 | // Create a temporary file to get completion list 480 | var save_info = file.save_tmp_file_for_completion_list_of_package(package_name); 481 | 482 | // Query completion server 483 | var fetch = query.get({ 484 | file: save_info.file_path, 485 | byte: Buffer.byteLength(save_info.contents, 'utf8'), 486 | add_args: ['-cp', save_info.cp_path] 487 | }); 488 | 489 | fetch.then(function(content) { 490 | 491 | // Remove temporary file 492 | file.remove_tmp_file(save_info); 493 | 494 | var has_list = content.indexOf('') != -1; 495 | if (!has_list) { 496 | return resolve([]); 497 | } 498 | 499 | xml2js.parseString(content, function(err, json) { 500 | // TODO care about err 501 | 502 | var info = this._parse_haxe_completion_json(json); 503 | if (info.list != null) { 504 | // Compute imports list 505 | var imports_list = []; 506 | for (var i = 0; i < info.list.length; i++) { 507 | imports_list.push(package_name + '.' + info.list[i].name); 508 | } 509 | 510 | // We've got the list of imports, query instance type 511 | fetch_instance_list(base_type_name, used_key_path, imports_list); 512 | 513 | } else { 514 | resolve([]); 515 | } 516 | }.bind(this)); 517 | 518 | }.bind(this)); 519 | 520 | } 521 | else { 522 | // No need to fetch package's sub-packages 523 | // Fetch instance's list directly 524 | fetch_instance_list(structure_type, used_key_path); 525 | } 526 | } 527 | else { 528 | return resolve([]); 529 | } 530 | 531 | } else { 532 | // Regular completion 533 | var suggestions = []; 534 | 535 | // If the info is a list 536 | if (info.list != null) { 537 | this._add_suggestions_with_list(info.list, suggestions); 538 | this.last_suggestions_kind = 'list'; 539 | } 540 | else if (info.type != null) { 541 | this._add_suggestion_with_type(info.type, suggestions); 542 | this.last_suggestions_kind = 'type'; 543 | } 544 | 545 | // Keep computed suggestions for later use 546 | this.last_suggestions = suggestions; 547 | 548 | // Filter and resolve 549 | var filtered = this._filter_suggestions(this.last_suggestions, this.last_suggestions_kind, opt, position_info); 550 | resolve(filtered); 551 | } 552 | 553 | }.bind(this)); //parseString 554 | 555 | } //contains 556 | 557 | }.bind(this)); //promise 558 | 559 | }, //_parse_suggestions 560 | 561 | _parse_haxe_completion_json: function(json) { 562 | 563 | var result; 564 | 565 | if (json.list) { 566 | result = {list: []}; 567 | for (var i = 0; i < json.list.i.length; ++i) { 568 | var node = json.list.i[i]; 569 | var name = node.$.n; 570 | var kind = node.$.k; 571 | var description = node.d || null; 572 | var type = node.t.length ? node.t[0] : null; 573 | 574 | var entry = { 575 | name: name, 576 | type: type 577 | }; 578 | 579 | if (kind) { 580 | entry.kind = kind; 581 | } 582 | 583 | if (description && description.length > 0) { 584 | entry.description = description.join("\n").trim(); 585 | } 586 | 587 | result.list.push(entry); 588 | } 589 | return result; 590 | 591 | } else if (json.type) { 592 | var argtypes = String(json.type).trim(); 593 | var info = code.parse_composed_type(argtypes); 594 | return { 595 | type: info 596 | }; 597 | } 598 | else if (json.il) { 599 | result = {list: []}; 600 | 601 | for (var i = 0; i < json.il.i.length; i++) { 602 | var raw_entry = json.il.i[i]; 603 | var name = raw_entry._; 604 | var description = raw_entry.d || null; 605 | var type = (raw_entry.$.t || raw_entry.$.p); 606 | 607 | if (raw_entry.$.k === 'package') { 608 | type = 'package'; 609 | } 610 | 611 | var entry = { 612 | name: name, 613 | type: type, 614 | kind: raw_entry.$.k 615 | }; 616 | 617 | if (description && description.length > 0) { 618 | entry.description = description.join("\n").trim(); 619 | } 620 | 621 | if (type != 'atom_tempfile__' && name != 'atom_tempfile__') { 622 | result.list.push(entry); 623 | } 624 | } 625 | 626 | return result; 627 | } 628 | 629 | return result; 630 | 631 | }, //parse_haxe_completion_json 632 | 633 | _add_suggestions_with_list: function(list, suggestions, options) { 634 | 635 | for (var i = 0; i < list.length; ++i) { 636 | 637 | var item = list[i]; 638 | 639 | var name = item.name; 640 | var type = code.parse_composed_type(item.type); 641 | 642 | // Detect method kind from toplevel completion 643 | if (item.kind == 'member' && type.args != null) { 644 | item.kind = 'method'; 645 | } 646 | 647 | var left = null, right = null, text = null, snippet = null, kind = null; 648 | 649 | // If the type is a method type 650 | if (type.args != null) { 651 | // Ignore methods? 652 | if (options != null && options.ignore_methods) { 653 | continue; 654 | } 655 | 656 | // Format method arguments 657 | var dumped_args = []; 658 | var number_of_chars = name.length; 659 | for (var j = 0; j < type.args.length; j++) { 660 | var arg = type.args[j]; 661 | var arg_str = ''; 662 | if (arg != null) { 663 | if (arg.name != null) { 664 | arg_str += arg.name; 665 | } 666 | else { 667 | arg_str += 'arg' + (j+1) 668 | } 669 | 670 | if (arg.optional) { 671 | arg_str = '?' + arg_str; 672 | } 673 | 674 | number_of_chars += arg_str.length + 2; 675 | if (number_of_chars > 80) { 676 | arg_str = arg_str.slice(0, arg_str.length - number_of_chars + 82); 677 | if (arg_str.length > 2) { 678 | // Strip the argument completely when only 2 characters ar left 679 | // Otherwise, it would look too ugly 680 | arg_str += '\u2026'; 681 | } 682 | else { 683 | arg_str = '\u2026'; 684 | } 685 | } 686 | 687 | arg_str = '${' + (j + 1) + ':' + arg_str + '}'; 688 | } 689 | dumped_args.push(arg_str); 690 | 691 | // If there are too many arguments in list, don't display all of them 692 | if (number_of_chars > 40) { 693 | break; 694 | } 695 | } 696 | 697 | // When suggesting a function, use a snippet in order to get nicer output 698 | // and to have the cursor put inside the parenthesis when confirming 699 | left = code.string_from_parsed_type(type.type); 700 | 701 | if (dumped_args.length > 0) { 702 | snippet = name + '(' + dumped_args.join(', ') + ')'; 703 | } else { 704 | text = name + '()'; 705 | } 706 | } 707 | else { 708 | // Otherwise just format the type 709 | left = code.string_from_parsed_type(type); 710 | text = name; 711 | } 712 | 713 | // Don't display Unknown types 714 | if (left === 'Unknown' || left.slice(0, 8) === 'Unknown<') { 715 | left = ''; 716 | } 717 | 718 | var icon_html = null; 719 | switch (item.kind) 720 | { 721 | case 'package': 722 | kind = 'import'; 723 | break; 724 | case 'local': 725 | kind = 'variable'; 726 | break; 727 | case 'var': 728 | case 'member': 729 | kind = 'property'; 730 | break; 731 | case 'static': 732 | kind = 'property'; 733 | icon_html = 's'; 734 | break; 735 | case 'type': 736 | case 'enum': 737 | kind = 'type'; 738 | break; 739 | case 'method': 740 | kind = 'method'; 741 | break; 742 | default: 743 | kind = 'value'; // Anything else 744 | break; 745 | } 746 | 747 | var completion_display_style = atom.config.get('haxe.completion_display_return_type_position'); 748 | 749 | if (completion_display_style == 'right') { 750 | right = left; 751 | left = null; 752 | } 753 | 754 | // Create final suggestion 755 | var suggestion = { 756 | replacementPrefix: '', 757 | className: 'haxe-autocomplete-suggestion-list-item' 758 | }; 759 | 760 | if (left != null) { 761 | suggestion.leftLabel = left; 762 | } 763 | 764 | if (right != null) { 765 | suggestion.rightLabel = right; 766 | } 767 | 768 | if (kind != null) { 769 | suggestion.type = kind; 770 | } 771 | 772 | if (icon_html != null) { 773 | suggestion.iconHTML = icon_html; 774 | } 775 | 776 | if (item.description != null) { 777 | suggestion.description = item.description; 778 | } 779 | 780 | if (snippet != null) { 781 | suggestion.snippet = snippet; 782 | } 783 | else { 784 | suggestion.text = text; 785 | } 786 | 787 | suggestions.push(suggestion); 788 | 789 | } //each item*/ 790 | }, //_add_suggestions_with_list 791 | 792 | _add_suggestion_with_type: function(type, suggestions) { 793 | 794 | // Compute list of arguments in signature 795 | var displayed_type; 796 | if (type.args != null && type.args.length > 0) { 797 | var call_args = []; 798 | for (var i = 0; i < type.args.length; i++) { 799 | var arg = type.args[i]; 800 | var arg_str = arg.name; 801 | 802 | if (arg_str == null) { 803 | arg_str = 'arg' + (i + 1) 804 | } 805 | 806 | if (arg.optional) { 807 | arg_str = '?' + arg_str; 808 | } 809 | 810 | if (arg.type != null) { 811 | var type_str = code.string_from_parsed_type(arg.type); 812 | 813 | // Don't display Unknown types 814 | if (type_str !== 'Unknown' && type_str.slice(0, 8) !== 'Unknown<') { 815 | arg_str = arg_str + ': ' + type_str; 816 | } 817 | } 818 | 819 | call_args.push(arg_str); 820 | } 821 | 822 | displayed_type = call_args.join(', '); 823 | } 824 | else { 825 | displayed_type = 'no parameters'; 826 | } 827 | 828 | suggestions.push({ 829 | _call_args: call_args, // for later argument highlighting when filtering 830 | rightLabelHTML: escape(displayed_type), 831 | text: '_', // No text as we will perform no replacemennt and only label will be visible 832 | snippet: '', 833 | replacementPrefix: '', 834 | className: 'haxe-autocomplete-suggestion-type-hint' 835 | }); 836 | 837 | }, 838 | 839 | _filter_suggestions: function(prev_suggestions, prev_suggestions_kind, options, position_info) { 840 | 841 | var prev_suggestion, suggestion, i; 842 | 843 | // No sugggestions to filter? return empty array 844 | if (prev_suggestions == null) { 845 | return []; 846 | } 847 | 848 | // Return key path type hint if needed 849 | if (position_info != null 850 | && position_info.call != null 851 | && position_info.call.partial_key == null 852 | && position_info.call.key_path != null 853 | && position_info.call.key_path.length > 0) { 854 | var key = position_info.call.key_path[position_info.call.key_path.length - 1]; 855 | // Look for a completion element with the same key 856 | for (i = 0; i < prev_suggestions.length; i++) { 857 | prev_suggestion = prev_suggestions[i]; 858 | 859 | if (prev_suggestion.text == key) { 860 | 861 | if (prev_suggestion.rightLabel != null) { 862 | // Found it. Create a type hint element from it and return only this one 863 | return [{ 864 | rightLabelHTML: '' + escape(prev_suggestion.text) + ': ' + escape(prev_suggestion.rightLabel) + '', 865 | text: '', // No text as we will perform no replacemennt and only label will be visible 866 | replacementPrefix: '', 867 | className: 'haxe-autocomplete-suggestion-type-hint' 868 | }]; 869 | } 870 | 871 | return []; 872 | } 873 | } 874 | } 875 | 876 | // Update prefix if needed 877 | var prefix = options.prefix; 878 | if (position_info != null && position_info.prefix != null) { 879 | prefix = position_info.prefix; 880 | } 881 | else if (this.prefixes.indexOf(prefix) != -1) { 882 | prefix = ''; 883 | } 884 | 885 | // Create filterable suggestions 886 | var suggestions = [] 887 | for (i = 0; i < prev_suggestions.length; i++) { 888 | prev_suggestion = prev_suggestions[i]; 889 | suggestion = {}; 890 | 891 | // text 892 | if (prev_suggestion.text != null) { 893 | suggestion.text = prev_suggestion.text; 894 | } 895 | // snippet 896 | if (prev_suggestion.snippet != null) { 897 | suggestion.snippet = prev_suggestion.snippet; 898 | } 899 | // right label 900 | if (prev_suggestion.rightLabel != null) { 901 | suggestion.rightLabel = prev_suggestion.rightLabel; 902 | } 903 | // left label 904 | if (prev_suggestion.leftLabel != null) { 905 | suggestion.leftLabel = prev_suggestion.leftLabel; 906 | } 907 | // type 908 | if (prev_suggestion.type != null) { 909 | suggestion.type = prev_suggestion.type; 910 | } 911 | // type 912 | if (prev_suggestion.iconHTML != null) { 913 | suggestion.iconHTML = prev_suggestion.iconHTML; 914 | } 915 | // label (html) 916 | if (prev_suggestion.rightLabelHTML != null) { 917 | suggestion.rightLabelHTML = prev_suggestion.rightLabelHTML; 918 | } 919 | // class name 920 | if (prev_suggestion.className != null) { 921 | suggestion.className = prev_suggestion.className; 922 | } 923 | // replacement prefix 924 | if (prev_suggestions_kind === 'list') { 925 | suggestion.replacementPrefix = prefix; 926 | } else { 927 | // When doing type hinting, no replacement will be done 928 | suggestion.replacementPrefix = ''; 929 | } 930 | // description 931 | if (prev_suggestion.description != null) { 932 | suggestion.description = prev_suggestion.description; 933 | } 934 | 935 | // call args 936 | if (prev_suggestion._call_args != null) { 937 | suggestion._call_args = prev_suggestion._call_args; 938 | } 939 | 940 | // filter key for fuzzaldrin 941 | if (suggestion.snippet != null) { 942 | suggestion._filter = suggestion.snippet; 943 | } else if (suggestion.text != null) { 944 | suggestion._filter = suggestion.text; 945 | } 946 | 947 | suggestions.push(suggestion); 948 | } 949 | 950 | // Filter suggestions if needed 951 | if (prev_suggestions_kind === 'list' && prefix != null && prefix.length > 0) { 952 | suggestions = filter(suggestions, prefix, {key: '_filter'}); 953 | } 954 | 955 | // When making suggestions for anonymous structures, 956 | // remove suggestions that match the already used keys 957 | if (prev_suggestions_kind === 'list' 958 | && position_info != null 959 | && position_info.call != null 960 | && position_info.call.used_keys != null 961 | && position_info.call.used_keys.length > 0) { 962 | var filtered_suggestions = []; 963 | var forbidden_keys = {}; 964 | for (i = 0; i < position_info.call.used_keys.length; i++) { 965 | forbidden_keys[position_info.call.used_keys[i]] = true; 966 | } 967 | for (i = 0; i < suggestions.length; i++) { 968 | suggestion = suggestions[i]; 969 | if (!forbidden_keys[suggestion.text]) { 970 | filtered_suggestions.push(suggestion); 971 | } 972 | } 973 | suggestions = filtered_suggestions; 974 | } 975 | 976 | // Highlight argument if doing type hinting 977 | if (prev_suggestions_kind === 'type' && position_info != null && position_info.call != null && suggestions.length === 1) { 978 | suggestion = suggestions[0]; 979 | if (suggestion._call_args != null && suggestion._call_args.length > 0) { 980 | var formatted_args = []; 981 | var current_arg_index = position_info.call.number_of_args - 1; 982 | for (i = 0; i < suggestion._call_args.length; i++) { 983 | var arg = suggestion._call_args[i]; 984 | if (i === current_arg_index) { 985 | formatted_args.push('' + escape(arg) + ''); 986 | } else { 987 | formatted_args.push(escape(arg)); 988 | } 989 | } 990 | suggestion.rightLabelHTML = formatted_args.join(', '); 991 | } 992 | } 993 | 994 | return suggestions; 995 | 996 | } //_filter_suggestions 997 | 998 | } //module.exports 999 | -------------------------------------------------------------------------------- /lib/completion/query.js: -------------------------------------------------------------------------------- 1 | 2 | // node built in 3 | var path = require('path') 4 | // lib code 5 | , debug = require('./debug') 6 | , run = require('../haxe-call') 7 | , state = require('../haxe-state') 8 | 9 | 10 | module.exports = { 11 | 12 | init: function() { 13 | }, 14 | 15 | dispose:function() { 16 | }, 17 | 18 | get: function(options) { 19 | 20 | if(!options) { 21 | // console.log('no options to query'); 22 | return null; 23 | } 24 | 25 | return new Promise(function(resolve, reject) { 26 | 27 | var port = atom.config.get('haxe.server_port'); 28 | 29 | var byte = options.byte || 0 30 | var file = options.file || ''; 31 | var cwd = options.cwd || state.hxml_cwd; 32 | var args = []; 33 | 34 | if(cwd) { 35 | args.push('--cwd'); 36 | args.push(cwd); 37 | } 38 | 39 | //allow custom args for get query 40 | if(options.add_args) { 41 | args = args.concat(options.add_args); 42 | } 43 | 44 | var hxml_args = state.hxml_as_args(options); 45 | if(!hxml_args) { 46 | log.error('no completion hxml is configured'); 47 | return reject(); 48 | } 49 | 50 | args = args.concat(hxml_args); 51 | 52 | args.push('--no-output'); 53 | args.push('--display'); 54 | 55 | if (options.mode != null) { 56 | args.push(file+'@'+byte+'@'+options.mode); 57 | } else { 58 | args.push(file+'@'+byte); 59 | } 60 | 61 | args.push('-D'); 62 | args.push('display-details'); 63 | 64 | args.push('--connect'); 65 | args.push(''+port); 66 | 67 | debug.query('on ' + port + ' with ' + args.join(' ')); 68 | debug.query(''); 69 | 70 | run.haxe(args).then(function(result) { 71 | resolve(result.err || result.out); 72 | }); 73 | 74 | }.bind(this)); //promise 75 | 76 | }, //get 77 | 78 | } //module.exports 79 | -------------------------------------------------------------------------------- /lib/completion/server.js: -------------------------------------------------------------------------------- 1 | 2 | // node built in 3 | var exec = require('child_process').spawn 4 | // lib code 5 | , debug = require('./debug') 6 | 7 | 8 | var should_run = false; 9 | module.exports = { 10 | 11 | init:function() { 12 | should_run = true; 13 | var time = atom.config.get('haxe.server_activation_start_delay'); 14 | setTimeout(this.reset.bind(this), time * 1000.0); 15 | }, 16 | 17 | dispose:function() { 18 | should_run = false; 19 | this.stop(); 20 | }, 21 | 22 | on_exit:function(code) { 23 | debug.server('process exit ' + code); 24 | this.process = null; 25 | 26 | // Try to start again the server if it was ended 27 | // when it should be active. This ensures the server will be 28 | // restarted when closing the window that was running it. 29 | if (should_run) setTimeout(this.reset_if_needed.bind(this), 5000.0); 30 | }, 31 | 32 | on_data:function(data) { 33 | debug.server(data.toString('utf-8')); 34 | }, 35 | 36 | on_error:function(data) { 37 | debug.server(data.toString('utf-8')); 38 | }, 39 | 40 | reset:function() { 41 | 42 | this.stop(); 43 | 44 | var port = atom.config.get('haxe.server_port'); 45 | var haxe_path = atom.config.get('haxe.haxe_path') || 'haxe'; 46 | 47 | debug.server('starting `' + haxe_path + '` on `' + port + '`'); 48 | 49 | this.process = exec(haxe_path, ['--wait',''+port]); 50 | this.process.stdout.on('data', this.on_data.bind(this)); 51 | this.process.stderr.on('data', this.on_error.bind(this)); 52 | this.process.on('close', this.on_exit.bind(this)); 53 | 54 | }, //reset 55 | 56 | reset_if_needed:function() { 57 | 58 | if (should_run && !this.process) this.reset(); 59 | 60 | }, //reset_if_needed 61 | 62 | stop:function() { 63 | 64 | if(this.process) { 65 | try { 66 | this.process.kill(); 67 | debug.server('stopped'); 68 | } catch(e) { 69 | //nothing to do 70 | } 71 | } 72 | 73 | this.process = null; 74 | 75 | } //stop 76 | 77 | 78 | } //module.exports 79 | -------------------------------------------------------------------------------- /lib/haxe-call.js: -------------------------------------------------------------------------------- 1 | 2 | //node built in 3 | var exec = require('./utils/exec'); 4 | 5 | //This can be require'd and then run haxe or haxelib commands 6 | // which will return a promise with the error, output and code 7 | 8 | module.exports = { 9 | 10 | haxe:function(args, ondataout, ondataerr) { 11 | var haxe_path = atom.config.get('haxe.haxe_path') || 'haxe'; 12 | return exec(haxe_path, args, ondataout, ondataerr); 13 | }, 14 | 15 | haxelib:function(args, ondataout, ondataerr) { 16 | var haxelib_path = atom.config.get('haxe.haxelib_path') || 'haxelib'; 17 | return exec(haxelib_path, args, ondataout, ondataerr); 18 | } 19 | 20 | } //module.exports 21 | -------------------------------------------------------------------------------- /lib/haxe-config.js: -------------------------------------------------------------------------------- 1 | 2 | //atom-haxe config module 3 | //All config values are here. 4 | 5 | module.exports = { 6 | 7 | server_activation_start_delay:{ 8 | title: 'Activation: Start up delay', 9 | description: 'Time in seconds. The delay before starting the completion server on Atom startup. This simply prevents any unnecessary slow down on Atom launch.', 10 | type: 'number', 11 | default: 3.0 12 | }, 13 | 14 | server_port: { 15 | title: 'Haxe Completion Server port', 16 | description: 'The port that the completion server will --wait on.', 17 | type: 'integer', 18 | default:6112 19 | }, 20 | 21 | haxe_path: { 22 | title: 'Haxe executable path', 23 | description: 'Only needed if you have configured a custom Haxe location.', 24 | type: 'string', 25 | default:'haxe' 26 | }, 27 | 28 | haxelib_path: { 29 | title: 'Haxelib executable path', 30 | description: 'Only needed if you have configured a custom Haxelib location.', 31 | type: 'string', 32 | default:'haxelib' 33 | }, 34 | 35 | build_selectors: { 36 | title: 'Build: allowed file scopes', 37 | description: 'When triggering a build command, only file scope in this list will trigger.', 38 | type: 'string', 39 | default:'source.haxe, source.hx, source.hxml' 40 | }, 41 | 42 | build_autosave: { 43 | title: 'Build: save all files before building', 44 | description: 'When triggering a build command, save all files first.', 45 | type: 'boolean', 46 | default: 'true' 47 | }, 48 | 49 | debug_logging: { 50 | title: 'Debug Logging', 51 | description: 'Enable to get more in depth logging for debugging problems with the package', 52 | type: 'boolean', 53 | default:'false' 54 | }, 55 | 56 | completion_avoid_saving_original_file: { 57 | title: 'Avoid saving original file for completion (experimental)', 58 | description: 'Use a different way of handling haxe autocomplete that ensure the original file remains untouched.', 59 | type: 'boolean', 60 | default: 'false' 61 | }, 62 | 63 | completion_display_return_type_position: { 64 | title: 'Completion return type display style', 65 | description: 'Display completion return type before (on the left) or after (on the right) the field.', 66 | type: 'string', 67 | default: 'right', 68 | enum: ['right', 'left'] 69 | } 70 | 71 | } //module.exports 72 | -------------------------------------------------------------------------------- /lib/haxe-state.js: -------------------------------------------------------------------------------- 1 | 2 | //node built in 3 | var path = require('path') 4 | //lib code 5 | , log = require('./utils/log') 6 | , hxml_utils = require('./utils/hxml') 7 | //dep code 8 | , tmp = require('temporary') 9 | 10 | module.exports = { 11 | 12 | //convenience for external 13 | valid:false, 14 | //the active consumer 15 | consumer:null, 16 | //the active hxml data 17 | hxml_file:null, 18 | hxml_content:null, 19 | hxml_cwd:null, 20 | //the temp file cache path 21 | tmp_path:null, 22 | 23 | //lifecycle 24 | 25 | init: function(state) { 26 | 27 | this.set(state); 28 | 29 | this.tmp_dir = new tmp.Dir('atom-haxe'); 30 | this.tmp_path = path.normalize(this.tmp_dir.path); 31 | 32 | log.debug('state set cache path ' + this.tmp_path); 33 | 34 | }, 35 | 36 | dispose:function() { 37 | this.tmp_dir.rmdir(); 38 | }, 39 | 40 | serialize:function() { 41 | 42 | var o = { 43 | hxml_file: this.hxml_file, 44 | hxml_cwd: this.hxml_cwd, 45 | hxml_content: this.hxml_content 46 | } 47 | 48 | // console.log('state',o); 49 | 50 | return o; 51 | }, 52 | 53 | //Consumer 54 | 55 | set_consumer: function(opt) { 56 | 57 | //notify existing 58 | if(this.consumer && this.consumer.onConsumerLost) { 59 | this.consumer.onConsumerLost(); 60 | } 61 | 62 | //set new 63 | this.consumer = opt; 64 | 65 | //apply 66 | if(opt) { 67 | 68 | log.debug('consumer; set external consumer to ' + this.consumer.name); 69 | 70 | if(this.consumer.hxml_content) { 71 | this.set_hxml_content(opt.hxml_content, opt.hxml_cwd); 72 | } else if(opt.hxml_file) { 73 | this.set_hxml_file(opt.hxml_file, opt.hxml_cwd); 74 | } 75 | } else { 76 | log.debug('consumer; unset external consumer'); 77 | } 78 | 79 | }, //set_consumer 80 | 81 | //State queries 82 | 83 | as_args:function(plus_args) { 84 | 85 | if(!this.valid) return null; 86 | 87 | var args = []; 88 | 89 | if(this.hxml_cwd) { 90 | args.push('--cwd'); 91 | args.push(this.hxml_cwd); 92 | } 93 | 94 | args = args.concat(this.hxml_as_args()); 95 | 96 | if(plus_args) { 97 | args = args.concat(plus_args); 98 | } 99 | 100 | return args; 101 | 102 | }, //as_args 103 | 104 | 105 | hxml_as_args:function(options) { 106 | 107 | if(!this.valid) return null; 108 | 109 | options = options || {}; 110 | 111 | var args = []; 112 | //check if hxml content is set 113 | var hxml = options.hxml_content || this.hxml_content; 114 | 115 | //if not, check for a file 116 | if(!hxml) { 117 | //check if there's a file given instead 118 | hxml = options.hxml_file || this.hxml_file; 119 | //if there is, make it relative 120 | if(hxml) { 121 | hxml = path.relative(this.hxml_cwd, hxml); 122 | args = [hxml]; 123 | } 124 | 125 | } else { 126 | 127 | args = hxml_utils.parse_hxml_args(hxml); 128 | 129 | } 130 | 131 | return args; 132 | 133 | }, //hxml_as_args 134 | 135 | //State updates 136 | 137 | set_hxml_file:function(file, cwd) { 138 | this.unset(true); 139 | this.hxml_file = file; 140 | this.hxml_cwd = cwd || path.dirname(file); 141 | log.debug('state hxml file to ' + this.hxml_file); 142 | log.debug('state hxml cwd to ' + this.hxml_cwd); 143 | this.valid = true; 144 | }, 145 | 146 | set_hxml_content:function(file_content, cwd) { 147 | this.unset(true); 148 | this.hxml_content = file_content; 149 | this.hxml_cwd = cwd; 150 | // log.debug('state hxml file content set to ' + this.hxml_content); 151 | log.debug('state set hxml, and cwd: ' + this.hxml_cwd); 152 | this.valid = true; 153 | }, 154 | 155 | unset:function(internal) { 156 | this.hxml_file = null; 157 | this.hxml_content = null; 158 | this.hxml_cwd = null; 159 | this.valid = false; 160 | if(!internal) log.debug('state unset'); 161 | }, 162 | 163 | set:function(state) { 164 | 165 | if(state) { 166 | 167 | //:todo: possibly more resilient since 168 | //this may be called from elsewhere later. 169 | //right now it's only called internally. 170 | var cwd = state.hxml_cwd; 171 | 172 | if(state.hxml_file) { 173 | this.set_hxml_file(state.hxml_file, cwd); 174 | } else if(state.hxml_content) { 175 | this.set_hxml_content(state.hxml_content, cwd); 176 | } 177 | 178 | this.valid = true; 179 | 180 | } //if state 181 | 182 | }, //set 183 | 184 | 185 | } //module.exports 186 | -------------------------------------------------------------------------------- /lib/haxe.js: -------------------------------------------------------------------------------- 1 | 2 | // lib code 3 | var server = require('./completion/server') 4 | , query = require('./completion/query') 5 | , build = require('./build/build') 6 | , lint = require('./linting/lint') 7 | , decl = require('./reflect/declaration') 8 | , state = require('./haxe-state') 9 | // lib debugging 10 | , comp_debug = require('./completion/debug') 11 | , log = require('./utils/log') 12 | 13 | // Ensure 'haxe' and 'haxelib' default binaries can be found 14 | // (OSX El Capitan launchctl PATH related solution, for now) 15 | if(process.platform == 'darwin') { 16 | process.env.PATH = ["/usr/local/bin", process.env.PATH].join(":"); 17 | } 18 | 19 | module.exports = { 20 | 21 | config: require('./haxe-config'), 22 | 23 | failed: false, 24 | disposables : [], 25 | 26 | //Lifecycle 27 | 28 | //This is automatic, don't add anything heavy in here 29 | activate: function(serialized_state) { 30 | 31 | this.check_dependency().then(function(){ 32 | 33 | this._init_internal(); 34 | log.init(); 35 | comp_debug.init(); 36 | server.init(); 37 | query.init(); 38 | lint.init(); 39 | decl.init(); 40 | state.init(serialized_state); 41 | 42 | }.bind(this)).catch(function(e) { 43 | 44 | if(e && e.missing_deps) { 45 | this.fail(e); 46 | } else { 47 | throw e; 48 | } 49 | 50 | }.bind(this)); 51 | 52 | }, //activate 53 | 54 | deactivate:function() { 55 | 56 | if(!this.failed) { 57 | this._dispose_internal(); 58 | state.dispose(); 59 | query.dispose(); 60 | server.dispose(); 61 | comp_debug.dispose(); 62 | log.dispose(); 63 | decl.dispose(); 64 | lint.dispose(); 65 | } 66 | 67 | }, //deactivate 68 | 69 | serialize: function() { 70 | 71 | if(!this.failed) return state.serialize(); 72 | 73 | }, //serialize 74 | 75 | check_dependency: function() { 76 | 77 | var required = ['linter', 'autocomplete-plus', 'language-haxe']; 78 | return new Promise(function(resolve, reject) { 79 | 80 | var missing = []; 81 | for(var i = 0; i < required.length; ++i) { 82 | var req = required[i]; 83 | if(!atom.packages.isPackageLoaded(req)) { 84 | missing.push(req); 85 | } 86 | } 87 | 88 | if(missing.length) { 89 | console.log('haxe / missing dependencies', missing); 90 | reject({ missing_deps:missing }); 91 | } else { 92 | resolve(); 93 | } 94 | 95 | }); //promise 96 | 97 | }, //check_dependency 98 | 99 | fail: function(e) { 100 | 101 | this.failed = true; 102 | var message = 'Haxe package is missing dependencies!
'; 103 | message += 'Please install/activate these via Preferences:

'; 104 | message += e.missing_deps.map(function(d){ return '- '+d+' package'; }).join('
'); 105 | 106 | atom.notifications.addWarning(message, {dismissable:true}); 107 | 108 | }, //fail 109 | 110 | //Menu handling 111 | 112 | display_context: function(event) { 113 | var editor = atom.workspace.getActiveTextEditor(); 114 | var scope = editor.getRootScopeDescriptor(); 115 | return (scope.scopes.indexOf('source.hxml') != -1); 116 | }, 117 | 118 | display_context_tree: function(event) { 119 | var key = '.hxml'; 120 | var val = event.target.innerText || ''; 121 | return val.indexOf(key, val.length - key.length) !== -1; 122 | }, 123 | 124 | //Commands 125 | 126 | set_hxml_file_from_treeview: function(opt) { 127 | 128 | var treeview = atom.packages.getLoadedPackage('tree-view'); 129 | if(!treeview) return; 130 | 131 | treeview = require(treeview.mainModulePath); 132 | if(!treeview.serialize) 133 | treeview = treeview.getTreeViewInstance(); 134 | 135 | var package_obj = treeview.serialize(); 136 | var file_path = package_obj.selectedPath; 137 | 138 | //we are the consumer now 139 | state.set_hxml_file( file_path ); 140 | state.set_consumer(null); 141 | 142 | }, //set_hxml_file_from_treeview 143 | 144 | build: function(e) { 145 | 146 | //continue propogation if 147 | //not set up properly 148 | if(this.failed) { 149 | return e.abortKeyBinding(); 150 | } 151 | 152 | //check selectors 153 | var selectors = atom.config.get('haxe.build_selectors'); 154 | //:todo: resilience 155 | var list = selectors.split(','); 156 | list = list.map(function(l) { return l.trim(); }); 157 | 158 | //get the consumer selectors 159 | if(state.consumer && state.consumer.onBuildSelectorQuery) { 160 | var consumer_list = state.consumer.onBuildSelectorQuery(); 161 | if(consumer_list) { 162 | list = list.concat(consumer_list); 163 | } 164 | } 165 | 166 | //find current scope of file 167 | var editor = atom.workspace.getActiveTextEditor(); 168 | var scope = editor ? editor.getRootScopeDescriptor() : null; 169 | if(editor && scope && scope.scopes.length) { 170 | 171 | //filter non scoped ones 172 | var allowed = list.filter(function(item){ 173 | if(scope.scopes.indexOf(item) != -1) { 174 | return true; 175 | } 176 | }); 177 | 178 | //any left? 179 | if(allowed.length) { 180 | build.run_build(); 181 | } else { 182 | var info = ' - scope: ' + scope.scopes.join(', '); 183 | info += ' - allowed: ' + list; 184 | log.debug('build / scope not in allowed selectors (see settings).' + info); 185 | } 186 | 187 | } //editor + scope 188 | 189 | }, 190 | 191 | //Providers 192 | 193 | //provides this plugin to the consumers, 194 | //allowing them to call set_completion_state 195 | //with options, to config the haxe query completion state 196 | completion_service:function() { 197 | return this; 198 | }, 199 | 200 | set_completion_consumer:function( options ) { 201 | 202 | var _str = JSON.stringify(options); 203 | 204 | var handle_err = function(err) { 205 | log.error('Failed to set completion consumer. ' + _str); 206 | log.error(err); 207 | return false; 208 | } 209 | 210 | if(!options.name || !options.onConsumerLost) { 211 | return handle_err('required: name and onConsumerLost(callback) properties'); 212 | } 213 | 214 | if(options.hxml_content && options.hxml_file) { 215 | return handle_err('error: hxml_content or hxml_file, not both.'); 216 | } 217 | 218 | if(options.hxml_content && !options.hxml_cwd) { 219 | return handle_err('required: hxml_cwd is needed when giving hxml_content'); 220 | } 221 | 222 | log.debug('set consumer: ' + _str); 223 | 224 | state.set_consumer(options); 225 | 226 | return true; 227 | 228 | }, //set_completion_consumer 229 | 230 | //handles the completion provider 231 | //for autocomplete-plus 232 | provide: function() { 233 | return require('./completion/provider'); 234 | }, 235 | 236 | //provides the haxe linter 237 | provide_linter: function() { 238 | var provider = { 239 | grammarScopes: ['source.haxe', 'source.hx'], 240 | scope: 'project', 241 | lintOnFly: false, 242 | lint: function(text_editor) { 243 | return new Promise(function(resolve, reject) { 244 | lint.lint_project(text_editor, resolve); 245 | }); 246 | } 247 | } 248 | return provider; 249 | }, 250 | 251 | 252 | //Internal conveniences 253 | 254 | _init_commands:function() { 255 | 256 | this._command('build', 257 | this.build.bind(this) ); 258 | 259 | this._command('toggle-log-view', 260 | log.toggle.bind(log) ); 261 | 262 | this._command('reset-server', 263 | server.reset.bind(server) ); 264 | 265 | this._command('stop-server', 266 | server.stop.bind(server) ); 267 | 268 | this._command('toggle-completion-debug', 269 | comp_debug.toggle.bind(comp_debug) ); 270 | 271 | this._command('set-hxml-file', 272 | this.set_hxml_file_from_treeview.bind(this) ); 273 | 274 | this._command('clear-project', 275 | state.unset.bind(state) ); 276 | 277 | this._command('go-to-declaration', 278 | decl.jump.bind(decl), 'symbols-view' ); 279 | 280 | }, //_init_commands 281 | 282 | _init_menus:function() { 283 | 284 | atom.contextMenu.add({ 285 | "atom-text-editor" : [ 286 | { type: 'separator' }, 287 | { label: 'Set as active HXML file', command: 'haxe:set-haxe-file', shouldDisplay:this.display_context.bind(this) }, 288 | { type: 'separator' } 289 | ], 290 | ".tree-view .file": [ 291 | { type: 'separator' }, 292 | { label: 'Set as active HXML file', command: 'haxe:set-hxml-file', shouldDisplay:this.display_context_tree.bind(this) }, 293 | { type: 'separator' } 294 | ] 295 | }); 296 | 297 | }, 298 | 299 | _init_internal:function() { 300 | 301 | this._init_commands(); 302 | this._init_menus(); 303 | 304 | }, // 305 | 306 | _dispose_internal:function() { 307 | 308 | for(var i = 0; i < this.disposables.length; ++i) { 309 | this.disposables[i].dispose(); 310 | } 311 | 312 | this.disposables = []; 313 | 314 | }, // 315 | 316 | _command:function(name, func, module) { 317 | 318 | if (module == null) { 319 | module = 'haxe'; 320 | } 321 | 322 | var cmd = atom.commands.add('atom-workspace', module+':'+name, func); 323 | this.disposables.push(cmd); 324 | 325 | return cmd; 326 | } // 327 | 328 | } //module.exports 329 | -------------------------------------------------------------------------------- /lib/linting/lint.js: -------------------------------------------------------------------------------- 1 | 2 | // node built in 3 | var path = require('path') 4 | , fs = require('fs') 5 | // lib code 6 | , run = require('../haxe-call') 7 | , state = require('../haxe-state') 8 | , compiler = require('../parsing/compiler') 9 | // atom related 10 | , Range = require('atom').Range 11 | 12 | module.exports = { 13 | 14 | // The first lint will wait until haxe compiler is ready 15 | // This will be set to false after the linting ran once 16 | should_wait_for_compiler: true, 17 | 18 | // Last compiler output errors 19 | compiler_errors: [], 20 | 21 | init: function() { 22 | 23 | }, 24 | 25 | dispose: function() { 26 | 27 | }, 28 | 29 | lint_project: function(editor, done) { 30 | 31 | // The first lint is delayed to be sure the compiler is ready 32 | if (this.should_wait_for_compiler) { 33 | var time = atom.config.get('haxe.server_activation_start_delay'); 34 | setTimeout((function() { 35 | // Update flag 36 | this.should_wait_for_compiler = false; 37 | // Lint 38 | this.lint_project(editor, done); 39 | 40 | }).bind(this), (time + 1) * 1000.0); 41 | 42 | return; 43 | } 44 | 45 | // Defer from 1ms because in many cases linters are triggered after 46 | // file save. In that case, we want to be sure the compiler will start 47 | // running before this code is executed. 48 | setImmediate((function() { 49 | 50 | this._run_compiler((function() { 51 | this._provide_lint_items(editor, done); 52 | }).bind(this)); 53 | 54 | }).bind(this)); 55 | }, 56 | 57 | //Internal 58 | 59 | _run_compiler: function(done) { 60 | 61 | var port = atom.config.get('haxe.server_port'); 62 | 63 | var args = []; 64 | args.push('--no-output'); 65 | 66 | // Add a define to let haxe code know 67 | // the compilation is running for linting 68 | args.push('-D'); 69 | args.push('lint'); 70 | 71 | if (port) { 72 | args.push('--connect'); 73 | args.push(String(port)); 74 | } 75 | 76 | args = state.as_args(args); 77 | 78 | run.haxe(args).then((function(result) { 79 | 80 | // Extract errors from output 81 | if (result.err.length > 0) { 82 | this.compiler_errors = compiler.parse_output(result.err); 83 | } else { 84 | this.compiler_errors = []; 85 | } 86 | 87 | done(); 88 | 89 | 90 | }).bind(this)); 91 | }, 92 | 93 | _provide_lint_items: function(editor, done) { 94 | 95 | var message_lines, entry, info, i, j, l, file_lines, start, end; 96 | 97 | var result = []; 98 | 99 | // Compute items 100 | if (this.compiler_errors.length > 0) { 101 | for (i = 0; i < this.compiler_errors.length; i++) { 102 | info = this.compiler_errors[i]; 103 | file_lines = null; 104 | 105 | // Error located on a characters range in the single line 106 | if (info.location === 'characters') { 107 | 108 | // Add one entry with the message 109 | entry = { 110 | text: info.message, 111 | line: info.line, 112 | filePath: info.file_path, 113 | range: new Range([info.line - 1, info.start], [info.line - 1, info.end]), 114 | type: info.type 115 | }; 116 | 117 | result.push(entry); 118 | } 119 | // Error located on multiple lines 120 | else if (info.location === 'lines') { 121 | // Use the editor content to locate lint only on visible character ranges 122 | if (file_lines == null) { 123 | // Use file contents to find the best range 124 | if (fs.existsSync(info.file_path)) { 125 | file_lines = String(fs.readFileSync(info.file_path)).split("\n"); 126 | 127 | // Get start and end in line after trimming left and right of the line 128 | start = file_lines[info.start - 1].length - file_lines[info.start - 1].replace(/^\s*/, '').length; 129 | end = file_lines[info.start - 1].replace(/\s*$/, '').length; 130 | 131 | // Add one entry with the message 132 | entry = { 133 | text: info.message, 134 | line: info.line, 135 | filePath: info.file_path, 136 | range: new Range([info.line - 1, start], [info.line - 1, end]), 137 | type: info.type 138 | }; 139 | 140 | result.push(entry); 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | done(result); 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /lib/parsing/compiler.js: -------------------------------------------------------------------------------- 1 | 2 | // lib code 3 | var state = require('../haxe-state') 4 | // node built in 5 | , path = require('path') 6 | 7 | // Match haxe compiler output info 8 | var REGEX_HAXE_COMPILER_OUTPUT_LINE = /^\s*(.+)?(?=\:[0-9]*\:)\:([0-9]+)\:\s+(characters|lines)\s+([0-9]+)\-([0-9]+)(?:\s+\:\s*(.*?))?\s*$/; 9 | // Match a warning message 10 | var REGEX_HAXE_WARNING_MESSAGE = /^Warning\s*\:\s*(.*)$/; 11 | 12 | module.exports = { 13 | 14 | // Parse haxe compiler output and extract info 15 | parse_output: function(output, options) { 16 | 17 | if (options == null) { 18 | options = {}; 19 | } 20 | 21 | var info = []; 22 | var prev_info = null; 23 | var lines = output.split("\n"); 24 | var cwd = state.hxml_cwd; 25 | var i, l, m, mtype, line, file_path, location, start, end, message, type; 26 | 27 | for (i = 0; i < lines.length; i++) { 28 | // Reset the regex index 29 | // This is still better than creating a new regex each time. 30 | REGEX_HAXE_COMPILER_OUTPUT_LINE.lastIndex = -1; 31 | 32 | line = lines[i]; 33 | 34 | if (info.length > 0) { 35 | prev_info = info[info.length - 1]; 36 | } 37 | 38 | if (m = line.match(REGEX_HAXE_COMPILER_OUTPUT_LINE)) { 39 | file_path = m[1]; 40 | line = parseInt(m[2], 10); 41 | location = m[3]; 42 | start = parseInt(m[4], 10); 43 | end = parseInt(m[5], 10); 44 | message = m[6]; 45 | type = 'Error'; 46 | 47 | if (mtype = message.match(REGEX_HAXE_WARNING_MESSAGE)) { 48 | type = 'Warning'; 49 | message = mtype[1]; 50 | } 51 | 52 | if (message != null || options.allow_empty_message) { 53 | 54 | // Make file_path absolute if possible 55 | if (cwd != null && !path.isAbsolute(file_path)) { 56 | file_path = path.join(cwd, file_path); 57 | } 58 | 59 | if (message != null 60 | && prev_info != null 61 | && prev_info.message != null 62 | && prev_info.file_path == file_path 63 | && prev_info.location == location 64 | && prev_info.line == line 65 | && prev_info.start == start 66 | && prev_info.end == end) { 67 | // Concatenate multiline message 68 | prev_info.message += "\n" + message; 69 | } 70 | else { 71 | info.push({ 72 | line: line, 73 | file_path: file_path, 74 | location: location, 75 | start: start, 76 | end: end, 77 | message: message, 78 | type: type 79 | }); 80 | } 81 | } 82 | } 83 | } //for lines 84 | 85 | // Prevent duplicate messages as this can happen, like multiple `Unexpected (` at the same location 86 | // We may want to remove this snippet in a newer haxe compiler version if the output is never duplicated anymore 87 | for (i = 0; i < info.length; i++) { 88 | message = info[i].message; 89 | if (message != null) { 90 | var message_lines = message.split("\n"); 91 | var all_lines_are_equal = true; 92 | if (message_lines.length > 1) { 93 | line = message_lines[0]; 94 | for (l = 1; l < message_lines.length; l++) { 95 | if (line != message_lines[l]) { 96 | all_lines_are_equal = false; 97 | break; 98 | } 99 | line = message_lines[l]; 100 | } 101 | // If all lines of message are equal, just keep one line 102 | if (all_lines_are_equal) { 103 | info[i].message = line; 104 | } 105 | } 106 | } 107 | } 108 | 109 | return info; 110 | 111 | } //parse_output 112 | 113 | } 114 | -------------------------------------------------------------------------------- /lib/parsing/signatures.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | 4 | 5 | //Based on 6 | // https://github.com/nadako/hxsublime/blob/master/src/SignatureHelper.hx 7 | //Not done, etc. 8 | 9 | //String->String 10 | getCloseChar:function( c ) { 11 | var r = ''; 12 | switch (c) { 13 | case "(": r = ")"; break; 14 | case "<": r = ">"; break; 15 | case "{": r = "}"; break; 16 | default: break; 17 | } 18 | return r; 19 | }, 20 | 21 | //String->String 22 | parseType: function(_intype) { 23 | 24 | var parenRegex = /^\((.*)\)$/; 25 | var argNameRegex = /^(\??\w+) : /; 26 | var monomorphRegex = /^Unknown<\d+>$/; 27 | 28 | // replace arrows to ease parsing ">" in type params 29 | var type = _intype.replace(/ -> /g, "%"); 30 | 31 | // prepare a simple toplevel signature without nested arrows 32 | // nested arrow can be in () or <> and we don't need to modify them, 33 | // so we store them separately in `groups` map and replace their occurence 34 | // with a group name in the toplevel string 35 | var toplevel = [];//StringBuf 36 | var groups = {}; //Map 37 | var closeStack = [];//new haxe.ds.GenericStack(); 38 | var depth = 0; 39 | var groupId = 0; 40 | 41 | for(var i = 0; i < type.length; ++i) { 42 | var char = type.charAt(i); 43 | if (char == "(" || char == "<" || char == "{") { 44 | depth++; 45 | closeStack.push(this.getCloseChar(char)); 46 | if (depth == 1) { 47 | groupId++; 48 | groups[groupId] = []; 49 | toplevel.push(char); 50 | toplevel.push('$'+groupId); 51 | continue; 52 | } 53 | } else if (char == closeStack[closeStack.length-1]) { 54 | closeStack.pop(); 55 | depth--; 56 | } 57 | 58 | if (depth == 0) { 59 | toplevel.push(char); 60 | } else { 61 | groups[groupId].push(char); 62 | } 63 | 64 | } //for 65 | 66 | // process a sigle type entry, replacing inner content from groups 67 | // and removing unnecessary parentheses, String->String 68 | var processType = function(_in_ptype) { 69 | 70 | var ptype = _in_ptype; 71 | var groupRegex = /\$(\d)+/g; 72 | var gr = groupRegex.exec(ptype); 73 | if(gr) { 74 | 75 | var swapgr = true; 76 | var gridx = 1; 77 | while(swapgr) { 78 | 79 | var grid = gr[gridx]; 80 | if(grid) { 81 | var groupId = parseInt(grid); 82 | var groupStr = groups[groupId].join(''); 83 | var idstr = '\\$'+groupId; 84 | var idreplace = new RegExp(idstr, 'g'); 85 | ptype = ptype.replace(idreplace, groupStr); 86 | ptype = ptype.replace(/%/g, "->"); 87 | gridx++; 88 | } else { 89 | swapgr = false; 90 | } 91 | 92 | } //while swapping 93 | } //gr 94 | 95 | var pr = parenRegex.exec(ptype); 96 | if(pr) { 97 | ptype = pr[1]; 98 | } 99 | 100 | return ptype; 101 | 102 | } //processType 103 | 104 | // split toplevel signature by the "%" (which is actually "->") 105 | var parts = toplevel.join('').split("%"); 106 | 107 | // get a return or variable type 108 | var returnType = processType(parts.pop()); 109 | 110 | // if there is only the return type, it's a variable 111 | // otherwise `parts` contains function arguments 112 | var isFunction = parts.length > 0; 113 | 114 | // format function arguments 115 | var args = []; 116 | for(var i = 0; i < parts.length; ++i) { 117 | var part = parts[i]; 118 | 119 | // get argument name and type 120 | // if function is not a method, argument name is generated by its position 121 | var argname = ''; 122 | var argtype = ''; 123 | var ar = argNameRegex.exec(part); 124 | if(ar) { 125 | argname = ar[1]; 126 | argtype = part.substr(ar[0].length, part.length); 127 | } else { 128 | argname = 'arg'+i; 129 | argtype = part; 130 | } 131 | 132 | argtype = processType(argtype); 133 | 134 | // we don't need to include the Void argument 135 | // because it represents absence of arguments 136 | if (argtype == "Void") { 137 | continue; 138 | } 139 | 140 | // if type is unknown, include only the argument name 141 | if(monomorphRegex.test(argtype)) { 142 | args.push(argname); 143 | } else { 144 | args.push(argname+':'+argtype); 145 | } 146 | 147 | } //each part 148 | 149 | // finally generate the signature 150 | var result = []; 151 | var res = { pre:_intype, sig:'', args:null, ret:null }; 152 | if (isFunction) { 153 | res.sig += "("; 154 | res.sig += args.join(", "); 155 | res.sig += ")"; 156 | res.args = args; 157 | res.ret = returnType; 158 | } else { 159 | res.sig = returnType; 160 | } 161 | 162 | return res; 163 | } 164 | 165 | } //sig_helper 166 | -------------------------------------------------------------------------------- /lib/reflect/declaration.js: -------------------------------------------------------------------------------- 1 | 2 | // lib code 3 | var state = require('../haxe-state') 4 | , run = require('../haxe-call') 5 | , uuid = require('../utils/uuid') 6 | , code = require('../utils/haxe-code') 7 | , compiler = require('../parsing/compiler') 8 | // node built in 9 | , path = require('path') 10 | // dep code 11 | , xml2js = require('xml2js') 12 | , fs = require('fs-extra') 13 | 14 | 15 | var REGEX_TYPE_PATH_AFTER = /^([A-Za-z0-9_]+)/; 16 | var REGEX_TYPE_PATH_BEFORE = /([\.A-Za-z0-9_]+)$/; 17 | var REGEX_ADDITIONAL_KEY_PATH = /(\.[A-Za-z0-9_]+)+(,|;|\(|\)|\s)?$/; 18 | var REGEX_IS_IDENTIFIER = /^([A-Za-z_][\.A-Za-z0-9_]+)$/; 19 | 20 | module.exports = { 21 | 22 | init: function() { 23 | 24 | // Don't try anything without valid hxml data 25 | if(!state.hxml_cwd) return; 26 | if(!state.hxml_content && !state.hxml_file) return; 27 | 28 | // Extract info from XML once, one second after the haxe server is supposed to be started 29 | // A better option could be to have an explicit event to catch when the server is ready 30 | var time = atom.config.get('haxe.server_activation_start_delay'); 31 | setTimeout(this._extract_info_from_xml_output.bind(this), (time + 1) * 1000.0); 32 | }, 33 | 34 | dispose: function() { 35 | this.info = null; 36 | this.hxml_for_info = null; 37 | }, 38 | 39 | jump: function() { 40 | 41 | // Don't try anything without valid hxml data 42 | if(!state.hxml_cwd) return; 43 | if(!state.hxml_content && !state.hxml_file) return; 44 | 45 | var editor = atom.workspace.getActiveTextEditor(); 46 | var file_path = editor.getPath(); 47 | 48 | if(!file_path) return; 49 | 50 | var buffer_pos = editor.getLastCursor().getBufferPosition().toArray(); 51 | var pretext = editor.getTextInBufferRange( [ [0,0], buffer_pos] ); 52 | var text = editor.getText(); 53 | var index = pretext.length; 54 | var posttext = text.slice(index); 55 | var info = {}; 56 | 57 | // Extract selected word (composed), if any 58 | REGEX_TYPE_PATH_BEFORE.lastIndex = -1; 59 | REGEX_TYPE_PATH_AFTER.lastIndex = -1; 60 | var word = ''; 61 | var m; 62 | if (m = pretext.match(REGEX_TYPE_PATH_BEFORE)) { 63 | word = m[1]; 64 | } 65 | if (m = posttext.match(REGEX_TYPE_PATH_AFTER)) { 66 | word += m[1]; 67 | } 68 | word = word.replace(/^\.+/, '').replace(/\.+$/, ''); 69 | 70 | // By checking if word is uppercase or not, we can 71 | // deduct whether it can be a type or not 72 | var word_is_uppercase_type = false; 73 | var word_last_dot_index = -1; 74 | var last_word_element = word; 75 | if (word.length > 0) { 76 | word_last_dot_index = word.lastIndexOf('.'); 77 | if (word_last_dot_index == -1) { 78 | if (word.charAt(0).toUpperCase() == word.charAt(0)) { 79 | word_is_uppercase_type = true; 80 | } 81 | } 82 | else { 83 | last_word_element = word.slice(word_last_dot_index + 1); 84 | if (last_word_element.charAt(0) != null 85 | && last_word_element.charAt(0).toUpperCase() == last_word_element.charAt(0)) { 86 | word_is_uppercase_type = true; 87 | } 88 | } 89 | } 90 | 91 | 92 | // Extract end of expression 93 | var expr_end = code.parse_end_of_expression(text, index); 94 | 95 | // If the expression end is just a key path, remove it as we want to query the requested word only 96 | REGEX_ADDITIONAL_KEY_PATH.lastIndex = -1; 97 | expr_end = expr_end.replace(REGEX_ADDITIONAL_KEY_PATH, '.'); 98 | 99 | var imports = code.extract_imports(text); 100 | 101 | // First, try to query haxe to get position as this is `the good way` to get our job done. 102 | // Unfortunately it doesn't work in all cases yet 103 | // If it fails, we fall back to a combination of compiler XML output and @type/@toplevel information 104 | // Overall, this rather quick implementation should still work in most common cases 105 | var find_declaration_from_position = function(word) { 106 | 107 | var display_arg = file_path + '@' + Buffer.byteLength(text.slice(0, index + expr_end.length), 'utf8') + '@position'; 108 | this._run_haxe_display(display_arg).then(function(result) { 109 | 110 | xml2js.parseString(result.err, function(err, json) { 111 | 112 | if (json != null) { 113 | // Position got resolved, let's jump! 114 | if (json.list != null && json.list.pos != null && json.list.pos.length > 0) { 115 | var parsed_pos = compiler.parse_output(json.list.pos[0], {allow_empty_message: true}); 116 | if (parsed_pos.length > 0) { 117 | parsed_pos = parsed_pos[0]; 118 | this._open({ 119 | file: parsed_pos.file_path, 120 | line: parsed_pos.line, 121 | name: word 122 | }); 123 | return; 124 | } 125 | } 126 | } 127 | 128 | // Fall back to the XML/@type/@toplevel resolution 129 | find_declaration_from_xml(word); 130 | 131 | }.bind(this)); 132 | 133 | }.bind(this)); 134 | 135 | }.bind(this); 136 | 137 | 138 | var find_declaration_from_type = function(word) { 139 | // Query haxe to get type 140 | var display_arg = file_path + '@' + Buffer.byteLength(text.slice(0, index + expr_end.length), 'utf8') + '@type'; 141 | this._run_haxe_display(display_arg).then(function(result) { 142 | 143 | xml2js.parseString(result.err, function(err, json) { 144 | 145 | if (json != null) { 146 | // Type got resolved, let's jump! 147 | if (json.type != null && info[json.type.trim()] != null) { 148 | this._open(info[json.type.trim()]); 149 | return; 150 | } 151 | } 152 | else { 153 | find_declaration_from_toplevel(word); 154 | } 155 | 156 | }.bind(this)); 157 | 158 | }.bind(this)); 159 | 160 | }.bind(this); 161 | 162 | 163 | var find_declaration_from_toplevel = function(word) { 164 | 165 | // No? Then use @toplevel to try to resolve identifier 166 | var index_for_toplevel = code.index_of_closest_block(text, index); 167 | 168 | display_arg = file_path + '@' + Buffer.byteLength(text.slice(0, index_for_toplevel), 'utf8') + '@toplevel'; 169 | this._run_haxe_display(display_arg).then(function(result) { 170 | 171 | xml2js.parseString(result.err, function(err, json) { 172 | 173 | if (json != null) { 174 | var types = this._parse_toplevel_list(json); 175 | 176 | if (types[word] != null) { 177 | // Is this identifier a local declaration? 178 | if (types[word] != null && types[word].kind === 'local') { 179 | this._open({ 180 | file: file_path, 181 | name: word, 182 | local_index: index 183 | }); 184 | return; 185 | } 186 | 187 | if (info[types[word].type] != null) { 188 | this._open(info[types[word].type]); 189 | return; 190 | } 191 | } 192 | 193 | } 194 | 195 | find_declaration_from_raw_word(word); 196 | 197 | }.bind(this)); 198 | 199 | }.bind(this)); 200 | 201 | }.bind(this); 202 | 203 | 204 | var find_declaration_from_raw_word = function(word) { 205 | 206 | // Try to extract file from raw word directly 207 | if (info[word] != null) { 208 | this._open(info[word]); 209 | return; 210 | } 211 | else { 212 | // Last hope, play with packages 213 | 214 | // Does it come from an alias? 215 | if (imports[word] != null && info[imports[word]] != null) { 216 | this._open(info[imports[word]]); 217 | return; 218 | } 219 | 220 | // Does it come from an imported package? 221 | for (var key in imports) { 222 | var imported_type = imports[key] + '.' + last_word_element; 223 | if (info[imported_type] != null) { 224 | this._open(info[imported_type]); 225 | return; 226 | } 227 | } 228 | 229 | // Is it a type of a parent package? 230 | var package_name = code.extract_package(text); 231 | var resolved_type; 232 | while (package_name.length > 0) { 233 | // Is it a type in the current package 234 | if (info[package_name + '.' + last_word_element] != null) { 235 | this._open(info[package_name + '.' + last_word_element]); 236 | return; 237 | } 238 | 239 | if (package_name.lastIndexOf('.') != -1) { 240 | package_name = package_name.slice(0, package_name.lastIndexOf('.')); 241 | } else { 242 | break; 243 | } 244 | } 245 | 246 | // Is there any other package having this type? 247 | for (var key in info) { 248 | if (key.slice(key.length - last_word_element.length - 1) == ('.' + last_word_element)) { 249 | this._open(info[key]); 250 | return; 251 | } 252 | } 253 | 254 | // If the word is composed of multiple identifiers, try removing one element 255 | if (word.lastIndexOf('.') != -1) { 256 | find_declaration_from_xml(word.slice(0, word.lastIndexOf('.'))); 257 | } 258 | } 259 | 260 | }.bind(this); 261 | 262 | 263 | var find_declaration_from_xml = function(word) { 264 | 265 | this._extract_info_from_xml_output().then(function(parsed_xml_info) { 266 | 267 | info = parsed_xml_info; 268 | 269 | REGEX_IS_IDENTIFIER.lastIndex = -1; 270 | if (REGEX_IS_IDENTIFIER.test(word) && word !== 'this') { 271 | // Don't try to extract type from the current word 272 | find_declaration_from_toplevel(word); 273 | } 274 | else { 275 | // The current word is not an identifier. Try to resolve to its type 276 | find_declaration_from_type(word); 277 | } 278 | 279 | }.bind(this)); 280 | 281 | }.bind(this); 282 | 283 | // Start resolution 284 | find_declaration_from_position(word); 285 | }, 286 | 287 | _open: function(info) { 288 | var file_path; 289 | var cwd = state.hxml_cwd; 290 | 291 | if (typeof(info) == 'string') { 292 | // Just open the file 293 | file_path = info; 294 | if (!path.isAbsolute(file_path)) { 295 | file_path = path.join(cwd, file_path); 296 | } 297 | atom.workspace.open(info); 298 | } 299 | else { 300 | file_path = info.file; 301 | if (!path.isAbsolute(file_path)) { 302 | file_path = path.join(cwd, file_path); 303 | } 304 | 305 | // Find identifier in content 306 | var identifier = info.name; 307 | if (identifier.lastIndexOf('.') != -1) { 308 | identifier = identifier.slice(identifier.lastIndexOf('.') + 1); 309 | } 310 | 311 | var contents = String(fs.readFileSync(file_path)); 312 | 313 | // Resolve local identifier 314 | if (info.line == null && info.local_index != null) { 315 | var index = code.find_local_declaration(contents, identifier, info.local_index); 316 | 317 | if (index != -1) { 318 | var lines = contents.slice(0, index).split("\n"); 319 | var line = lines.length - 1; 320 | var column = lines[lines.length - 1].length; 321 | this._update_editor(file_path, line, column); 322 | } 323 | else { 324 | // Nothing found 325 | } 326 | } 327 | else if (info.line != null) { 328 | // Find the characters in line from identifier 329 | var lines = contents.slice(0, index).split("\n"); 330 | var target_line = lines[info.line-1]; 331 | 332 | var regex = new RegExp('[^A-Za-z0-9_]' + identifier + '[^A-Za-z0-9_]'); 333 | var match; 334 | if (match = target_line.match(regex)) { 335 | var line = info.line - 1; 336 | var column = target_line.indexOf(match[0]) + match[0].length - identifier.length - 1; 337 | // We found it, let's open at the exact location 338 | this._update_editor(file_path, line, column); 339 | } 340 | else { 341 | this._update_editor(file_path, info.line - 1); 342 | } 343 | } 344 | else { 345 | var cleaned_contents = code.code_with_empty_comments_and_strings(contents); 346 | var keyword = info.kind; 347 | if (keyword === 'class') { 348 | keyword = '(?:class|interface)'; 349 | } 350 | var regex = new RegExp(keyword + '\\s+' + identifier + '[^A-Za-z0-9_]'); 351 | var match; 352 | if (match = cleaned_contents.match(regex)) { 353 | var index = cleaned_contents.indexOf(match[0]); 354 | var lines = contents.slice(0, index).split("\n"); 355 | var line = lines.length - 1; 356 | var column = lines[lines.length - 1].length + match[0].length - identifier.length - 1; 357 | 358 | // We found it, let's open at the exact location 359 | this._update_editor(file_path, line, column); 360 | } 361 | else { 362 | this._update_editor(file_path); 363 | } 364 | } 365 | 366 | } 367 | }, 368 | 369 | _update_editor: function(file_path, line, column) { 370 | 371 | var params = {}; 372 | if (line != null) params.initialLine = line; 373 | if (column != null) params.initialColumn = column; 374 | 375 | atom.workspace.open(file_path, params).then(function() { 376 | 377 | if (column != null) { 378 | // Select the identifier 379 | var editor = atom.workspace.getActiveTextEditor(); 380 | editor.selectToEndOfWord(); 381 | 382 | // Workaround to ensure the editor will be centered to the identifier 383 | // Without it, the identifier is often too much at the bottom of the screen 384 | editor.scrollToBottom(); 385 | editor.scrollToCursorPosition(); 386 | } 387 | 388 | }.bind(this)); 389 | }, 390 | 391 | _extract_info_from_xml_output: function() { 392 | return new Promise(function(resolve, reject) { 393 | 394 | // If info is already extracted, just return it 395 | if (this.info != null) { 396 | // Reset info if hxml is different than previous run 397 | if (this.hxml_for_info != state.hxml_content) { 398 | this.info = null; 399 | } else { 400 | return resolve(this.info); 401 | } 402 | } 403 | 404 | var port = atom.config.get('haxe.server_port'); 405 | var tmp_path = state.tmp_path; 406 | var hash = uuid.v1(); 407 | var xml_path = path.join(tmp_path, hash, 'info.xml'); 408 | 409 | fs.ensureDirSync(path.dirname(xml_path)); 410 | 411 | var args = []; 412 | args.push('--no-output'); 413 | 414 | if (port) { 415 | args.push('--connect'); 416 | args.push(String(port)); 417 | } 418 | 419 | args.push('-xml'); 420 | args.push(xml_path); 421 | 422 | args = state.as_args(args); 423 | 424 | run.haxe(args).then((function(result) { 425 | 426 | // Get xml contents 427 | var content = fs.readFileSync(xml_path); 428 | // Remove file 429 | fs.removeSync(path.dirname(xml_path)); 430 | 431 | xml2js.parseString(content, function (err, json) { 432 | 433 | if (json == null) return resolve({}); 434 | 435 | // Parse xml result 436 | var info = {}; 437 | var kinds = ['abstract', 'class', 'enum', 'typedef']; 438 | for (var i = 0; i < kinds.length; i++) { 439 | var kind = kinds[i]; 440 | var kind_entries = json.haxe[kind]; 441 | if (kind_entries != null && kind_entries.length > 0) { 442 | for (var j = 0; j < kind_entries.length; j++) { 443 | var entry = kind_entries[j]; 444 | info[entry.$.path] = { 445 | kind: kind, 446 | file: entry.$.file, 447 | module: entry.$.module, 448 | name: entry.$.path 449 | }; 450 | } 451 | } 452 | } 453 | 454 | var has_keys = false; 455 | for (var key in info) { 456 | has_keys = true; 457 | break; 458 | } 459 | 460 | if (has_keys) { 461 | this.hxml_for_info = state.hxml_content; 462 | this.info = info; 463 | resolve(this.info); 464 | } else { 465 | resolve({}); 466 | } 467 | 468 | }.bind(this)); 469 | 470 | }).bind(this)); 471 | 472 | }.bind(this)); 473 | }, 474 | 475 | _parse_toplevel_list: function(json) { 476 | 477 | var types = {}; 478 | 479 | // If json is not valid, return empty list 480 | if (json.il == null || json.il.i == null) return types; 481 | 482 | for (var i = 0; i < json.il.i.length; i++) { 483 | var raw_entry = json.il.i[i]; 484 | var name = raw_entry._; 485 | if (types[name] == null) { 486 | types[name] = { 487 | type: (String(raw_entry.$.t || raw_entry.$.p).trim()), 488 | kind: raw_entry.$.k 489 | }; 490 | } 491 | } 492 | 493 | return types; 494 | }, 495 | 496 | // Query haxe server to display information (type, toplevel, position) 497 | _run_haxe_display: function(display_arg) { 498 | 499 | var port = atom.config.get('haxe.server_port'); 500 | var args = []; 501 | args.push('--no-output'); 502 | 503 | if (port) { 504 | args.push('--connect'); 505 | args.push(String(port)); 506 | } 507 | 508 | args.push('--display'); 509 | args.push(display_arg); 510 | 511 | args = state.as_args(args); 512 | 513 | return run.haxe(args); 514 | } 515 | 516 | } 517 | -------------------------------------------------------------------------------- /lib/utils/escape-html.js: -------------------------------------------------------------------------------- 1 | 2 | //https://github.com/component/escape-html 3 | //from npm install escape-html 4 | 5 | module.exports = function(html) { 6 | return String(html) 7 | .replace(/&/g, '&') 8 | .replace(/"/g, '"') 9 | .replace(/'/g, ''') 10 | .replace(//g, '>'); 12 | } 13 | -------------------------------------------------------------------------------- /lib/utils/exec.js: -------------------------------------------------------------------------------- 1 | 2 | var spawn = require('child_process').spawn 3 | 4 | //runs a command with args, 5 | //returning a promise that will resolve with 6 | //{ out:..., err:..., code:... } 7 | //the promise does not reject. 8 | //pass the ondataout and ondataerr handlers 9 | //that will pass incremental changes to you 10 | //but this doesn't change the above behavior. 11 | 12 | module.exports = function(cmd, args, ondataout, ondataerr) { 13 | 14 | return new Promise(function(resolve, reject) { 15 | 16 | var total_err = ''; 17 | var total_out = ''; 18 | var proc = spawn(cmd, args); 19 | 20 | proc.stdout.on('data', function(data){ 21 | var s = data.toString('utf-8'); 22 | total_out += s; 23 | if(ondataout) ondataout(s); 24 | }); 25 | 26 | proc.stderr.on('data', function(data){ 27 | var s = data.toString('utf-8'); 28 | total_err += s; 29 | if(ondataerr) ondataerr(s); 30 | }); 31 | 32 | proc.on('close', function(code) { 33 | resolve({ 34 | out:total_out, 35 | err:total_err, 36 | code:code 37 | }); 38 | }); //on close 39 | 40 | }); //Promise 41 | 42 | } //module.exports 43 | -------------------------------------------------------------------------------- /lib/utils/extend.js: -------------------------------------------------------------------------------- 1 | 2 | var hasProp = {}.hasOwnProperty; 3 | 4 | module.exports = function(child, parent) { 5 | 6 | for (var key in parent) { 7 | if (hasProp.call(parent, key)) { 8 | child[key] = parent[key]; 9 | } 10 | } 11 | 12 | function ctor() { 13 | this.constructor = child; 14 | } 15 | 16 | ctor.prototype = parent.prototype; 17 | child.prototype = new ctor(); 18 | child.__super__ = parent.prototype; 19 | 20 | return child; 21 | 22 | } //module.exports 23 | -------------------------------------------------------------------------------- /lib/utils/haxe-code.js: -------------------------------------------------------------------------------- 1 | 2 | // Match any single/double quoted string 3 | var REGEX_BEGINS_WITH_STRING = /^(?:"(?:[^"\\]*(?:\\.[^"\\]*)*)"|'(?:[^'\\]*(?:\\.[^'\\]*)*)')/; 4 | var REGEX_BEGINS_WITH_REGEX = /^~\/(?:[^\/\\]*(?:\\.[^\/\\]*)*)\//; 5 | var REGEX_ENDS_WITH_BEFORE_CALL_CHAR = /[a-zA-Z0-9_\]\)]\s*$/; 6 | var REGEX_ENDS_WITH_BEFORE_SIGNATURE_CHAR = /[a-zA-Z0-9_\>]\s*$/; 7 | var REGEX_ENDS_WITH_KEY = /([a-zA-Z0-9_]+)\s*\:$/; 8 | var REGEX_ENDS_WITH_ALPHANUMERIC = /([a-zA-Z0-9_]+)$/; 9 | var REGEX_BEGINS_WITH_KEY = /^([a-zA-Z0-9_]+)\s*\:/; 10 | var REGEX_PACKAGE = /^package\s*([a-zA-Z0-9_]*(\.[a-zA-Z0-9_]+)*)/; 11 | var REGEX_ENDS_WITH_FUNCTION_DEF = /[^a-zA-Z0-9_]function(?:\s+[a-zA-Z0-9_]+)?(?:\<[a-zA-Z0-9_\<\>, ]+\>)?$/; 12 | var REGEX_IMPORT = /import\s*([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)(?:\s+(?:in|as)\s+([a-zA-Z0-9_]+))?/g; 13 | var REGEX_ENDS_WITH_FUNCTION_KEYWORD = /[^a-zA-Z0-9_]function\s*$/; 14 | 15 | module.exports = { 16 | 17 | // Parse a composed haxe type (that can be a whole function signature) 18 | // and return an object with all the informations walkable recursively (json-friendly) 19 | // A function type will have an `args` value next to the `type` value 20 | // while a regular type will only have a `type` value. 21 | // In case the type is itself named inside another function signature, a `name` value 22 | // Will be added to it. 23 | parse_composed_type: function(raw_composed_type, ctx) { 24 | 25 | var info = {}; 26 | var len = raw_composed_type.length; 27 | 28 | var current_item = ''; 29 | var items = []; 30 | var item_params = []; 31 | var c, item, sub_item, params; 32 | 33 | if (ctx == null) { 34 | ctx = { 35 | i: 0, // index 36 | stop: null // the character that stopped the last recursive call 37 | }; 38 | 39 | // Remove potential references to temporary package 40 | raw_composed_type = raw_composed_type.split('atom_tempfile__.').join(''); 41 | } 42 | 43 | // Iterate over each characters and parse groups recursively 44 | while (ctx.i < len) { 45 | c = raw_composed_type.charAt(ctx.i); 46 | 47 | if (c === '(') { 48 | ctx.i++; 49 | if (current_item.length > 0 && current_item.charAt(current_item.length - 1) === ':') { 50 | // New group, continue parsing in a sub call until the end of the group 51 | item = { 52 | name: current_item.slice(0, current_item.length-1), 53 | type: this.parse_composed_type(raw_composed_type, ctx) 54 | }; 55 | if (item.name.charAt(0) === '?') { 56 | item.optional = true; 57 | item.name = item.name.slice(1); 58 | } 59 | items.push(item); 60 | } else { 61 | items.push(this.parse_composed_type(raw_composed_type, ctx)); 62 | } 63 | current_item = ''; 64 | } 65 | else if (c === '<') { 66 | ctx.i++; 67 | 68 | // Add type parameters 69 | params = []; 70 | do { 71 | params.push(this.parse_composed_type(raw_composed_type, ctx)); 72 | } 73 | while (ctx.stop === ','); 74 | 75 | if (current_item.length > 0) { 76 | item = this.parse_composed_type(current_item); 77 | 78 | item.type = { 79 | type: item.type, 80 | params: params 81 | }; 82 | 83 | items.push(item); 84 | } 85 | item_params.push([]); 86 | current_item = ''; 87 | } 88 | else if (c === '{') { 89 | // Parse structure type 90 | if (current_item.length > 0 && current_item.charAt(current_item.length - 1) === ':') { 91 | item = { 92 | name: current_item.slice(0, current_item.length-1), 93 | type: this.parse_structure_type(raw_composed_type, ctx) 94 | }; 95 | if (item.name.charAt(0) === '?') { 96 | item.optional = true; 97 | item.name = item.name.slice(1); 98 | } 99 | items.push(item); 100 | } else { 101 | items.push(this.parse_structure_type(raw_composed_type, ctx)); 102 | } 103 | current_item = ''; 104 | } 105 | else if (c === ')') { 106 | ctx.i++; 107 | ctx.stop = ')'; 108 | break; 109 | } 110 | else if (c === '>') { 111 | ctx.i++; 112 | ctx.stop = '>'; 113 | break; 114 | } 115 | else if (c === ',') { 116 | ctx.i++; 117 | ctx.stop = ','; 118 | break; 119 | } 120 | else if (c === '-' && raw_composed_type.charAt(ctx.i + 1) === '>') { 121 | if (current_item.length > 0) { 122 | // Parse the current item as a composed type in case there are 123 | // nested groups inside 124 | items.push(this.parse_composed_type(current_item)); 125 | } 126 | current_item = ''; 127 | ctx.i += 2; 128 | } 129 | else if (c.trim() === '') { 130 | ctx.i++; 131 | } 132 | else { 133 | current_item += c; 134 | ctx.i++; 135 | } 136 | } 137 | 138 | // Stopped by end of string 139 | if (ctx.i >= len) { 140 | ctx.stop = null; 141 | } 142 | 143 | if (current_item.length > 0) { 144 | if (current_item.indexOf('->') != -1) { 145 | // Parse the current item as a composed type as there as still 146 | // nested groups inside 147 | items.push(this.parse_composed_type(current_item)); 148 | } 149 | else { 150 | items.push(this.parse_type(current_item)); 151 | } 152 | } 153 | 154 | if (items.length > 1) { 155 | // If multiple items were parsed, that means it is a function signature 156 | // Extract arguments and return type 157 | info.args = [].concat(items); 158 | info.type = info.args.pop(); 159 | if (info.args.length === 1 && info.args[0].type === 'Void') { 160 | info.args = []; 161 | } 162 | } 163 | else if (items.length === 1) { 164 | // If only 1 item was parsed, this is a simple type 165 | info = items[0]; 166 | } 167 | 168 | return info; 169 | }, 170 | 171 | 172 | // Parse structure type like {f:Int} 173 | // Can be nested. 174 | // Will update ctx.i (index) accordingly to allow 175 | // a parent method to continue parsing of a bigger string 176 | parse_structure_type: function(raw_structure_type, ctx) { 177 | 178 | var item = ''; 179 | var len = raw_structure_type.length; 180 | var number_of_lts = 0; 181 | var c; 182 | 183 | if (ctx == null) { 184 | ctx = { 185 | i: 0 // index 186 | }; 187 | } 188 | 189 | while (ctx.i < len) { 190 | c = raw_structure_type.charAt(ctx.i); 191 | 192 | if (c === '{') { 193 | number_of_lts++; 194 | ctx.i++; 195 | item += c; 196 | } 197 | else if (c === '}') { 198 | number_of_lts--; 199 | ctx.i++; 200 | item += c; 201 | if (number_of_lts <= 0) { 202 | break; 203 | } 204 | } 205 | else if (c.trim() === '') { 206 | ctx.i++; 207 | } 208 | else if (number_of_lts === 0) { 209 | item = '{}'; 210 | break; 211 | } 212 | else { 213 | item += c; 214 | ctx.i++; 215 | } 216 | } 217 | 218 | return { 219 | type: item 220 | }; 221 | }, 222 | 223 | // Parse haxe type / haxe named argument 224 | // It will return an object with a `type` value or with both a `type` and `name` values 225 | parse_type: function(raw_type) { 226 | 227 | var parts = raw_type.split(':'); 228 | var result = {}; 229 | 230 | if (parts.length === 2) { 231 | result.type = parts[1]; 232 | result.name = parts[0]; 233 | 234 | } else { 235 | result.type = parts[0]; 236 | } 237 | 238 | // Optional? 239 | if (result.name != null && result.name.charAt(0) === '?') { 240 | result.optional = true; 241 | result.name = result.name.slice(1); 242 | } 243 | 244 | return result; 245 | }, 246 | 247 | // Get string from parsed haxe type 248 | // It may be useful to stringify a sub-type (group) 249 | // of a previously parsed type 250 | string_from_parsed_type: function(parsed_type) { 251 | if (parsed_type == null) { 252 | return ''; 253 | } 254 | 255 | if (typeof(parsed_type) == 'object') { 256 | var result; 257 | 258 | if (parsed_type.args != null) { 259 | var str_args; 260 | if (parsed_type.args.length > 0) { 261 | var arg_items = []; 262 | var str_arg; 263 | for (var i = 0; i < parsed_type.args.length; i++) { 264 | str_arg = this.string_from_parsed_type(parsed_type.args[i]); 265 | if (parsed_type.args[i].args != null && parsed_type.args[i].args.length == 1) { 266 | str_arg = '(' + str_arg + ')'; 267 | } 268 | arg_items.push(str_arg); 269 | } 270 | str_args = arg_items.join('->'); 271 | } 272 | else { 273 | str_args = 'Void'; 274 | } 275 | 276 | if (parsed_type.type != null && parsed_type.type.args != null) { 277 | result = str_args + '->(' + this.string_from_parsed_type(parsed_type.type) + ')'; 278 | } else { 279 | result = str_args + '->' + this.string_from_parsed_type(parsed_type.type) 280 | } 281 | } 282 | else { 283 | result = this.string_from_parsed_type(parsed_type.type); 284 | } 285 | 286 | if (parsed_type.params != null && parsed_type.params.length > 0) { 287 | var params = []; 288 | for (var i = 0; i < parsed_type.params.length; i++) { 289 | params.push(this.string_from_parsed_type(parsed_type.params[i])); 290 | } 291 | 292 | result += '<' + params.join(',') + '>'; 293 | } 294 | 295 | return result; 296 | } 297 | 298 | return String(parsed_type); 299 | }, 300 | 301 | // Try to match a partial function call or declaration from the given 302 | // text and index position and return info if succeeded or null. 303 | // Default behavior is to parse function call only. If an options argument is given with a `parse_declaration` key to true, 304 | // it will instead only accept a signature which is a declaration (like `function foo(a:T, b|)`) 305 | // The provided info are: 306 | // `signature_start` the index of the opening parenthesis starting the function call signature 307 | // `number_of_args` the number of arguments between the signature start and the given index 308 | // `key_path` (optional) an array of keys, in case the index is inside an anonymous structure given as argument 309 | // `partial_key` (optional) a string of the key being written at the given index if inside an anonymous structure given as argument 310 | parse_partial_signature: function(original_text, index, options) { 311 | // Cleanup text 312 | text = this.code_with_empty_comments_and_strings(original_text.slice(0, index)); 313 | 314 | options = options || {}; 315 | 316 | var i = index - 1; 317 | var number_of_args = 0; 318 | var number_of_parens = 0; 319 | var number_of_braces = 0; 320 | var number_of_lts = 0; 321 | var number_of_brackets = 0; 322 | var number_of_unclosed_parens = 0; 323 | var number_of_unclosed_braces = 0; 324 | var number_of_unclosed_lts = 0; 325 | var number_of_unclosed_brackets = 0; 326 | var signature_start = -1; 327 | var did_extract_used_keys = false; 328 | var c, arg, m; 329 | var partial_arg = null; 330 | 331 | // A key path will be detected when giving 332 | // anonymous structure as argument. The key path will allow to 333 | // know exactly which key or value we are currently writing. 334 | // Coupled with typedefs, it can allow to compute suggestions for 335 | // anonymous structure keys and values 336 | var can_set_colon_index = !options.parse_declaration; 337 | var colon_index = -1; 338 | var key_path = []; 339 | var used_keys = []; 340 | var partial_key = null; 341 | 342 | while (i > 0) { 343 | c = text.charAt(i); 344 | 345 | if (c === '"' || c === '\'') { 346 | // Continue until we reach the beginning of the string 347 | while (i >= 0) { 348 | i--; 349 | if (text.charAt(i) === c) { 350 | i--; 351 | break; 352 | } 353 | } 354 | } 355 | else if (c === ',') { 356 | if (number_of_parens === 0 && number_of_braces === 0 && number_of_lts === 0 && number_of_brackets === 0) { 357 | can_set_colon_index = false; 358 | number_of_args++; 359 | if (partial_arg == null) { 360 | partial_arg = original_text.slice(i + 1, index).replace(/^\s+/, ''); 361 | } 362 | } 363 | i--; 364 | } 365 | else if (c === ')') { 366 | number_of_parens++; 367 | i--; 368 | } 369 | else if (c === '}') { 370 | number_of_braces++; 371 | i--; 372 | } 373 | else if (c === ']') { 374 | number_of_brackets++; 375 | i--; 376 | } 377 | else if (c === ':') { 378 | if (can_set_colon_index && number_of_braces === 0 && number_of_parens == 0 && number_of_lts === 0) { 379 | colon_index = i; 380 | can_set_colon_index = false; 381 | } 382 | i--; 383 | } 384 | else if (c === '{') { 385 | if (number_of_braces === 0) { 386 | // Reset number of arguments because we found that 387 | // all the already parsed text is inside an unclosed brace token 388 | number_of_args = 0; 389 | number_of_unclosed_braces++; 390 | 391 | if (!options.parse_declaration) { 392 | can_set_colon_index = true; 393 | 394 | if (!did_extract_used_keys) { 395 | // Extract already used keys 396 | used_keys = this.extract_used_keys_in_structure(text.slice(i+1)); 397 | did_extract_used_keys = true; 398 | } 399 | 400 | // Match key 401 | if (colon_index != -1) { 402 | REGEX_ENDS_WITH_KEY.lastIndex = -1; 403 | if (m = text.slice(0, colon_index + 1).match(REGEX_ENDS_WITH_KEY)) { 404 | key_path.unshift(m[1]); 405 | } 406 | } 407 | else if (key_path.length === 0) { 408 | REGEX_ENDS_WITH_ALPHANUMERIC.lastIndex = -1; 409 | if (m = text.slice(0, index).match(REGEX_ENDS_WITH_ALPHANUMERIC)) { 410 | partial_key = m[1]; 411 | } else { 412 | partial_key = ''; 413 | } 414 | } 415 | } 416 | } 417 | else { 418 | number_of_braces--; 419 | } 420 | i--; 421 | } 422 | else if (c === '(') { 423 | if (number_of_parens > 0) { 424 | 425 | number_of_parens--; 426 | 427 | // Ensure the unclosed brace is not a function body 428 | if (number_of_parens == 0 && number_of_unclosed_braces > 0 && REGEX_ENDS_WITH_FUNCTION_KEYWORD.test(text.slice(0, i))) { 429 | // In that case, this is not an anonymous structure 430 | return null; 431 | } 432 | 433 | i--; 434 | } 435 | else { 436 | REGEX_ENDS_WITH_BEFORE_CALL_CHAR.lastIndex = -1; 437 | REGEX_ENDS_WITH_BEFORE_SIGNATURE_CHAR.lastIndex = -1; 438 | if ((!options.parse_declaration && REGEX_ENDS_WITH_BEFORE_CALL_CHAR.test(text.slice(0, i))) 439 | || (options.parse_declaration && REGEX_ENDS_WITH_BEFORE_SIGNATURE_CHAR.test(text.slice(0, i)))) { 440 | 441 | REGEX_ENDS_WITH_FUNCTION_DEF.lastIndex = -1; 442 | if (text.slice(0, i).match(REGEX_ENDS_WITH_FUNCTION_DEF)) { 443 | if (!options.parse_declaration) { 444 | // Perform no completion on function definition signature 445 | return null; 446 | } 447 | } else if (options.parse_declaration) { 448 | return null; 449 | } 450 | number_of_args++; 451 | signature_start = i; 452 | if (partial_arg == null) { 453 | partial_arg = original_text.slice(i + 1, index).replace(/^\s+/, ''); 454 | } 455 | break; 456 | } 457 | else { 458 | // Reset number of arguments because we found that 459 | // all the already parsed text is inside an unclosed paren token 460 | number_of_args = 0; 461 | 462 | // Reset key path also if needed 463 | if (!options.parse_declaration) { 464 | can_set_colon_index = true; 465 | colon_index = -1; 466 | } 467 | 468 | number_of_unclosed_parens++; 469 | i--; 470 | } 471 | } 472 | } 473 | else if (number_of_parens === 0 && c === '>' && text.charAt(i - 1) !== '-') { 474 | number_of_lts++; 475 | i-- 476 | } 477 | else if (number_of_parens === 0 && c === '<') { 478 | if (number_of_lts > 0) { 479 | number_of_lts--; 480 | } else { 481 | // Reset number of arguments because we found that 482 | // all the already parsed text is inside an unclosed lower-than token 483 | number_of_args = 0; 484 | 485 | // Reset key path also if needed 486 | can_set_colon_index = true; 487 | colon_index = -1; 488 | 489 | number_of_unclosed_lts++; 490 | } 491 | i--; 492 | } 493 | else if (c === '[') { 494 | if (number_of_brackets > 0) { 495 | number_of_brackets--; 496 | } else { 497 | // Reset number of arguments because we found that 498 | // all the already parsed text is inside an unclosed lower-than token 499 | number_of_args = 0; 500 | 501 | // Reset key path also if needed 502 | can_set_colon_index = true; 503 | colon_index = -1; 504 | 505 | number_of_unclosed_brackets++; 506 | } 507 | i--; 508 | } 509 | else { 510 | i--; 511 | } 512 | } 513 | 514 | if (signature_start === -1) { 515 | return null; 516 | } 517 | 518 | var result = { 519 | signature_start: signature_start, 520 | number_of_args: number_of_args 521 | }; 522 | 523 | if (!options.parse_declaration && number_of_unclosed_braces > 0) { 524 | result.key_path = key_path; 525 | result.partial_key = partial_key; 526 | result.used_keys = used_keys; 527 | } 528 | 529 | // Add partial arg, only if it is not empty and doesn't finish with spaces 530 | if (partial_arg != null && partial_arg.length > 0 && partial_arg.trim().length == partial_arg.length) { 531 | result.partial_arg = partial_arg; 532 | } 533 | 534 | return result; 535 | }, 536 | 537 | // Find the position of the local declaration of the given identifier, from the given index. 538 | // It will take care of searching in scopes that can reach the index (thus, ignoring declarations in other code blocks) 539 | // Declarations can be: 540 | // * var `identifier` 541 | // * function `identifier` 542 | // * function foo(`identifier` 543 | // * function foo(arg1, `identifier` 544 | // * ... 545 | // Returns an index or -1 if nothing was found 546 | find_local_declaration: function(text, identifier, index) { 547 | 548 | // Cleanup text 549 | text = this.code_with_empty_comments_and_strings(text); 550 | 551 | var i = index - 1; 552 | var number_of_args = 0; 553 | var number_of_parens = 0; 554 | var number_of_braces = 0; 555 | var number_of_lts = 0; 556 | var number_of_brackets = 0; 557 | var number_of_unclosed_parens = 0; 558 | var number_of_unclosed_braces = 0; 559 | var number_of_unclosed_lts = 0; 560 | var number_of_unclosed_brackets = 0; 561 | var c, m; 562 | var identifier_last_char = identifier.charAt(identifier.length - 1); 563 | var regex_identifier_decl = new RegExp('(var|\\?|,|\\(|function)\\s*' + identifier + '$'); 564 | 565 | while (i > 0) { 566 | c = text.charAt(i); 567 | 568 | if (c === '"' || c === '\'') { 569 | // Continue until we reach the beginning of the string 570 | while (i >= 0) { 571 | i--; 572 | if (text.charAt(i) === c) { 573 | i--; 574 | break; 575 | } 576 | } 577 | } 578 | else if (c === identifier_last_char) { 579 | if (number_of_braces === 0 && number_of_lts === 0 && number_of_brackets === 0) { 580 | regex_identifier_decl.lastIndex = -1; 581 | if (m = text.slice(0, i + 1).match(regex_identifier_decl)) { 582 | if (m[1] === '(' || m[1] === '?' || m[1] === ',') { 583 | // Is the identifier inside a signature? Ensure we are in a function declaration signature, not a simple call 584 | var info = this.parse_partial_signature(text, i + 1, {parse_declaration: true}); 585 | if (info != null) { 586 | // Yes, return position 587 | return i - identifier.length + 1; 588 | } 589 | } else { 590 | // All right, the identifier has a variable or function declaration 591 | return i - identifier.length + 1; 592 | } 593 | } 594 | } 595 | i--; 596 | } 597 | else if (c === ')') { 598 | number_of_parens++; 599 | i--; 600 | } 601 | else if (c === '}') { 602 | number_of_braces++; 603 | i--; 604 | } 605 | else if (c === ']') { 606 | number_of_brackets++; 607 | i--; 608 | } 609 | else if (c === '{') { 610 | if (number_of_braces === 0) { 611 | number_of_unclosed_braces++; 612 | } 613 | else { 614 | number_of_braces--; 615 | } 616 | i--; 617 | } 618 | else if (c === '(') { 619 | if (number_of_parens > 0) { 620 | number_of_parens--; 621 | } 622 | else { 623 | number_of_unclosed_parens++; 624 | } 625 | i--; 626 | } 627 | else if (number_of_parens === 0 && c === '>' && text.charAt(i - 1) !== '-') { 628 | number_of_lts++; 629 | i-- 630 | } 631 | else if (number_of_parens === 0 && c === '<') { 632 | if (number_of_lts > 0) { 633 | number_of_lts--; 634 | } else { 635 | number_of_unclosed_lts++; 636 | } 637 | i--; 638 | } 639 | else if (c === '[') { 640 | if (number_of_brackets > 0) { 641 | number_of_brackets--; 642 | } else { 643 | number_of_unclosed_brackets++; 644 | } 645 | i--; 646 | } 647 | else { 648 | i--; 649 | } 650 | } 651 | 652 | return -1; 653 | }, 654 | 655 | // Extract used keys in structure 656 | // For instance, when writing: `{a: {b: "c"}, d: |`, used_keys will contain 'a' and 'd'. 657 | extract_used_keys_in_structure: function(cleaned_text) { 658 | 659 | var i = 0, len = cleaned_text.length; 660 | var number_of_braces = 0; 661 | var number_of_parens = 0; 662 | var number_of_lts = 0; 663 | var number_of_brackets = 0; 664 | var c; 665 | var used_keys = []; 666 | 667 | while (i < len) { 668 | c = cleaned_text.charAt(i); 669 | if (c === '{') { 670 | number_of_braces++; 671 | i++; 672 | } 673 | else if (c === '}') { 674 | number_of_braces--; 675 | i++; 676 | } 677 | else if (c === '(') { 678 | number_of_parens++; 679 | i++; 680 | } 681 | else if (c === ')') { 682 | number_of_parens--; 683 | i++; 684 | } 685 | else if (c === '[') { 686 | number_of_brackets++; 687 | i++; 688 | } 689 | else if (c === ']') { 690 | number_of_brackets--; 691 | i++; 692 | } 693 | else if (c === '<') { 694 | number_of_lts++; 695 | i++ 696 | } 697 | else if (c === '>' && cleaned_text.charAt(i - 1) !== '-') { 698 | number_of_lts--; 699 | i++; 700 | } 701 | else if (number_of_braces === 0 && number_of_parens === 0 && number_of_lts === 0 && number_of_brackets === 0) { 702 | REGEX_BEGINS_WITH_KEY.lastIndex = -1; 703 | if (m = cleaned_text.slice(i).match(REGEX_BEGINS_WITH_KEY)) { 704 | i += m[0].length; 705 | used_keys.push(m[1]); 706 | } 707 | else { 708 | i++; 709 | } 710 | } else { 711 | i++; 712 | } 713 | } 714 | 715 | return used_keys; 716 | }, 717 | 718 | // Return the given code after replacing single-line/multiline comments 719 | // and string contents with white spaces. Also replaces strings in regex format (~/.../). 720 | // In other words, the output will be the same haxe code, with the same text length 721 | // but strings will be only composed of spaces and comments completely replaced with spaces 722 | // Use this method to simplify later parsing of the code and/or make it more efficient 723 | // where you don't need string and comment contents 724 | code_with_empty_comments_and_strings: function(input) { 725 | 726 | var i = 0; 727 | var output = ''; 728 | var len = input.length; 729 | var is_in_single_line_comment = false; 730 | var is_in_multiline_comment = false; 731 | var matches, k; 732 | 733 | while (i < len) { 734 | 735 | if (is_in_single_line_comment) { 736 | if (input.charAt(i) === "\n") { 737 | is_in_single_line_comment = false; 738 | output += "\n"; 739 | } 740 | else { 741 | output += ' '; 742 | } 743 | i++; 744 | } 745 | else if (is_in_multiline_comment) { 746 | if (input.substr(i, 2) === '*/') { 747 | is_in_multiline_comment = false; 748 | output += ' '; 749 | i += 2; 750 | } 751 | else { 752 | if (input.charAt(i) === "\n") { 753 | output += "\n"; 754 | } 755 | else { 756 | output += ' '; 757 | } 758 | i++; 759 | } 760 | } 761 | else if (input.substr(i, 2) === '//') { 762 | is_in_single_line_comment = true; 763 | output += ' '; 764 | i += 2; 765 | } 766 | else if (input.substr(i, 2) === '/*') { 767 | is_in_multiline_comment = true; 768 | output += ' '; 769 | i += 2; 770 | } 771 | else if (input.charAt(i) === '\'' || input.charAt(i) === '"') { 772 | REGEX_BEGINS_WITH_STRING.lastIndex = -1; 773 | if (matches = input.substring(i).match(REGEX_BEGINS_WITH_STRING)) { 774 | var match_len = matches[0].length; 775 | output += '"'; 776 | for (k = 0; k < match_len - 2; k++) { 777 | output += ' '; 778 | } 779 | output += '"'; 780 | i += match_len; 781 | } 782 | else { 783 | // Input finishes with non terminated string 784 | // In that case, remove the partial string and put spaces 785 | while (i < len) { 786 | output += ' '; 787 | i++; 788 | } 789 | } 790 | } 791 | else if (input.charAt(i) === '~') { 792 | REGEX_BEGINS_WITH_REGEX.lastIndex = -1; 793 | if (matches = input.substring(i).match(REGEX_BEGINS_WITH_REGEX)) { 794 | var match_len = matches[0].length; 795 | output += '~/'; 796 | for (k = 1; k < match_len - 2; k++) { 797 | output += ' '; 798 | } 799 | output += '/'; 800 | i += match_len; 801 | } 802 | else { 803 | // Input finishes with non terminated regex 804 | // In that case, remove the partial regex and put spaces 805 | while (i < len) { 806 | output += ' '; 807 | i++; 808 | } 809 | } 810 | } 811 | else { 812 | output += input.charAt(i); 813 | i++; 814 | } 815 | } 816 | 817 | return output; 818 | }, 819 | 820 | 821 | index_of_closest_block: function(text, index) { 822 | if (index == null) index = 0; 823 | 824 | // Cleanup text 825 | text = this.code_with_empty_comments_and_strings(text.slice(index)); 826 | 827 | var i = 0; 828 | var len = text.length; 829 | var c; 830 | 831 | while (i < len) { 832 | c = text.charAt(i); 833 | 834 | if (c === '}') { 835 | return index + i; 836 | } 837 | else if (c === '{') { 838 | return index + i + 1; 839 | } 840 | 841 | i++; 842 | } 843 | 844 | return index + len - 1; 845 | }, 846 | 847 | // Extract end of expression 848 | parse_end_of_expression: function(text, index) { 849 | if (index == null) index = 0; 850 | 851 | // Cleanup text 852 | var original_text = text; 853 | text = this.code_with_empty_comments_and_strings(text.slice(index)); 854 | 855 | var i = 0; 856 | var len = text.length; 857 | var number_of_parens = 0; 858 | var m, c; 859 | var result = ''; 860 | 861 | while (i < len) { 862 | c = text.charAt(i); 863 | 864 | if (c === '(') { 865 | REGEX_ENDS_WITH_BEFORE_CALL_CHAR.lastIndex = -1; 866 | if (REGEX_ENDS_WITH_BEFORE_CALL_CHAR.test(original_text.slice(0, index + i))) { 867 | result += c; 868 | break; 869 | } 870 | number_of_parens++; 871 | result += c; 872 | i++; 873 | } 874 | else if (c === ')') { 875 | result += c; 876 | if (number_of_parens > 0) { 877 | number_of_parens--; 878 | i++; 879 | } else { 880 | break; 881 | } 882 | } 883 | else if (c === ';') { 884 | result += c; 885 | if (number_of_parens > 0) { 886 | i++; 887 | } else { 888 | break; 889 | } 890 | } 891 | else if (c === ',') { 892 | result += c; 893 | if (number_of_parens > 0) { 894 | i++; 895 | } else { 896 | break; 897 | } 898 | } 899 | else if (c.trim() === '') { 900 | result += c; 901 | if (number_of_parens > 0) { 902 | i++; 903 | } else { 904 | break; 905 | } 906 | } 907 | else { 908 | result += c; 909 | i++; 910 | } 911 | 912 | } 913 | 914 | return result; 915 | 916 | }, 917 | 918 | // Extract a mapping of imports 919 | // From the given haxe code contents. 920 | // Alias (in / as) are also parsed. 921 | extract_imports: function(input) { 922 | 923 | // Cleanup input 924 | input = this.code_with_empty_comments_and_strings(input); 925 | 926 | var imports = {}; 927 | 928 | // Run regexp 929 | REGEX_IMPORT.lastIndex = -1; 930 | input.replace(REGEX_IMPORT, function() { 931 | var match = ([]).slice.call(arguments, 0); 932 | if (match[2] != null) { 933 | imports[match[2]] = match[1]; 934 | } else { 935 | imports[match[1]] = match[1]; 936 | } 937 | }); 938 | 939 | return imports; 940 | }, 941 | 942 | // Extract a package (as string) 943 | // From the given haxe code contents. 944 | // Default package will be an empty string 945 | extract_package: function(input) { 946 | 947 | var i = 0; 948 | var len = input.length; 949 | var is_in_single_line_comment = false; 950 | var is_in_multiline_comment = false; 951 | var matches; 952 | 953 | while (i < len) { 954 | 955 | if (is_in_single_line_comment) { 956 | if (input.charAt(i) === "\n") { 957 | is_in_single_line_comment = false; 958 | } 959 | i++; 960 | } 961 | else if (is_in_multiline_comment) { 962 | if (input.substr(i, 2) === '*/') { 963 | is_in_multiline_comment = false; 964 | i += 2; 965 | } 966 | else { 967 | i++; 968 | } 969 | } 970 | else if (input.substr(i, 2) === '//') { 971 | is_in_single_line_comment = true; 972 | i += 2; 973 | } 974 | else if (input.substr(i, 2) === '/*') { 975 | is_in_multiline_comment = true; 976 | i += 2; 977 | } 978 | else if (input.charAt(i).trim() === '') { 979 | i++; 980 | } 981 | else if ((REGEX_PACKAGE.lastIndex = -1) && (matches = input.slice(i).match(REGEX_PACKAGE))) { 982 | return matches[1]; 983 | } 984 | else { 985 | // Something that is neither a comment or a package token shown up. 986 | // We are done 987 | return ''; 988 | } 989 | } 990 | 991 | return ''; 992 | }, 993 | 994 | // Return the content after having detected and replaced the package name 995 | replace_package: function(input, new_package_name) { 996 | 997 | var i = 0; 998 | var len = input.length; 999 | var is_in_single_line_comment = false; 1000 | var is_in_multiline_comment = false; 1001 | var matches; 1002 | 1003 | while (i < len) { 1004 | 1005 | if (is_in_single_line_comment) { 1006 | if (input.charAt(i) === "\n") { 1007 | is_in_single_line_comment = false; 1008 | } 1009 | i++; 1010 | } 1011 | else if (is_in_multiline_comment) { 1012 | if (input.substr(i, 2) === '*/') { 1013 | is_in_multiline_comment = false; 1014 | i += 2; 1015 | } 1016 | else { 1017 | i++; 1018 | } 1019 | } 1020 | else if (input.substr(i, 2) === '//') { 1021 | is_in_single_line_comment = true; 1022 | i += 2; 1023 | } 1024 | else if (input.substr(i, 2) === '/*') { 1025 | is_in_multiline_comment = true; 1026 | i += 2; 1027 | } 1028 | else if (input.charAt(i).trim() === '') { 1029 | i++; 1030 | } 1031 | else if ((REGEX_PACKAGE.lastIndex = -1) && (matches = input.slice(i).match(REGEX_PACKAGE))) { 1032 | // Package detected. Replace it 1033 | return input.slice(0, i) + 'package ' + new_package_name + input.slice(i + matches[0].length); 1034 | } 1035 | else { 1036 | // Something that is neither a comment or a package token shown up. 1037 | // No package in this file. Add the package at the beginning of the contents 1038 | return "package " + new_package_name + ";\n" + input; 1039 | } 1040 | } 1041 | 1042 | // No package in this file. Add the package at the beginning of the contents 1043 | return "package " + new_package_name + ";\n" + input; 1044 | } 1045 | 1046 | } //module.exports 1047 | -------------------------------------------------------------------------------- /lib/utils/hxml.js: -------------------------------------------------------------------------------- 1 | 2 | // Match any single/double quoted string 3 | var REGEX_BEGINS_WITH_STRING = /^(?:"(?:[^"\\]*(?:\\.[^"\\]*)*)"|\'(?:[^\'\\]*(?:\\.[^\'\\]*)*)\')/; 4 | 5 | module.exports = { 6 | 7 | // Parse hxml data and return an array of arguments (compatible with node child_process.spawn) 8 | parse_hxml_args: function(raw_hxml) { 9 | var args = []; 10 | var i = 0; 11 | var len = raw_hxml.length; 12 | var current_arg = ''; 13 | var prev_arg = null; 14 | var number_of_parens = 0; 15 | var c, m; 16 | 17 | while (i < len) { 18 | c = raw_hxml.charAt(i); 19 | 20 | if (c === '(') { 21 | if (prev_arg === '--macro') { 22 | number_of_parens++; 23 | } 24 | current_arg += c; 25 | i++ 26 | } 27 | else if (number_of_parens > 0 && c === ')') { 28 | number_of_parens--; 29 | current_arg += c; 30 | i++; 31 | } 32 | else if (c === '"' || c === '\'') { 33 | REGEX_BEGINS_WITH_STRING.lastIndex = -1; 34 | if (m = raw_hxml.slice(i).match(REGEX_BEGINS_WITH_STRING)) { 35 | current_arg += m[0]; 36 | i += m[0].length; 37 | } 38 | else { 39 | // This should not happen, but if it happens, just add the character 40 | current_arg += c; 41 | i++; 42 | } 43 | } 44 | else if (c.trim() === '') { 45 | if (number_of_parens == 0) { 46 | if (current_arg.length > 0) { 47 | prev_arg = current_arg; 48 | current_arg = ''; 49 | args.push(prev_arg); 50 | } 51 | } 52 | else { 53 | current_arg += c; 54 | } 55 | i++; 56 | } 57 | else { 58 | current_arg += c; 59 | i++; 60 | } 61 | 62 | } 63 | 64 | if (current_arg.length > 0) { 65 | args.push(current_arg); 66 | } 67 | 68 | return args; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /lib/utils/log.js: -------------------------------------------------------------------------------- 1 | 2 | //atom built in 3 | var atom_panels = require('atom-message-panel') 4 | , MessagePanelView = atom_panels.MessagePanelView 5 | , PlainMessageView = atom_panels.PlainMessageView 6 | 7 | module.exports = { 8 | 9 | visible : false, 10 | 11 | init: function() { 12 | 13 | this.view = new MessagePanelView({ 14 | title: 'Haxe' 15 | }); 16 | 17 | }, 18 | 19 | dispose: function() { 20 | 21 | }, 22 | 23 | //Public API 24 | 25 | show: function() { 26 | this.view.attach(); 27 | this.visible = true; 28 | }, 29 | 30 | hide: function() { 31 | this.view.close(); 32 | this.visible = false; 33 | }, 34 | 35 | toggle: function() { 36 | this.visible = !this.visible; 37 | if(this.visible) this.show(); 38 | else this.hide(); 39 | }, 40 | 41 | error: function(message, clear, sticky) { 42 | this._msg(message, 'error', clear, sticky); 43 | }, 44 | 45 | success: function(message, clear, sticky) { 46 | this._msg(message, 'success', clear, sticky); 47 | }, 48 | 49 | debug: function(message, clear, sticky) { 50 | var _debug = atom.config.get('haxe.debug_logging'); 51 | if(_debug) { 52 | this._msg(message, null, clear, sticky); 53 | } 54 | }, 55 | 56 | msg: function(message, clear, sticky) { 57 | this._msg(message, null, clear, sticky); 58 | }, 59 | 60 | info: function(message, clear, sticky) { 61 | this._msg(message, 'highlight', clear, sticky); 62 | }, 63 | 64 | //Internal 65 | 66 | _msg : function(message, type, clear, sticky) { 67 | 68 | var typename; 69 | 70 | if(clear) this.view.clear(); 71 | if(type) typename = 'text-'+type; 72 | 73 | var msg = new PlainMessageView({ 74 | message: message, 75 | className: typename 76 | }); 77 | 78 | this.view.add( msg ); 79 | this.view.body.scrollTop(1e10); 80 | this.show(); 81 | 82 | clearTimeout(this.timeoutid); 83 | 84 | if(!sticky) { 85 | //:TODO: could probably config the timeout 86 | this.timeoutid = setTimeout(this.hide.bind(this), 5000); 87 | } 88 | 89 | } //_msg 90 | 91 | } //module.exports 92 | -------------------------------------------------------------------------------- /lib/utils/uuid.js: -------------------------------------------------------------------------------- 1 | 2 | //https://github.com/defunctzombie/node-uuid 3 | //from npm install uuid 4 | 5 | // uuid.js 6 | // 7 | // Copyright (c) 2010-2012 Robert Kieffer 8 | // MIT License - http://opensource.org/licenses/mit-license.php 9 | 10 | // Unique ID creation requires a high quality random # generator. We feature 11 | // detect to determine the best RNG source, normalizing to a function that 12 | // returns 128-bits of randomness, since that's what's usually required 13 | var rb = require('crypto').randomBytes; 14 | var _rng = function() { 15 | return rb(16); 16 | }; 17 | 18 | // Maps for number <-> hex string conversion 19 | var _byteToHex = []; 20 | var _hexToByte = {}; 21 | for (var i = 0; i < 256; i++) { 22 | _byteToHex[i] = (i + 0x100).toString(16).substr(1); 23 | _hexToByte[_byteToHex[i]] = i; 24 | } 25 | 26 | // **`parse()` - Parse a UUID into it's component bytes** 27 | function parse(s, buf, offset) { 28 | var i = (buf && offset) || 0, ii = 0; 29 | 30 | buf = buf || []; 31 | s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) { 32 | if (ii < 16) { // Don't overflow! 33 | buf[i + ii++] = _hexToByte[oct]; 34 | } 35 | }); 36 | 37 | // Zero out remaining bytes if string was short 38 | while (ii < 16) { 39 | buf[i + ii++] = 0; 40 | } 41 | 42 | return buf; 43 | } 44 | 45 | // **`unparse()` - Convert UUID byte array (ala parse()) into a string** 46 | function unparse(buf, offset) { 47 | var i = offset || 0, bth = _byteToHex; 48 | return bth[buf[i++]] + bth[buf[i++]] + 49 | bth[buf[i++]] + bth[buf[i++]] + '-' + 50 | bth[buf[i++]] + bth[buf[i++]] + '-' + 51 | bth[buf[i++]] + bth[buf[i++]] + '-' + 52 | bth[buf[i++]] + bth[buf[i++]] + '-' + 53 | bth[buf[i++]] + bth[buf[i++]] + 54 | bth[buf[i++]] + bth[buf[i++]] + 55 | bth[buf[i++]] + bth[buf[i++]]; 56 | } 57 | 58 | // **`v1()` - Generate time-based UUID** 59 | // 60 | // Inspired by https://github.com/LiosK/UUID.js 61 | // and http://docs.python.org/library/uuid.html 62 | 63 | // random #'s we need to init node and clockseq 64 | var _seedBytes = _rng(); 65 | 66 | // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) 67 | var _nodeId = [ 68 | _seedBytes[0] | 0x01, 69 | _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5] 70 | ]; 71 | 72 | // Per 4.2.2, randomize (14 bit) clockseq 73 | var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff; 74 | 75 | // Previous uuid creation time 76 | var _lastMSecs = 0, _lastNSecs = 0; 77 | 78 | // See https://github.com/broofa/node-uuid for API details 79 | function v1(options, buf, offset) { 80 | var i = buf && offset || 0; 81 | var b = buf || []; 82 | 83 | options = options || {}; 84 | 85 | var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; 86 | 87 | // UUID timestamps are 100 nano-second units since the Gregorian epoch, 88 | // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so 89 | // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' 90 | // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. 91 | var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime(); 92 | 93 | // Per 4.2.1.2, use count of uuid's generated during the current clock 94 | // cycle to simulate higher resolution clock 95 | var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; 96 | 97 | // Time since last uuid creation (in msecs) 98 | var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000; 99 | 100 | // Per 4.2.1.2, Bump clockseq on clock regression 101 | if (dt < 0 && options.clockseq === undefined) { 102 | clockseq = clockseq + 1 & 0x3fff; 103 | } 104 | 105 | // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new 106 | // time interval 107 | if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) { 108 | nsecs = 0; 109 | } 110 | 111 | // Per 4.2.1.2 Throw error if too many uuids are requested 112 | if (nsecs >= 10000) { 113 | throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); 114 | } 115 | 116 | _lastMSecs = msecs; 117 | _lastNSecs = nsecs; 118 | _clockseq = clockseq; 119 | 120 | // Per 4.1.4 - Convert from unix epoch to Gregorian epoch 121 | msecs += 12219292800000; 122 | 123 | // `time_low` 124 | var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000; 125 | b[i++] = tl >>> 24 & 0xff; 126 | b[i++] = tl >>> 16 & 0xff; 127 | b[i++] = tl >>> 8 & 0xff; 128 | b[i++] = tl & 0xff; 129 | 130 | // `time_mid` 131 | var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff; 132 | b[i++] = tmh >>> 8 & 0xff; 133 | b[i++] = tmh & 0xff; 134 | 135 | // `time_high_and_version` 136 | b[i++] = tmh >>> 24 & 0xf | 0x10; // include version 137 | b[i++] = tmh >>> 16 & 0xff; 138 | 139 | // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) 140 | b[i++] = clockseq >>> 8 | 0x80; 141 | 142 | // `clock_seq_low` 143 | b[i++] = clockseq & 0xff; 144 | 145 | // `node` 146 | var node = options.node || _nodeId; 147 | for (var n = 0; n < 6; n++) { 148 | b[i + n] = node[n]; 149 | } 150 | 151 | return buf ? buf : unparse(b); 152 | } 153 | 154 | // **`v4()` - Generate random UUID** 155 | 156 | // See https://github.com/broofa/node-uuid for API details 157 | function v4(options, buf, offset) { 158 | // Deprecated - 'format' argument, as supported in v1.2 159 | var i = buf && offset || 0; 160 | 161 | if (typeof(options) == 'string') { 162 | buf = options == 'binary' ? new Array(16) : null; 163 | options = null; 164 | } 165 | options = options || {}; 166 | 167 | var rnds = options.random || (options.rng || _rng)(); 168 | 169 | // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` 170 | rnds[6] = (rnds[6] & 0x0f) | 0x40; 171 | rnds[8] = (rnds[8] & 0x3f) | 0x80; 172 | 173 | // Copy bytes to buffer, if provided 174 | if (buf) { 175 | for (var ii = 0; ii < 16; ii++) { 176 | buf[i + ii] = rnds[ii]; 177 | } 178 | } 179 | 180 | return buf || unparse(rnds); 181 | } 182 | 183 | // Export public API 184 | var uuid = v4; 185 | uuid.v1 = v1; 186 | uuid.v4 = v4; 187 | uuid.parse = parse; 188 | uuid.unparse = unparse; 189 | 190 | module.exports = uuid; 191 | -------------------------------------------------------------------------------- /menus/atom-haxe.cson: -------------------------------------------------------------------------------- 1 | 2 | 'menu': [ 3 | { 4 | 'label': 'Packages' 5 | 'submenu': [ 6 | 'label': 'Haxe' 7 | 'submenu': [ 8 | { 9 | 'label': 'Clear active project info' 10 | 'command': 'haxe:clear-project' 11 | }, 12 | { type: 'separator' }, 13 | { 14 | 'label': 'Start/Reset completion server' 15 | 'command': 'haxe:reset-server' 16 | }, 17 | { 18 | 'label': 'Stop completion server' 19 | 'command': 'haxe:stop-server' 20 | }, 21 | { type: 'separator' }, 22 | { 23 | 'label': 'Toggle completion debug' 24 | 'command': 'haxe:toggle-completion-debug' 25 | }, 26 | { 27 | 'label': 'Toggle log view' 28 | 'command': 'haxe:toggle-log-view' 29 | } 30 | ] 31 | ] 32 | } 33 | ] 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "haxe", 3 | "main": "./lib/haxe.js", 4 | "version": "0.8.14", 5 | "description": "atom.io haxe plugin, includes completion, error checking and more.", 6 | "activationCommands": {}, 7 | "providedServices": { 8 | "haxe-completion.provider": { 9 | "description": "Provides a haxe completion server, with query interface.", 10 | "versions": { 11 | "1.0.0": "completion_service" 12 | } 13 | }, 14 | "autocomplete.provider": { 15 | "versions": { 16 | "2.0.0": "provide" 17 | } 18 | }, 19 | "linter": { 20 | "versions": { 21 | "1.0.0": "provide_linter" 22 | } 23 | } 24 | }, 25 | "repository": "https://github.com/snowkit/atom-haxe", 26 | "license": "MIT", 27 | "engines": { 28 | "atom": ">=0.174.0 <2.0.0" 29 | }, 30 | "dependencies": { 31 | "xml2js": "~0.4.5", 32 | "fs-extra": "~0.16.5", 33 | "atom-message-panel": "~1.2.2", 34 | "temporary": "~0.0.8", 35 | "fuzzaldrin": "~2.1.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /settings/haxe.cson: -------------------------------------------------------------------------------- 1 | '.source.haxe': 2 | 'editor': 3 | 'commentStart': '// ' 4 | 'commentEnd': '' 5 | 'foldEndPattern': '^\\s*\\}|^\\s*\\]|^\\s*\\)' 6 | 'increaseIndentPattern': '(?x)\n\t\t(\n\t\t\t\\{ [^}"\']*\n\t\t|\t\\( [^)"\']*\n\t\t|\tcase[\\s\\S]*:\n\t\t|\tdefault:\n\t\t)\n\t\t$\n\t' 7 | 'decreaseIndentPattern': '^(.*\\*/)?\\s*(\\}|\\))' 8 | -------------------------------------------------------------------------------- /styles/atom-haxe.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | .atom-haxe { 8 | 9 | } 10 | 11 | .haxe-completion-log { 12 | max-height:100%; 13 | height:100%; 14 | } 15 | 16 | .haxe-completion-log-query-content, .haxe-completion-log-server-content { 17 | border:solid 1px #666; 18 | padding:1em; 19 | height:50%; 20 | max-height:50%; 21 | min-height:50%; 22 | width:32em; 23 | max-width:40em; 24 | overflow:auto; 25 | white-space:pre-wrap; 26 | } 27 | 28 | //use \n as newline in messages. 29 | //note this isn't really scoped to our 30 | //plugin yet :todo: 31 | .plain-message { 32 | white-space:pre-wrap; 33 | } 34 | 35 | 36 | // Fixing up autocomplete-plus for us 37 | atom-overlay { 38 | // see https://github.com/atom-community/autocomplete-plus/issues/255 39 | // https://github.com/atom-community/autocomplete-plus/issues/255#issuecomment-74010347 40 | autocomplete-suggestion-list.select-list.popover-list ol.list-group li { 41 | color: inherit; 42 | } 43 | } 44 | 45 | 46 | // Fixing up linter overlay for us 47 | // In order to be able to display multiline messages 48 | atom-text-editor[data-grammar="source haxe"] { 49 | atom-overlay { 50 | .linter-list { 51 | .linter-content { 52 | display: inline-block; 53 | max-width: 640px; 54 | white-space: normal; 55 | vertical-align: top; 56 | } 57 | } 58 | } 59 | } 60 | 61 | // Fixing up truncated right label 62 | atom-text-editor[data-grammar="source haxe"] { 63 | atom-overlay.autocomplete-plus autocomplete-suggestion-list.autocomplete-suggestion-list .right-label { 64 | max-width: none; 65 | } 66 | atom-overlay.autocomplete-plus autocomplete-suggestion-list.autocomplete-suggestion-list .left-label { 67 | max-width: none; 68 | } 69 | } 70 | 71 | // Type-hinting suggestion 72 | // or Error as suggestion 73 | atom-text-editor[data-grammar="source haxe"] { 74 | atom-overlay.autocomplete-plus autocomplete-suggestion-list.autocomplete-suggestion-list.select-list.popover-list { 75 | .haxe-autocomplete-suggestion-type-hint, 76 | .haxe-autocomplete-suggestion-type-hint.selected, 77 | .haxe-autocomplete-suggestion-error, 78 | .haxe-autocomplete-suggestion-error.selected { 79 | background-color: transparent; 80 | margin: 0; 81 | padding: 0; 82 | 83 | .word { 84 | display: none; 85 | } 86 | 87 | .selected { 88 | background-color: transparent; 89 | } 90 | 91 | .completion-label, .right-label { 92 | float: left; 93 | font-size: 12px; 94 | width: 100%; 95 | text-align: center; 96 | margin: 2px 0 0 0; 97 | padding: 0 5px; 98 | position: relative; 99 | left: -4px; 100 | top: 0; 101 | 102 | .current-argument { 103 | font-weight: bold; 104 | color: white; 105 | } 106 | } 107 | } 108 | 109 | .haxe-autocomplete-suggestion-error .completion-label, .haxe-autocomplete-suggestion-error .right-label { 110 | color: #c40000; 111 | } 112 | } 113 | } 114 | --------------------------------------------------------------------------------