├── .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 | 
63 | 
64 |
65 | #### code linting
66 | 
67 |
68 | #### build workflow
69 | 
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 |
--------------------------------------------------------------------------------