├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── grammars ├── stuff.cson ├── uno.cson ├── unoconfig.cson ├── unoproj.cson ├── unosln.cson ├── ux.cson └── uxl.cson ├── keymaps └── fuse.cson ├── lib ├── buildObserver.coffee ├── daemon.coffee ├── daemonConnection.coffee ├── errorListView.coffee ├── focusEditor.coffee ├── fuse.coffee ├── fuseBottomPanel.coffee ├── fuseLauncher.coffee ├── messageTypes.coffee ├── messages.coffee ├── outputView.coffee ├── preview.coffee ├── selectionChangedNotifier.coffee └── uxProvider.coffee ├── menus └── fuse.cson ├── package.json ├── settings ├── language-uno.cson └── language-ux.cson ├── snippets └── language-uno.cson ├── spec ├── fuse-spec.coffee └── fuse-view-spec.coffee └── styles ├── Fuse.png ├── fuse.less └── stylesheets.less /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{coffee,cson}] 4 | indent_style = space 5 | indent_size = 2 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.4.1 2 | - Fixed a problem during start-up when the Fuse daemon couldn't be started. We now log a warning instead of crashing. 3 | 4 | # 0.4.0 5 | - Fixed auto-indentation in UX and Unoproj 6 | 7 | # 0.3.2 8 | - Fixed potential problems after updating to Atom (v1.12) 9 | 10 | # 0.3.1 11 | ## Bugs 12 | - Fixed code suggestions for whenever carret is behind '/>' 13 | 14 | # 0.3.0 15 | ## Features 16 | - Add toggle comments support to UX (eg. Edit->Toggle Comments) 17 | 18 | ## Bugs 19 | - Fixed bug where plugin crashed if Fuse wasn't found. Atom will now instead show an error 20 | 21 | # 0.2.8 22 | ## Features 23 | - Fuse panel is not being visible before there are data there (it was always shown before, which users reported as annoying) 24 | 25 | # 0.2.7 26 | ## Bugs 27 | - Fixed crash reports related to not being able to send messages to daemon. 28 | 29 | ## Features 30 | - Added inline JavaScript syntax highlighting for UX. 31 | 32 | # 0.2.6 - Scroll To Bottom Patch 33 | ## Bugs 34 | - Fixed a bug where output panel was not scrolled to bottom when lines were appended. 35 | 36 | # 0.2.5 - First Official Release 37 | 38 | ## Features 39 | - Preview your app from Atom. 40 | - Syntax highlighting for Uno and UX. 41 | - Build and debug output including error list. 42 | - Selection of UX tags reflected in preview based on caret position. 43 | - Code completion in UX. 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fuse 2 | 3 | Atom Editor plugin for [Fuse](https://www.fusetools.com/). 4 | 5 | ## Current Features 6 | * Preview your app from Atom. 7 | * Syntax highlighting for Uno and UX. 8 | * Build and debug output including error list. 9 | * Selection of UX tags reflected in preview based on caret position. 10 | * Code completion in UX. 11 | 12 | ![A screenshot of your package](http://i.imgur.com/pFUfiLe.gif) 13 | 14 | ## Troubleshooting 15 | Make sure that language-fuse package is not installed or uninstall it, before using 16 | this package. 17 | -------------------------------------------------------------------------------- /grammars/stuff.cson: -------------------------------------------------------------------------------- 1 | 'scopeName': 'text.stuff' 2 | 'name': 'Stuff File' 3 | 'fileTypes': [ 4 | 'stuff' 5 | ] 6 | 'patterns': [ 7 | { 8 | 'include': 'source.js' 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /grammars/uno.cson: -------------------------------------------------------------------------------- 1 | 'scopeName': 'source.uno' 2 | 'name': 'Uno' 3 | 'fileTypes': [ 4 | 'uno' 5 | ] 6 | 'foldingStartMarker': '^\\s*#\\s*region|^\\s*/\\*|^(?![^{]*?//|[^{]*?/\\*(?!.*?\\*/.*?\\{)).*?\\{\\s*($|//|/\\*(?!.*?\\*/.*\\S))' 7 | 'foldingStopMarker': '^\\s*#\\s*endregion|^\\s*\\*/|^\\s*\\}' 8 | 'patterns': [ 9 | { 10 | 'captures': 11 | '1': 12 | 'name': 'keyword.other.using.source.uno' 13 | 'begin': '^\\s*(using)\b\s*' 14 | 'end': '\\s*(?:$|(;))' 15 | 'name': 'meta.keyword.using.source.uno' 16 | } 17 | { 18 | 'begin': '^\\s*((namespace)\\s+([\\w.]+))' 19 | 'beginCaptures': 20 | '1': 21 | 'name': 'meta.namespace.identifier.source.uno' 22 | '2': 23 | 'name': 'keyword.other.namespace.source.uno' 24 | '3': 25 | 'name': 'entity.name.type.namespace.source.uno' 26 | 'end': '}' 27 | 'endCaptures': 28 | '0': 29 | 'name': 'punctuation.section.namespace.end.source.uno' 30 | 'name': 'meta.namespace.source.uno' 31 | 'patterns': [ 32 | { 33 | 'begin': '{' 34 | 'beginCaptures': 35 | '0': 36 | 'name': 'punctuation.section.namespace.begin.source.uno' 37 | 'end': '(?=})' 38 | 'name': 'meta.namespace.body.source.uno' 39 | 'patterns': [ 40 | { 41 | 'include': '#code' 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | { 48 | 'include': '#code' 49 | } 50 | ] 51 | 'repository': 52 | 'block': 53 | 'patterns': [ 54 | { 55 | 'begin': '{' 56 | 'beginCaptures': 57 | '0': 58 | 'name': 'punctuation.section.block.begin.source.uno' 59 | 'end': '}' 60 | 'endCaptures': 61 | '0': 62 | 'name': 'punctuation.section.block.end.source.uno' 63 | 'name': 'meta.block.source.uno' 64 | 'patterns': [ 65 | { 66 | 'include': '#code' 67 | } 68 | ] 69 | } 70 | ] 71 | 'builtinTypes': 72 | 'patterns': [ 73 | { 74 | 'match': '\\b(bool|byte|sbyte|char|decimal|double|float([234]?|[234]x[234]?)|int[234]?|uint|long|ulong|object|short|ushort|string|void|class|struct|enum|interface|block)\\b' 75 | 'name': 'storage.type.source.uno' 76 | } 77 | ] 78 | 'class': 79 | 'begin': '(?=\\w?[\\w\\s]*(?:class|struct|interface|enum|block)\\s+\\w+)' 80 | 'end': '}' 81 | 'endCaptures': 82 | '0': 83 | 'name': 'punctuation.section.class.end.source.uno' 84 | 'name': 'meta.class.source.uno' 85 | 'patterns': [ 86 | { 87 | 'include': '#storage-modifiers' 88 | } 89 | { 90 | 'include': '#comments' 91 | } 92 | { 93 | 'captures': 94 | '1': 95 | 'name': 'storage.modifier.source.uno' 96 | '2': 97 | 'name': 'entity.name.type.class.source.uno' 98 | 'match': '(class|struct|interface|enum|block)\\s+(\\w+)' 99 | 'name': 'meta.class.identifier.source.uno' 100 | } 101 | { 102 | 'begin': ':' 103 | 'end': '(?={)' 104 | 'patterns': [ 105 | { 106 | 'captures': 107 | '1': 108 | 'name': 'storage.type.source.uno' 109 | 'match': '\\s*,?([A-Za-z_]\\w*)\\b' 110 | } 111 | ] 112 | } 113 | { 114 | 'begin': '{' 115 | 'beginCaptures': 116 | '0': 117 | 'name': 'punctuation.section.class.begin.source.uno' 118 | 'end': '(?=})' 119 | 'name': 'meta.class.body.source.uno' 120 | 'patterns': [ 121 | { 122 | 'include': '#method' 123 | } 124 | { 125 | 'include': '#code' 126 | } 127 | ] 128 | } 129 | ] 130 | 'code': 131 | 'patterns': [ 132 | { 133 | 'include': '#block' 134 | } 135 | { 136 | 'include': '#comments' 137 | } 138 | { 139 | 'include': '#class' 140 | } 141 | { 142 | 'include': '#constants' 143 | } 144 | { 145 | 'include': '#storage-modifiers' 146 | } 147 | { 148 | 'include': '#keywords' 149 | } 150 | { 151 | 'include': '#preprocessor' 152 | } 153 | { 154 | 'include': '#method-call' 155 | } 156 | { 157 | 'include': '#builtinTypes' 158 | } 159 | { 160 | 'include': '#documentation' 161 | } 162 | ] 163 | 'comments': 164 | 'patterns': [ 165 | { 166 | 'begin': '///' 167 | 'captures': 168 | '0': 169 | 'name': 'punctuation.definition.comment.source.uno' 170 | 'end': '$\\n?' 171 | 'name': 'comment.block.documentation.source.uno' 172 | 'patterns': [ 173 | { 174 | 'include': 'text.xml' 175 | } 176 | ] 177 | } 178 | { 179 | 'begin': '/\\*' 180 | 'captures': 181 | '0': 182 | 'name': 'punctuation.definition.comment.source.uno' 183 | 'end': '\\*/\\n?' 184 | 'name': 'comment.block.source.uno' 185 | } 186 | { 187 | 'begin': '//' 188 | 'captures': 189 | '1': 190 | 'name': 'punctuation.definition.comment.source.uno' 191 | 'end': '$\\n?' 192 | 'name': 'comment.line.double-slash.source.uno' 193 | } 194 | ] 195 | 'constants': 196 | 'patterns': [ 197 | { 198 | 'match': '\\b(true|false|null|this|base)\\b' 199 | 'name': 'constant.language.source.uno' 200 | } 201 | { 202 | 'match': '\\b((0(x|X)[0-9a-fA-F]*)|(([0-9]+\\.?[0-9]*)|(\\.[0-9]+))((e|E)(\\+|-)?[0-9]+)?)(L|l|UL|ul|u|U|F|f|ll|LL|ull|ULL)?\\b' 203 | 'name': 'constant.numeric.source.uno' 204 | } 205 | { 206 | 'captures': 207 | '0': 208 | 'name': 'punctuation.definition.string.begin.source.uno' 209 | 'match': '@"([^"]|"")*"' 210 | 'name': 'string.quoted.double.literal.source.uno' 211 | } 212 | { 213 | 'begin': '"' 214 | 'beginCaptures': 215 | '0': 216 | 'name': 'punctuation.definition.string.begin.source.uno' 217 | 'end': '"' 218 | 'endCaptures': 219 | '0': 220 | 'name': 'punctuation.definition.string.end.source.uno' 221 | 'name': 'string.quoted.double.source.uno' 222 | 'patterns': [ 223 | { 224 | 'match': '\\\\.' 225 | 'name': 'constant.character.escape.source.uno' 226 | } 227 | ] 228 | } 229 | { 230 | 'begin': '\'' 231 | 'beginCaptures': 232 | '0': 233 | 'name': 'punctuation.definition.string.begin.source.uno' 234 | 'end': '\'' 235 | 'endCaptures': 236 | '0': 237 | 'name': 'punctuation.definition.string.end.source.uno' 238 | 'name': 'string.quoted.single.source.uno' 239 | 'patterns': [ 240 | { 241 | 'match': '\\\\.' 242 | 'name': 'constant.character.escape.source.uno' 243 | } 244 | ] 245 | } 246 | ] 247 | 'keywords': 248 | 'patterns': [ 249 | { 250 | 'match': '\\b(if|else|while|for|foreach|in|do|return|continue|break|switch|case|default|goto|throw|try|catch|finally|lock|yield)\\b' 251 | 'name': 'keyword.control.source.uno' 252 | } 253 | { 254 | 'match': '\\b(from|where|select|group|into|orderby|join|let|on|equals|by|ascending|descending)\\b' 255 | 'name': 'keyword.linq.source.uno' 256 | } 257 | { 258 | 'match': '\\b(new|is|as|using|checked|unchecked|typeof|sizeof|override|readonly|stackalloc)\\b' 259 | 'name': 'keyword.operator.source.uno' 260 | } 261 | { 262 | 'match': '\\b(var|event|delegate|add|remove|set|get|value|apply)\\b' 263 | 'name': 'keyword.other.source.uno' 264 | } 265 | ] 266 | 'method': 267 | 'patterns': [ 268 | { 269 | 'begin': '\\[' 270 | 'end': '\\]' 271 | 'name': 'meta.method.annotation.source.uno' 272 | 'patterns': [ 273 | { 274 | 'include': '#constants' 275 | } 276 | { 277 | 'include': '#preprocessor' 278 | } 279 | { 280 | 'include': '#builtinTypes' 281 | } 282 | ] 283 | } 284 | { 285 | 'begin': '(?=\\bnew\\s+)(?=[\\w<].*\\s+)(?=[^=]+\\()' 286 | 'end': '(?={|;)' 287 | 'name': 'meta.new-object.source.uno' 288 | 'patterns': [ 289 | { 290 | 'include': '#code' 291 | } 292 | ] 293 | } 294 | { 295 | 'begin': '(?!new)(?=[\\w<].*\\s+)(?=[^=]+\\()' 296 | 'end': '(})|(?=;)' 297 | 'endCaptures': 298 | '1': 299 | 'name': 'punctuation.section.method.end.source.uno' 300 | 'name': 'meta.method.source.uno' 301 | 'patterns': [ 302 | { 303 | 'include': '#storage-modifiers' 304 | } 305 | { 306 | 'begin': '([\\w.]+)\\s*\\(' 307 | 'beginCaptures': 308 | '1': 309 | 'name': 'entity.name.function.source.uno' 310 | 'end': '\\)' 311 | 'name': 'meta.method.identifier.source.uno' 312 | 'patterns': [ 313 | { 314 | 'include': '#parameters' 315 | } 316 | ] 317 | } 318 | { 319 | 'begin': '(?=\\w.*\\s+[\\w.]+\\s*\\()' 320 | 'end': '(?=[\\w.]+\\s*\\()' 321 | 'name': 'meta.method.return-type.source.uno' 322 | 'patterns': [ 323 | { 324 | 'include': '#builtinTypes' 325 | } 326 | ] 327 | } 328 | { 329 | 'begin': ':\\s*(this|base)\\s*\\(' 330 | 'beginCaptures': 331 | '1': 332 | 'name': 'constant.language.source.uno' 333 | 'end': '\\)' 334 | 'name': 'meta.method.base-call.source.uno' 335 | 'patterns': [ 336 | { 337 | 'include': '#builtinTypes' 338 | } 339 | ] 340 | } 341 | { 342 | 'begin': '{' 343 | 'beginCaptures': 344 | '0': 345 | 'name': 'punctuation.section.method.begin.source.uno' 346 | 'end': '(?=})' 347 | 'name': 'meta.method.body.source.uno' 348 | 'patterns': [ 349 | { 350 | 'include': '#code' 351 | } 352 | ] 353 | } 354 | ] 355 | } 356 | { 357 | 'begin': '(?!new)(?=[\\w<].*\\s+)(?=[^=]+\\{)' 358 | 'end': '}' 359 | 'endCaptures': 360 | '0': 361 | 'name': 'punctuation.section.property.end.source.uno' 362 | 'name': 'meta.property.source.uno' 363 | 'patterns': [ 364 | { 365 | 'include': '#storage-modifiers' 366 | } 367 | { 368 | 'begin': '([\\w.]+)\\s*(?={)' 369 | 'captures': 370 | '1': 371 | 'name': 'entity.name.function.source.uno' 372 | 'end': '(?={)' 373 | 'name': 'meta.method.identifier.source.uno' 374 | } 375 | { 376 | 'begin': '(?=\\w.*\\s+[\\w.]+\\s*\\{)' 377 | 'end': '(?=[\\w.]+\\s*\\{)' 378 | 'name': 'meta.method.return-type.source.uno' 379 | 'patterns': [ 380 | { 381 | 'include': '#builtinTypes' 382 | } 383 | ] 384 | } 385 | { 386 | 'begin': '{' 387 | 'beginCaptures': 388 | '0': 389 | 'name': 'punctuation.section.property.begin.source.uno' 390 | 'end': '(?=})' 391 | 'name': 'meta.method.body.source.uno' 392 | 'patterns': [ 393 | { 394 | 'include': '#code' 395 | } 396 | ] 397 | } 398 | ] 399 | } 400 | ] 401 | 'method-call': 402 | 'begin': '([\\w$]+)(\\()' 403 | 'beginCaptures': 404 | '1': 405 | 'name': 'meta.method.source.uno' 406 | '2': 407 | 'name': 'punctuation.definition.method-parameters.begin.source.uno' 408 | 'end': '\\)' 409 | 'endCaptures': 410 | '0': 411 | 'name': 'punctuation.definition.method-parameters.end.source.uno' 412 | 'name': 'meta.method-call.source.uno' 413 | 'patterns': [ 414 | { 415 | 'match': ',' 416 | 'name': 'punctuation.definition.seperator.parameter.source.uno' 417 | } 418 | { 419 | 'include': '#code' 420 | } 421 | ] 422 | 'parameters': 423 | 'begin': '\\b(ref|params|out)?\\s*\\b([\\w.\\[\\]]+)\\s+(\\w+)\\s*(=)?' 424 | 'beginCaptures': 425 | '1': 426 | 'name': 'storage.type.modifier.source.uno' 427 | '2': 428 | 'name': 'storage.type.generic.source.uno' 429 | '3': 430 | 'name': 'variable.parameter.function.source.uno' 431 | '4': 432 | 'name': 'keyword.operator.assignment.source.uno' 433 | 'end': '(?:(,)|(?=[\\)]))' 434 | 'endCaptures': 435 | '1': 436 | 'name': 'punctuation.definition.separator.parameter.source.uno' 437 | 'patterns': [ 438 | { 439 | 'include': '#constants' 440 | } 441 | { 442 | 'include': '#block' 443 | } 444 | ] 445 | 'preprocessor': 446 | 'patterns': [ 447 | { 448 | 'captures': 449 | '2': 450 | 'name': 'meta.toc-list.region.source.uno' 451 | 'match': '^\\s*#\\s*(region)\\b(.*)$' 452 | 'name': 'meta.preprocessor.source.uno' 453 | } 454 | { 455 | 'captures': 456 | '2': 457 | 'name': 'entity.name.function.preprocessor.source.uno' 458 | 'match': '^\\s*#\\s*(define)\\b\\s*(\\S*)' 459 | 'name': 'meta.preprocessor.source.uno' 460 | } 461 | { 462 | 'captures': 463 | '2': 464 | 'name': 'keyword.control.import.source.uno' 465 | 'match': '^\\s*#\\s*(if|else|elif|endif|define|undef|warning|error|line|pragma|region|endregion)\\b' 466 | 'name': 'meta.preprocessor.source.uno' 467 | } 468 | ] 469 | 'storage-modifiers': 470 | 'match': '\\b(event|delegate|internal|public|protected|private|static|const|new|sealed|abstract|virtual|override|extern|unsafe|readonly|volatile|implicit|explicit|operator|async|partial|intrinsic|swizzler)\\b' 471 | 'name': 'storage.modifier.source.uno' 472 | -------------------------------------------------------------------------------- /grammars/unoconfig.cson: -------------------------------------------------------------------------------- 1 | 'scopeName': 'text.unoconfig' 2 | 'name': 'Uno Config' 3 | 'fileTypes': [ 4 | 'unoconfig' 5 | ] 6 | 'patterns': [ 7 | { 8 | 'include': 'source.js' 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /grammars/unoproj.cson: -------------------------------------------------------------------------------- 1 | 'scopeName': 'text.json.unoproj' 2 | 'name': 'Uno Project' 3 | 'fileTypes': [ 4 | 'unoproj' 5 | ] 6 | 'patterns': [ 7 | { 8 | 'include': 'source.json' 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /grammars/unosln.cson: -------------------------------------------------------------------------------- 1 | 'scopeName': 'text.unosln' 2 | 'name': 'Uno Solution' 3 | 'fileTypes': [ 4 | 'unosln' 5 | ] 6 | 'patterns': [ 7 | { 8 | 'include': 'text.xml' 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /grammars/ux.cson: -------------------------------------------------------------------------------- 1 | 'scopeName': 'text.xml.ux' 2 | 'name': 'UX' 3 | 'fileTypes': [ 4 | 'ux' 5 | ] 6 | 'patterns': [ 7 | { 8 | 'include': 'text.xml' 9 | } 10 | { 11 | 'begin': '(?:^\\s+)?(<)((?i:JavaScript))\\b(?![^>]*/>)' 12 | 'beginCaptures': 13 | '1': 14 | 'name': 'punctuation.definition.tag.xml' 15 | '2': 16 | 'name': 'entity.name.tag.javascript.ux' 17 | 'end': '(?<=)(?:\\s*\\n)?' 18 | 'endCaptures': 19 | '2': 20 | 'name': 'punctuation.definition.tag.xml' 21 | 'contentName': 'source.js.embedded.ux' 22 | 'patterns': [ 23 | { 24 | 'include': '#tag-stuff' 25 | } 26 | { 27 | 'begin': '(?)' 28 | 'captures': 29 | '1': 30 | 'name': 'punctuation.definition.tag.xml' 31 | '2': 32 | 'name': 'entity.name.tag.javascript.ux' 33 | 'end': '( 6 | super @dispose 7 | @emitter = new Emitter 8 | 9 | @subscriptions = new CompositeDisposable 10 | @subscriptions.add observeBroadcastedEvents 'Fuse.BuildStarted', false, @onBuildStarted 11 | @subscriptions.add observeBroadcastedEvents 'Fuse.BuildIssueDetected', false, @onBuildIssueDetected 12 | 13 | observeOnBuildStarted: (callback) -> 14 | @emitter.on 'build-started', callback 15 | 16 | observeOnBuildIssues: (callback) -> 17 | @emitter.on 'build-issue-detected', callback 18 | 19 | onBuildStarted: (msg) => 20 | @emitter.emit 'build-started', msg.data 21 | 22 | onBuildIssueDetected: (msg) => 23 | @emitter.emit 'build-issue-detected', msg.data 24 | 25 | dispose: -> 26 | @subscriptions.dispose() 27 | -------------------------------------------------------------------------------- /lib/daemon.coffee: -------------------------------------------------------------------------------- 1 | {Disposable, Emitter} = require 'atom' 2 | DaemonConnection = require './daemonConnection' 3 | {Event, Response, Request} = require './messageTypes' 4 | {SubscribeRequest, PublishServiceRequest} = require './messages' 5 | 6 | module.exports = 7 | class Daemon extends Disposable 8 | uniqueId: 0 9 | daemonConnection: null 10 | requestsInAir: {} 11 | requestListeners: {} 12 | eventSubscriptions: [] 13 | 14 | constructor: (fuseLauncher) -> 15 | super(@dispose) 16 | @emitter = new Emitter 17 | @daemonConnection = new DaemonReconnector( 18 | fuseLauncher, 19 | @messageFromDaemon, 20 | () => 21 | tmpCopy = @eventSubscriptions.splice(0) 22 | @eventSubscriptions = [] 23 | @observeBroadcastedEvents(sub.filter, sub.replay, sub.callback) for sub in tmpCopy 24 | for own requestName, callback of @requestListeners 25 | @registerRequestListener requestName, callback 26 | ) 27 | 28 | broadcastEvent: (event) -> 29 | @daemonConnection.send(event.messageType, event.serialize()) 30 | 31 | registerRequestListener: (requestName, callback) => 32 | publishServiceRequest = new PublishServiceRequest { 33 | requestNames: [ requestName ] 34 | } 35 | 36 | @request(publishServiceRequest, (response) => 37 | if response.status != "Success" 38 | console.log "fuse: Failed to register " + requestName " request" 39 | else 40 | @requestListeners[requestName] = callback 41 | console.log "fuse: Successfully registered " + requestName + " request" 42 | ) 43 | 44 | observeBroadcastedEvents: (filter, replay, callback) => 45 | subscriptionId = @getUniqueId() 46 | subscribeRequest = new SubscribeRequest { 47 | filter: filter, 48 | replay: replay, 49 | subscriptionId: subscriptionId 50 | } 51 | 52 | @request(subscribeRequest, (response) -> 53 | if response.status != "Success" 54 | console.log("Failed to subscribe to events. " + 55 | "Filter: #{filter}, Replay: #{replay}") 56 | ) 57 | 58 | @eventSubscriptions.push filter: filter, replay: replay, callback: callback 59 | 60 | return @emitter.on 'new-event', (msg) => 61 | callback(msg) if msg.subscriptionId == subscriptionId 62 | 63 | messageFromDaemon: (msg) => 64 | if msg instanceof Response 65 | request = @requestsInAir[msg.id] 66 | if not request? 67 | console.log( 68 | 'fuse: Got response however response id does not match any request.') 69 | return 70 | 71 | request.callback(msg) 72 | else if msg instanceof Event 73 | @emitter.emit 'new-event', msg 74 | 75 | else if msg instanceof Request 76 | callback = @requestListeners[msg.name] 77 | if callback? 78 | callback msg, (response) => 79 | console.log("Sending response:") 80 | @daemonConnection.send response.messageType, response.serialize() 81 | else 82 | console.log('fuse: Received request with name ' + msg.name + ' having no registered listener') 83 | 84 | request: (request, callback) => 85 | if not callback? 86 | throw new Error("Expected callback to not be undefined.") 87 | 88 | id = @getUniqueId() 89 | serializedMsg = request.serialize(id) 90 | @requestsInAir[id] = { callback: callback } 91 | @daemonConnection.send(request.messageType, serializedMsg) 92 | 93 | getUniqueId: -> 94 | return @uniqueId++ 95 | 96 | dispose: => 97 | @eventSubscriptions = [] 98 | @daemonConnection.dispose() 99 | 100 | class DaemonReconnector extends Disposable 101 | constructor: (@fuseLauncher, @msgReceivedCallback, @onReconnected) -> 102 | super(@dispose) 103 | @daemonConnection = @connect() 104 | 105 | connect: -> 106 | try 107 | connection = new DaemonConnection(@fuseLauncher, @msgReceivedCallback, @onLostConnection) 108 | return connection 109 | catch error 110 | console.log error 111 | 112 | send: (msgType, serializedMsg) => 113 | if not @daemonConnection? 114 | console.log('fuse: Connects to daemon again.') 115 | @daemonConnection = @connect() 116 | @onReconnected?() 117 | 118 | @daemonConnection.send(msgType, serializedMsg) 119 | 120 | onLostConnection: => 121 | @daemonConnection = null 122 | 123 | dispose: => 124 | @daemonConnection?.dispose() 125 | @daemonConnection = null 126 | -------------------------------------------------------------------------------- /lib/daemonConnection.coffee: -------------------------------------------------------------------------------- 1 | {Disposable} = require 'atom' 2 | {spawn} = require 'child_process' 3 | {Event, Message} = require './messageTypes' 4 | 5 | module.exports = 6 | class DaemonConnection extends Disposable 7 | fuseClient: null 8 | 9 | constructor: (fuseLauncher, @msgReceivedCallback, @onExit) -> 10 | super(@dispose) 11 | @fuseClient = fuseLauncher.run ['daemon-client', 'Atom Plugin'] 12 | 13 | buffer = new Buffer(0) 14 | @fuseClient.stdout.on('data', (data) => 15 | latestBuf = Buffer.concat([buffer, data]) 16 | buffer = @parseMsgFromBuffer(latestBuf, @msgReceivedCallback) 17 | ) 18 | 19 | @fuseClient.stderr.on('data', (data) -> 20 | console.log(data.toString('utf-8')) 21 | ) 22 | 23 | @fuseClient.on('close', (code) => 24 | console.log('fuse: daemon client closed with code ' + code) 25 | @onExit?() 26 | ) 27 | 28 | @fuseClient.on('error', => 29 | console.log 'fuse: deamon failed to start' 30 | @onExit?() 31 | ) 32 | 33 | parseMsgFromBuffer: (buffer, msgCallback) => 34 | start = 0 35 | while start < buffer.length 36 | endOfMsgType = buffer.indexOf('\n', start) 37 | if(endOfMsgType < 0) 38 | break # Incomplete or corrupt data 39 | 40 | startOfLength = endOfMsgType + 1 41 | endOfLength = buffer.indexOf('\n', startOfLength) 42 | if(endOfLength < 0) 43 | break # Incomplete or corrupt data 44 | 45 | msgType = buffer.toString('utf-8', start, endOfMsgType) 46 | length = parseInt(buffer.toString('utf-8', startOfLength, endOfLength)) 47 | if length == NaN 48 | console.log('fuse: Corrupt length in data received from Fuse.') 49 | # Try recover by starting from the beginning 50 | start = endOfLength + 1 51 | continue 52 | 53 | startOfData = endOfLength + 1 54 | endOfData = startOfData + length 55 | if buffer.length < endOfData 56 | break # Incomplete data 57 | 58 | jsonData = buffer.toString('utf-8', startOfData, endOfData) 59 | msgCallback? Message.deserialize(msgType, jsonData) 60 | start = endOfData 61 | 62 | return buffer.slice(start, buffer.length) 63 | 64 | send: (msgType, serializedMsg) => 65 | # Pack the message to be compatible with Fuse Protocol. 66 | # As: 67 | # ``` 68 | # MessageType (msgType) 69 | # Length (length) 70 | # JSON(serializedMsg) 71 | # ``` 72 | # For example: 73 | # ``` 74 | # Event 75 | # 50 76 | # { 77 | # "Name": "Test", 78 | # "Data": 79 | # { 80 | # "Foo": "Bar" 81 | # } 82 | # } 83 | # ``` 84 | length = Buffer.byteLength(serializedMsg, 'utf-8') 85 | packedMsg = msgType + '\n' + length + '\n' + serializedMsg 86 | try 87 | @fuseClient.stdin.write packedMsg 88 | catch e 89 | console.log e 90 | 91 | dispose: => 92 | @fuseClient.kill() 93 | -------------------------------------------------------------------------------- /lib/errorListView.coffee: -------------------------------------------------------------------------------- 1 | {$, $$, View, TextEditorView, ScrollView} = require 'atom-space-pen-views' 2 | {Point, Emitter} = require 'atom' 3 | 4 | module.exports = 5 | ErrorListModel: 6 | class ErrorListModel 7 | buildEvents: [] 8 | 9 | constructor: (@buildObserver) -> 10 | @emitter = new Emitter 11 | 12 | lastId = -1 13 | @buildObserver.observeOnBuildStarted (data) => 14 | if lastId != data.BuildId 15 | lastId = data.BuildId 16 | @clear() 17 | 18 | @buildObserver.observeOnBuildIssues (data) => 19 | if lastId != data.BuildId 20 | return 21 | position = data.StartPosition ? {Line: 0, Character: 0} 22 | position = new Point(position.Line - 1, position.Character - 1) 23 | @report({ 24 | type: data.IssueType, 25 | description: data.Message, 26 | file: data.Path, 27 | position: position 28 | }) 29 | @focus() 30 | 31 | observeBuildEvents: (callback) -> 32 | callback(buildEvent) for buildEvent in @buildEvents 33 | return @emitter.on 'new-build-event', callback 34 | 35 | report: (args) -> 36 | @buildEvents.push args 37 | @emitter.emit 'new-build-event', args 38 | 39 | observeOnClear: (callback) -> 40 | return @emitter.on 'clear-build-events', callback 41 | 42 | openEditorForPath: (file, position) -> 43 | if not file 44 | return 45 | atom.workspace.open(file, initialLine: position.row, initialColumn: position.column) 46 | 47 | onFocusChanged: (callback) -> 48 | @emitter.on 'focus', callback 49 | 50 | focus: -> 51 | @emitter.emit 'focus' 52 | 53 | clear: -> 54 | @buildEvents = [] 55 | @emitter.emit 'clear-build-events' 56 | 57 | ErrorListView: 58 | class ErrorListView extends View 59 | @content: -> 60 | @div => 61 | @table class: 'error-list-table native-key-bindings', tabindex: -1, => 62 | @thead => 63 | @tr => 64 | @th 'Type' 65 | @th 'Description' 66 | @th 'File' 67 | @th 'Line : Column' 68 | @tbody outlet: 'errorTableBody' 69 | 70 | initialize: (@model) -> 71 | @handleEvents() 72 | 73 | @buildEventsSub = @model?.observeBuildEvents (evt) => 74 | @report(evt.type, evt.description, evt.file, evt.position) 75 | 76 | @clearSub = @model?.observeOnClear => 77 | @clear() 78 | 79 | destroy: -> 80 | @buildEventsSub?.dispose() 81 | @clearSub?.dispose() 82 | 83 | clear: -> 84 | @errorTableBody.empty() 85 | 86 | report: (type, message, file, position) -> 87 | pos = {line: position.row + 1, character: position.column + 1} 88 | if typeof message == 'string' 89 | @errorTableBody.append "#{type}#{message}#{file}#{pos.line} : #{pos.character}" 90 | else 91 | @errorTableBody.append message 92 | 93 | @show() 94 | 95 | handleEvents: -> 96 | @on 'dblclick', '.error-list-table tbody tr', @errorDoubleClicked 97 | 98 | errorDoubleClicked: (e) => 99 | target = e.currentTarget 100 | path = target.cells[2].outerText 101 | lineCol = target.cells[3].outerText.split(' : ') 102 | @model.openEditorForPath path, new Point(parseInt(lineCol[0]) - 1, parseInt(lineCol[1]) - 1) 103 | -------------------------------------------------------------------------------- /lib/focusEditor.coffee: -------------------------------------------------------------------------------- 1 | {Response} = require './messageTypes' 2 | 3 | module.exports = 4 | FocusEditorListener: 5 | class FocusEditorListener 6 | constructor: (registerRequestListener) -> 7 | registerRequestListener 'FocusEditor', @onFocusEditorRequest 8 | 9 | onFocusEditorRequest: (request, responder) -> 10 | args = request.arguments 11 | console.log "fuse: Bringing focus to " + args.File + "(" + args.Line + "," + args.Column + ")" 12 | if atom.project.contains args.Project 13 | atom.workspace.open args.File, { initialLine: args.Line - 1, initialColumn: args.Column - 1, searchAllPanes: true } 14 | atom.focus() 15 | responder new Response(request.id, "Success", [], {}) 16 | else 17 | responder new Response(request.id, "Unhandled", [], {}) 18 | 19 | -------------------------------------------------------------------------------- /lib/fuse.coffee: -------------------------------------------------------------------------------- 1 | SelectionChangedNotifier = require './selectionChangedNotifier' 2 | {FocusEditorListener} = require './focusEditor' 3 | Daemon = require './daemon' 4 | UXProvider = require './uxProvider' 5 | BuildObserver = require './buildObserver' 6 | {ErrorListView, ErrorListModel} = require './errorListView' 7 | {SubscribeRequest,FocusDesignerRequest} = require './messages' 8 | process = require 'process' 9 | {CompositeDisposable, Disposable, Point} = require 'atom' 10 | FuseBottomPanel = require './fuseBottomPanel' 11 | FuseLauncher = require './fuseLauncher' 12 | Preview = require './preview' 13 | Path = require 'path' 14 | {OutputView, LogEvent, OutputModel} = require './outputView' 15 | 16 | module.exports = Fuse = 17 | config: 18 | fuseCommand: 19 | type: 'string' 20 | default: 'fuse' 21 | description: 'Set absolute path/name of fuse executable.' 22 | fuseSelection: 23 | type: 'boolean' 24 | default: 'true' 25 | description: 'Enable selection of UX tags reflected in preview based on caret position.' 26 | 27 | subscriptions: null 28 | daemon: null 29 | 30 | activate: (state) -> 31 | console.log('fuse: Starting fuse.') 32 | if process.platform == 'darwin' 33 | process.env["PATH"] += ':/usr/local/bin' 34 | 35 | fuseLauncher = new FuseLauncher atom.config.get('fuse.fuseCommand') 36 | 37 | @subscriptions = new CompositeDisposable 38 | @daemon = new Daemon(fuseLauncher) 39 | @subscriptions.add @daemon 40 | @subscriptions.add new SelectionChangedNotifier @daemon 41 | 42 | @fuseBottomPanel = new FuseBottomPanel state.fuseBottomPanel 43 | atom.workspace.addBottomPanel(item: @fuseBottomPanel, visibility: false, priority: 100) 44 | 45 | @subscriptions.add atom.commands.add 'atom-workspace', 'fuse:panel': => 46 | @fuseBottomPanel.toggle() 47 | 48 | buildObserver = new BuildObserver @daemon.observeBroadcastedEvents 49 | @subscriptions.add buildObserver 50 | 51 | focusEditorListener = new FocusEditorListener @daemon.registerRequestListener 52 | 53 | errorlistModel = new ErrorListModel buildObserver 54 | outputModel = new OutputModel buildObserver 55 | 56 | @fuseBottomPanel.addTab 'Error List', -> 57 | new ErrorListView errorlistModel 58 | @fuseBottomPanel.addTab 'Output', -> 59 | new OutputView outputModel 60 | 61 | @subscriptions.add errorlistModel.onFocusChanged () => 62 | @fuseBottomPanel.focusTab 'Error List' 63 | 64 | @subscriptions.add outputModel.onFocusChanged () => 65 | @fuseBottomPanel.focusTab 'Output' 66 | 67 | @subscriptions.add atom.commands.add 'atom-workspace', 'fuse:locate-in-designer': => 68 | Fuse.locateInDesigner(fuseLauncher, @daemon, outputModel) 69 | 70 | @subscriptions.add atom.commands.add 'atom-workspace', 'fuse:preview-local': -> 71 | textEditor = @getModel().getActiveTextEditor() 72 | Fuse.previewWithOutput(fuseLauncher, 'local', Path.dirname(textEditor.getPath()), outputModel) 73 | 74 | @subscriptions.add atom.commands.add 'atom-workspace', 'fuse:preview-android': -> 75 | textEditor = @getModel().getActiveTextEditor() 76 | Fuse.previewWithOutput(fuseLauncher, 'android', Path.dirname(textEditor.getPath()), outputModel) 77 | 78 | @subscriptions.add atom.commands.add 'atom-workspace', 'fuse:preview-ios': -> 79 | textEditor = @getModel().getActiveTextEditor() 80 | Fuse.previewWithOutput(fuseLauncher, 'ios', Path.dirname(textEditor.getPath()), outputModel) 81 | 82 | @uxProvider = new UXProvider @daemon 83 | 84 | previewWithOutput: (fuseLauncher, target, path, output) -> 85 | p = Preview.run(fuseLauncher, target, path) 86 | output.clear() 87 | output.focus() 88 | 89 | p.observeOutput (msg) -> 90 | output.log new LogEvent(message: msg) 91 | p.observeError (msg) -> 92 | output.log new LogEvent(message: msg) 93 | 94 | locateInDesigner: (fuseLauncher, daemon, outputModel) -> 95 | console.log "fuse: Runnning locate in designer command" 96 | textEditor = atom.workspace.getActiveTextEditor() 97 | if textEditor? 98 | console.log "fuse: Sending locate in designer request for " + textEditor.getPath() 99 | position = textEditor.getCursorBufferPosition() 100 | message = new FocusDesignerRequest { 101 | file: textEditor.getPath(), 102 | line: position.row + 1, 103 | column: position.column + 1 104 | } 105 | daemon.request message, (response) -> 106 | console.log "Got response for message" 107 | console.dir response 108 | if response.status == "Unhandled" 109 | Fuse.previewWithOutput(fuseLauncher, 'local', Path.dirname(textEditor.getPath()), outputModel) 110 | 111 | getProvider: -> 112 | @uxProvider 113 | 114 | deactivate: -> 115 | @subscriptions?.dispose() 116 | @fuseBottomPanel?.destroy() 117 | 118 | serialize: -> 119 | fuseBottomPanel: @fuseBottomPanel?.serialize() 120 | -------------------------------------------------------------------------------- /lib/fuseBottomPanel.coffee: -------------------------------------------------------------------------------- 1 | {$, $$, View} = require 'atom-space-pen-views' 2 | 3 | module.exports = 4 | class FuseBottomPanel extends View 5 | @content: -> 6 | @div class: 'fuse view-resizer panel', => 7 | @div class: 'view-resize-handle', outlet: 'resizeHandle' 8 | @div class: 'fuse-panel-heading panel-heading', dblclick: 'toggle', outlet: 'heading', => 9 | @div class: 'fuse-img' 10 | @span class: 'panel-head-text', outlet: 'headText', 'Fuse' 11 | @div class: 'panel-body view-scroller', outlet: 'body' 12 | 13 | innerElement: null 14 | tabConstructors: {} 15 | 16 | initialize: (serializedState) -> 17 | @numTabs = 0 18 | @body.height serializedState?.height ? 200 19 | @hide() 20 | 21 | @handleEvents() 22 | 23 | handleEvents: -> 24 | @on 'mousedown', '.view-resize-handle', @resizeStarted 25 | 26 | addTab: (header, factory) -> 27 | id = header.replace(/\s+/g, '-') 28 | @heading.prepend $$ -> 29 | @button class: 'fuse-button btn pull-right', id: id, header 30 | @on 'click', "\##{id}", (args) => @setInnerElement(header, factory()) 31 | 32 | if @numTabs == 0 33 | @setInnerElement(header, factory()) 34 | ++@numTabs 35 | 36 | @tabConstructors[header] = factory: factory 37 | 38 | setInnerElement: (header, element) -> 39 | @headText.text(header) 40 | @body.empty().append(element) 41 | 42 | @innerElement?.destroy?() 43 | @innerElement = element 44 | @innerElement.setScrollProvider? @body 45 | 46 | resizeStarted: => 47 | $(document).on('mousemove', @resizeView) 48 | $(document).on('mouseup', @resizeStopped) 49 | 50 | resizeStopped: => 51 | $(document).off('mousemove', @resizeView) 52 | $(document).off('mouseup', @resizeStopped) 53 | 54 | focusTab: (header) -> 55 | tabFactory = @tabConstructors[header] 56 | if not tabFactory? 57 | console.log(header + " no tab factory with that name.") 58 | return 59 | 60 | @setInnerElement(header, tabFactory.factory()) 61 | @show() 62 | 63 | resizeView: ({which, pageY}) => 64 | return @resizeStopped() unless which is 1 65 | @body.height($(document.body).height() - pageY - @heading.outerHeight()) 66 | 67 | destroy: -> 68 | @innerElement?.destroy?() 69 | @hide() 70 | 71 | serialize: -> 72 | height: @body.height() 73 | -------------------------------------------------------------------------------- /lib/fuseLauncher.coffee: -------------------------------------------------------------------------------- 1 | {spawn} = require 'child_process' 2 | 3 | module.exports = 4 | class FuseLauncher 5 | constructor: (@fusePath) -> 6 | 7 | run: (args) -> 8 | return spawn @fusePath, args 9 | -------------------------------------------------------------------------------- /lib/messageTypes.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | Message: 3 | class Message 4 | constructor: (@messageType) -> 5 | 6 | @deserialize: (msgType, json) -> 7 | if msgType == "Event" 8 | return Event.deserialize(json) 9 | else if msgType == "Request" 10 | return Request.deserialize(json) 11 | else if msgType == "Response" 12 | return Response.deserialize(json) 13 | else 14 | console.log("fuse: Unknown message type.") 15 | Event: 16 | class Event extends Message 17 | constructor: (@name, @data) -> 18 | super "Event" 19 | 20 | serialize: -> 21 | return JSON.stringify({ 22 | Name: @name, 23 | Data: @data 24 | }) 25 | 26 | @deserialize: (json) -> 27 | evtObj = JSON.parse(json) 28 | event = new Event(evtObj.Name, evtObj.Data) 29 | event["subscriptionId"] = evtObj.SubscriptionId 30 | return event 31 | Request: 32 | class Request extends Message 33 | constructor: (@name, @arguments) -> 34 | super "Request" 35 | 36 | serialize: (id) -> 37 | return JSON.stringify({ 38 | Name: @name, 39 | Id: id, 40 | Arguments: @arguments 41 | }) 42 | 43 | @deserialize: (json) -> 44 | reqObj = JSON.parse(json) 45 | request = new Request(reqObj.Name, reqObj.Arguments) 46 | request["id"] = reqObj.Id 47 | return request 48 | Response: 49 | class Response extends Message 50 | constructor: (@id, @status, @errors, @result) -> 51 | super "Response" 52 | 53 | serialize: -> 54 | return JSON.stringify({ 55 | Id: @id 56 | Status: @status 57 | Errors: @errors 58 | Result: @result 59 | }) 60 | 61 | @deserialize: (json) -> 62 | resObj = JSON.parse(json) 63 | response = new Response(resObj.Id, 64 | resObj.Status, 65 | resObj.Errors, 66 | resObj.Result) 67 | return response 68 | -------------------------------------------------------------------------------- /lib/messages.coffee: -------------------------------------------------------------------------------- 1 | {Event, Request} = require './messageTypes.coffee' 2 | 3 | module.exports = 4 | MessageHelper: 5 | class MessageHelper 6 | @convertToCaretPos: (point) -> 7 | return { Line: point.row + 1, Character: point.column + 1 } 8 | 9 | SelectionChangedEvent: 10 | # Public: {SelectionChangedEvent} is a datastructure that can be sent to daemon. 11 | class SelectionChangedEvent extends Event 12 | # Creates a new {SelectionChangedEvent}. 13 | # data - An object consisting of: 14 | # path - The {string} path of file where selection happened. 15 | # text - The {string} text of the file selected 16 | # (this may differ from what is saved to disk). 17 | # caretPosition - A {Point} storing caret position in file. 18 | constructor: (data) -> 19 | super "Fuse.Preview.SelectionChanged", { 20 | Path: data.path, 21 | Text: data.text, 22 | CaretPosition: MessageHelper.convertToCaretPos(data.caretPosition) 23 | } 24 | 25 | SubscribeRequest: 26 | class SubscribeRequest extends Request 27 | constructor: (args) -> 28 | super "Subscribe", { 29 | Filter: args.filter, 30 | Replay: args.replay, 31 | SubscriptionId: args.subscriptionId 32 | } 33 | 34 | GetCodeSuggestionsRequest: 35 | class GetCodeSuggestionsRequest extends Request 36 | constructor: (args) -> 37 | super "Fuse.GetCodeSuggestions", { 38 | Text: args.text, 39 | Path: args.path, 40 | SyntaxType: args.syntaxType, 41 | CaretPosition: MessageHelper.convertToCaretPos(args.caretPosition) 42 | } 43 | 44 | PublishServiceRequest: 45 | class PublishServiceRequest extends Request 46 | constructor: (args) -> 47 | super "PublishService", { 48 | RequestNames: args.requestNames 49 | } 50 | 51 | FocusDesignerRequest: 52 | class FocusDesignerRequest extends Request 53 | constructor: (args) -> 54 | super "FocusDesigner", { 55 | File: args.file, 56 | Line: args.line, 57 | Column: args.column 58 | } 59 | -------------------------------------------------------------------------------- /lib/outputView.coffee: -------------------------------------------------------------------------------- 1 | {$, $$, View} = require 'atom-space-pen-views' 2 | {Emitter, Disposable, CompositeDisposable} = require 'atom' 3 | 4 | module.exports = 5 | LogEvent: 6 | class LogEvent 7 | constructor: (args) -> 8 | {@message} = args 9 | OutputModel: 10 | class OutputModel extends Disposable 11 | logEvents: [] 12 | 13 | constructor: (buildObserver) -> 14 | super @dispose 15 | @emitter = new Emitter 16 | 17 | observeLogEvents: (callback) -> 18 | callback(logEvent) for logEvent in @logEvents 19 | return @emitter.on 'new-log-event', callback 20 | 21 | observeOnClear: (callback) -> 22 | return @emitter.on 'clear', callback 23 | 24 | onFocusChanged: (callback) -> 25 | @emitter.on 'focus', callback 26 | 27 | log: (logEvent) -> 28 | @logEvents.push logEvent 29 | @emitter.emit 'new-log-event', logEvent 30 | 31 | clear: -> 32 | @logEvents = [] 33 | @emitter.emit 'clear' 34 | 35 | focus: -> 36 | @emitter.emit 'focus' 37 | 38 | dispose: -> 39 | @buildLogEventSub.dispose() 40 | 41 | OutputView: 42 | class OutputView extends View 43 | @content: -> 44 | @div => 45 | @pre class: 'output-panel native-key-bindings', outlet: 'output', tabindex: -1 46 | 47 | initialize: (model) -> 48 | @logEventSub = model.observeLogEvents (logEvent) => 49 | @log(logEvent.message) 50 | @clearSub = model.observeOnClear => 51 | @clear() 52 | @oldOutputHeight = 0 53 | 54 | clear: -> 55 | @output.empty() 56 | 57 | log: (message) -> 58 | if typeof message == 'string' 59 | @output.append $$ -> 60 | @p message 61 | else 62 | @output.append message 63 | 64 | if not @scrollProvider? 65 | return 66 | 67 | outputHeight = @output.innerHeight() 68 | deltaHeight = outputHeight - @oldOutputHeight 69 | @oldOutputHeight = outputHeight 70 | atBottom = @scrollProvider.scrollTop() + @scrollProvider.innerHeight() + deltaHeight >= outputHeight 71 | if atBottom 72 | @scrollProvider.scrollTop outputHeight 73 | 74 | setScrollProvider: (@scrollProvider) -> 75 | @scrollProvider.scrollTop(@output.innerHeight()) 76 | 77 | destroy: -> 78 | @logEventSub?.dispose() 79 | @clearSub?.dispose() 80 | -------------------------------------------------------------------------------- /lib/preview.coffee: -------------------------------------------------------------------------------- 1 | {Disposable} = require 'atom' 2 | 3 | module.exports = 4 | class Preview extends Disposable 5 | constructor: (fuseLauncher, target, path) -> 6 | super @dispose 7 | @fuseProc = fuseLauncher.run ['preview', '-t=' + target, '--name=' + 'AtomEditor', path] 8 | 9 | @run: (fuseLauncher, target, path) -> 10 | return new Preview(fuseLauncher, target, path) 11 | 12 | observeOutput: (callback) -> 13 | @fuseProc.stdout.on 'data', (data) -> 14 | callback data.toString('utf-8').replace('\xa0','\x20') 15 | 16 | observeError: (callback) -> 17 | @fuseProc.stderr.on 'data', (data) -> 18 | callback data.toString('utf-8').replace('\xa0','\x20') 19 | 20 | observeKill: (callback) -> 21 | @fuseProc.on 'close', (code) => 22 | callback code 23 | 24 | dispose: -> 25 | @fuseProc.kill() 26 | -------------------------------------------------------------------------------- /lib/selectionChangedNotifier.coffee: -------------------------------------------------------------------------------- 1 | {SelectionChangedEvent} = require './messages' 2 | {Disposable, CompositeDisposable} = require 'atom' 3 | 4 | # Public: The {SelectionChangedNotifier} will listen for 5 | # cursor position changes done in an UX file. 6 | # These changes are then sent as events to the Fuse daemon. 7 | module.exports = 8 | class SelectionChangedNotifier extends Disposable 9 | textEditorSub = null 10 | fuseSelectionSub = null 11 | 12 | # Public: Creates a new {SelectionChangedNotifier}. 13 | # Constructor will start listening. 14 | # Dispose {SelectionChangedNotifier} to stop listening. 15 | # 16 | # daemon - An reference to an instance of type {Daemon}. 17 | constructor: (@daemon) -> 18 | super(@dispose) 19 | 20 | @fuseSelectionSub = atom.config.observe 'fuse.fuseSelection', (turnOnSelection) => 21 | @textEditorSub?.dispose() 22 | if turnOnSelection 23 | @textEditorSub = @hookSelectionObserver() 24 | 25 | hookSelectionObserver: () -> 26 | subscriptions = new CompositeDisposable 27 | subscriptions.add atom.workspace.observeTextEditors (editor) => 28 | if editor.getGrammar().name is "UX" 29 | cursorChangeSub = editor.onDidChangeCursorPosition (event) => 30 | @cursorPositionChangedInUxEditor(editor, event) 31 | 32 | destroySub = editor.onDidDestroy -> 33 | cursorChangeSub.dispose() 34 | destroySub.dispose() 35 | 36 | subscriptions.add cursorChangeSub 37 | subscriptions.add destroySub 38 | 39 | return subscriptions 40 | 41 | cursorPositionChangedInUxEditor: (editor, event) -> 42 | path = editor.getPath() 43 | text = editor.getText() 44 | cursorPos = event.newBufferPosition 45 | @daemon.broadcastEvent( 46 | new SelectionChangedEvent( 47 | path: path, 48 | text: text, 49 | caretPosition: cursorPos)) 50 | 51 | dispose: => 52 | @fuseSelectionSub.dispose() 53 | @textEditorSub?.dispose() 54 | -------------------------------------------------------------------------------- /lib/uxProvider.coffee: -------------------------------------------------------------------------------- 1 | {GetCodeSuggestionsRequest} = require './messages' 2 | {Range} = require 'atom' 3 | 4 | module.exports = 5 | class UXProvider 6 | selector: '.text.xml.ux' 7 | disableForSelector: '.text.xml.ux .comment' 8 | 9 | inclusionPriority: 1 10 | excludeLowerPriority: true 11 | 12 | filterSuggestions: true 13 | 14 | constructor: (@daemon) -> 15 | 16 | getSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix, activatedManually}) -> 17 | scopes = scopeDescriptor.scopes 18 | 19 | # Check if we are around a '/>' 20 | # Or in a inner element area 21 | nextTwoChars = editor.getTextInBufferRange( 22 | new Range(bufferPosition, bufferPosition.translate([0,2]))) 23 | isEndOfScope = scopes.indexOf('punctuation.definition.tag.xml') isnt -1 and nextTwoChars != "/>" 24 | 25 | if isEndOfScope or scopes[scopes.length - 1] == 'text.ux' 26 | return [] 27 | 28 | new Promise (resolve) => 29 | path = editor.getPath() 30 | text = editor.getText().replace(/\r/gm, '') 31 | @daemon.request( 32 | new GetCodeSuggestionsRequest( 33 | text: text, 34 | path: path, 35 | syntaxType: 'ux' 36 | caretPosition: bufferPosition 37 | ), 38 | (response) => 39 | if response.status != "Success" 40 | resolve([]) 41 | return 42 | 43 | suggestions = response.result.CodeSuggestions 44 | completions = [] 45 | for suggestion in suggestions 46 | completions.push @buildSuggestion(suggestion) 47 | 48 | resolve(completions) 49 | ) 50 | 51 | buildSuggestion: (suggestion) -> 52 | if suggestion.Type == 'Class' 53 | return { 54 | text: suggestion.Suggestion, 55 | type: 'tag' 56 | } 57 | else if suggestion.Type == 'Property' 58 | return { 59 | displayText: suggestion.Suggestion, 60 | snippet: "#{suggestion.Suggestion}=\"$1\"$0" 61 | type: 'attribute' 62 | } 63 | else 64 | return { 65 | text: suggestion.Suggestion, 66 | type: 'value' 67 | } 68 | 69 | onDidInsertSuggestion: ({editor, triggerPosition, suggestion}) -> 70 | 71 | dispose: -> 72 | -------------------------------------------------------------------------------- /menus/fuse.cson: -------------------------------------------------------------------------------- 1 | # See https://atom.io/docs/latest/hacking-atom-package-word-count#menus for more details 2 | 'context-menu': 3 | 'atom-text-editor': [ 4 | { 5 | label: "Locate in designer" 6 | command: "fuse:locate-in-designer" 7 | } 8 | { 9 | label: 'Fuse preview' 10 | submenu: [ 11 | {label: 'Local', command: 'fuse:preview-local'} 12 | {label: 'iOS', command: 'fuse:preview-ios'} 13 | {label: 'Android', command: 'fuse:preview-android'} 14 | ] 15 | } 16 | ] 17 | 'menu': [ 18 | { 19 | 'label': 'Packages' 20 | 'submenu': [ 21 | 'label': 'Fuse' 22 | 'submenu': [ 23 | 'label': 'Toggle Panel' 24 | 'command': 'fuse:panel' 25 | ] 26 | ] 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fuse", 3 | "main": "./lib/fuse", 4 | "version": "0.4.2", 5 | "description": "Fuse package for Atom (www.fusetools.com)", 6 | "repository": "https://github.com/fusetools/Fuse.AtomPlugin", 7 | "keywords": [], 8 | "activationCommands": {}, 9 | "license": "MIT", 10 | "engines": { 11 | "atom": ">=1.0.0 <2.0.0" 12 | }, 13 | "providedServices": { 14 | "autocomplete.provider": { 15 | "versions": { 16 | "2.0.0": "getProvider" 17 | } 18 | } 19 | }, 20 | "dependencies": { 21 | "atom-package-dependencies": "latest", 22 | "atom-space-pen-views": ">=2.1.0" 23 | }, 24 | "package-dependencies": {} 25 | } 26 | -------------------------------------------------------------------------------- /settings/language-uno.cson: -------------------------------------------------------------------------------- 1 | '.source.uno': 2 | 'editor': 3 | 'commentStart': '// ' 4 | 'increaseIndentPattern': '(?x)\n\t\t^ .* \\{ [^}"\']* $\n\t| ^ \\s* \\{ \\} $\n\t' 5 | 'decreaseIndentPattern': '(?x)\n\t\t^ (.*\\*/)? \\s* \\} ( [^}{"\']* \\{ | \\s* while \\s* \\( .* )? [;\\s]* (//.*|/\\*.*\\*/\\s*)? $\n\t' 6 | '.source.nant-build': 7 | 'editor': 8 | 'increaseIndentPattern': '<[^/>]*>\\s*$' 9 | -------------------------------------------------------------------------------- /settings/language-ux.cson: -------------------------------------------------------------------------------- 1 | '.text.xml.ux': 2 | 'editor': 3 | 'commentStart': '' 5 | -------------------------------------------------------------------------------- /snippets/language-uno.cson: -------------------------------------------------------------------------------- 1 | '.source.uno': 2 | 'Abstract': 3 | 'prefix': 'ab' 4 | 'body': 'abstract ' 5 | 'Async Task': 6 | 'prefix': 'at' 7 | 'body': 'async Task<${0:T}> ${1:MethodName}($2) {\n\t$3\n}' 8 | 'Async Void': 9 | 'prefix': 'av' 10 | 'body': 'async void ${0:MethodName}($1) {\n\t$2\n}' 11 | 'Await': 12 | 'prefix': 'aw' 13 | 'body': 'await ' 14 | 'Break': 15 | 'prefix': 'br' 16 | 'body': 'break;\n' 17 | 'Case': 18 | 'prefix': 'cs' 19 | 'body': 'case ${1:Condition}:\n\t$2\n$0' 20 | 'Catch': 21 | 'prefix': 'ca' 22 | 'body': 'catch (${1:Exception} ${2:e}) {\n\t$0\n}' 23 | 'Class': 24 | 'prefix': 'cl' 25 | 'body': 'class $1\n{\n\t$0\n}' 26 | 'Constant String': 27 | 'prefix': 'cos' 28 | 'body': 'public const string ${1:Var} = $2;$0' 29 | 'Constant': 30 | 'prefix': 'co' 31 | 'body': 'public const ${1:string} ${2:Var} = $3;$0' 32 | 'Default': 33 | 'prefix': 'de' 34 | 'body': 'default:\n\t$0' 35 | 'Do While': 36 | 'prefix': 'do' 37 | 'body': 'do {\n\t$0\n} while (${1:Condition});' 38 | 'Else If': 39 | 'prefix': 'elif' 40 | 'body': 'else if (${1:Condition}) {\n\t$0\n}' 41 | 'Else': 42 | 'prefix': 'el' 43 | 'body': 'else {\n\t$0\n}' 44 | 'Enumeration': 45 | 'prefix': 'enum' 46 | 'body': 'enum $1\n{\n\t$0\n}' 47 | 'Finally': 48 | 'prefix': 'fy' 49 | 'body': 'finally {\n\t$0\n}' 50 | 'For': 51 | 'prefix': 'for' 52 | 'body': 'for (${1:Initializer}; ${2:Condition}; ${3:Update}) {\n\t$0\n}' 53 | 'For Each': 54 | 'prefix': 'fore' 55 | 'body': 'foreach (${1:Type} in ${2:Collection}) {\n\t$0\n}' 56 | 'If ': 57 | 'prefix': 'if' 58 | 'body': 'if (${1:Condition}) {\n\t$0\n}' 59 | 'Interface': 60 | 'prefix': 'in' 61 | 'body': 'interface $1\n{\n\t$0\n}' 62 | 'Method (Main)': 63 | 'prefix': 'main' 64 | 'body': '/// \n/// The main entry point for the application\n/// \n[STAThread]\npublic static void Main(string[] args)\n{\n\t$0\n}' 65 | 'Method': 66 | 'prefix': 'm' 67 | 'body': '${1:void} ${2:Method}($3)\n{\n\t$0\n}' 68 | 'Namespace ': 69 | 'prefix': 'ns' 70 | 'body': 'namespace ${1:NamespaceName}\n{\n\t$0\n}' 71 | 'Override': 72 | 'prefix': 'over' 73 | 'body': 'override ' 74 | 'Private': 75 | 'prefix': 'pr' 76 | 'body': 'private ' 77 | 'Property': 78 | 'prefix': 'prop' 79 | 'body': '${1:string} ${2:PropertyName} { get; set; }' 80 | 'Protected': 81 | 'prefix': 'po' 82 | 'body': 'protected ' 83 | 'Public ': 84 | 'prefix': 'pu' 85 | 'body': 'public ' 86 | 'Region': 87 | 'prefix': 'reg' 88 | 'body': '#region ${1:Region Name}\n\n$0\n\n#endregion\n' 89 | 'Return': 90 | 'prefix': 're' 91 | 'body': 'return ' 92 | 'Sealed': 93 | 'prefix': 'se' 94 | 'body': 'sealed ' 95 | 'Static': 96 | 'prefix': 'st' 97 | 'body': 'static ' 98 | 'Struct': 99 | 'prefix': 'su' 100 | 'body': 'struct $1\n{\n\t$0\n}' 101 | 'Switch': 102 | 'prefix': 'sw' 103 | 'body': 'switch (${1:Expression}) {\n\t$0\n}' 104 | 'Throw New': 105 | 'prefix': 'tn' 106 | 'body': 'throw new $0' 107 | 'Throw': 108 | 'prefix': 'th' 109 | 'body': 'throw $0' 110 | 'Try': 111 | 'prefix': 'tr' 112 | 'body': 'try {\n\t$0\n}' 113 | 'Using': 114 | 'prefix': 'us' 115 | 'body': 'using ${1:System};$0' 116 | 'Variable': 117 | 'prefix': 'v' 118 | 'body': '${1:string} ${2:var}${3: = ${0:null}};' 119 | 'Virtual': 120 | 'prefix': 'virt' 121 | 'body': 'virtual ' 122 | 'While': 123 | 'prefix': 'wh' 124 | 'body': 'while (${1:Condition}) {\n\t$0\n}' 125 | 'Write': 126 | 'prefix': 'w' 127 | 'body': 'Console.Write($1);$0' 128 | 'WriteLine': 129 | 'prefix': 'wl' 130 | 'body': 'Console.WriteLine($1);$0' 131 | -------------------------------------------------------------------------------- /spec/fuse-spec.coffee: -------------------------------------------------------------------------------- 1 | Fuse = require '../lib/fuse' 2 | -------------------------------------------------------------------------------- /spec/fuse-view-spec.coffee: -------------------------------------------------------------------------------- 1 | FuseView = require '../lib/fuse-view' 2 | -------------------------------------------------------------------------------- /styles/Fuse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuse-open/atom-plugin/ce7ef8d48d7e0caf353aa72f1cf12a2cc6dcf8a3/styles/Fuse.png -------------------------------------------------------------------------------- /styles/fuse.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 | .fuse { 8 | &.view-resizer { 9 | overflow: hidden; 10 | cursor: default; 11 | -webkit-user-select: none; 12 | 13 | .view-resize-handle { 14 | position: absolute; 15 | top: 0; 16 | bottom: 0; 17 | height: 10px; 18 | width: 100%; 19 | cursor: ns-resize; 20 | z-index: 3; 21 | } 22 | } 23 | 24 | .view-scroller { 25 | width: 100%; 26 | overflow:auto; 27 | } 28 | 29 | .error-list-table { 30 | width: 100%; 31 | 32 | th { 33 | padding: 5px; 34 | font-size: 1.2em; 35 | } 36 | 37 | tr:hover { 38 | background-color: @button-background-color-hover; 39 | } 40 | 41 | td { 42 | padding: 5px; 43 | } 44 | } 45 | 46 | .output-panel { 47 | background-color: @tool-panel-background-color; 48 | cursor: text; 49 | -webkit-user-select: text; 50 | 51 | p { 52 | margin: 0; 53 | } 54 | } 55 | 56 | .fuse-panel-heading { 57 | font-size: 1.2em; 58 | } 59 | 60 | .panel-head-text { 61 | margin-top: 8px; 62 | display: inline-block; 63 | } 64 | 65 | .fuse-img { 66 | display: inline-block; 67 | vertical-align:top; 68 | background-image: url('atom://fuse/styles/Fuse.png'); 69 | background-repeat: no-repeat; 70 | width: 32px; 71 | height: 32px; 72 | } 73 | 74 | .fuse-button { 75 | margin-left: 5px; 76 | margin-right: 5px; 77 | font-size: 0.9em; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /styles/stylesheets.less: -------------------------------------------------------------------------------- 1 | .preprocessor { 2 | color: #dc322f; 3 | } 4 | --------------------------------------------------------------------------------