├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── lib ├── atom-symfony2.coffee ├── config.coffee ├── plugin.coffee ├── providers │ ├── repository-provider.coffee │ ├── service-provider.coffee │ └── services │ │ ├── file-class-provider.coffee │ │ ├── file-service-provider.coffee │ │ ├── xml-class-provider.coffee │ │ ├── xml-service-provider.coffee │ │ ├── yaml-class-provider.coffee │ │ └── yaml-service-provider.coffee └── services │ ├── php-parser.coffee │ └── symfony2-proxy.coffee ├── package.json ├── spec └── atom-symfony2-spec.coffee └── styles └── atom-symfony2.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peekmo/atom-symfony2/c49d39780c85f2580903400753bcef91da22aeac/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Axel Anceau (Peekmo) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Symfony2 tools for atom editor. 4 | Depends on [atom-autocomplete-php](https://atom.io/packages/atom-autocomplete-php) 5 | 6 | Note : This package is in its early stage, feel free to help or to report issues 7 | 8 | # Features 9 | 10 | - Autocomplete on service names in .xml files and .yml files 11 | - Autocomplete on class names in .xml files and .yml files 12 | - Services completion in symfony2 controllers (or everywhere you're using the container) 13 | 14 | # Configuration 15 | 16 | You must have a fully working [atom-autocomplete-php](https://atom.io/packages/atom-autocomplete-php) configuration 17 | 18 | - Relative path to console : Insert here, a coma separated list of all your path to console in your projects. By default in Symfony2, it's ```app/console``` 19 | 20 | ![Example](http://i.imgur.com/l8ZGAoe.png) 21 | 22 | ![A screenshot of your package](https://f.cloud.github.com/assets/69169/2290250/c35d867a-a017-11e3-86be-cd7c5bf3ff9b.gif) 23 | -------------------------------------------------------------------------------- /lib/atom-symfony2.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | proxy = require './services/symfony2-proxy.coffee' 3 | parser = require './services/php-parser.coffee' 4 | config = require './config.coffee' 5 | plugin = require './plugin.coffee' 6 | YamlServiceProvider = require './providers/services/yaml-service-provider.coffee' 7 | YamlClassProvider = require './providers/services/yaml-class-provider.coffee' 8 | XmlServiceProvider = require './providers/services/xml-service-provider.coffee' 9 | XmlClassProvider = require './providers/services/xml-class-provider.coffee' 10 | ServiceProvider = require './providers/service-provider.coffee' 11 | RepositoryProvider = require './providers/repository-provider.coffee' 12 | 13 | module.exports = 14 | config: 15 | console: 16 | title: 'Relative path to console file' 17 | description: 'Relative path to symfony2 console file (default app/console)' 18 | type: 'array' 19 | default: ['app/console'] 20 | order: 1 21 | 22 | activate: -> 23 | config.init() 24 | proxy.init() 25 | 26 | @providers = [] 27 | @providers.push(new YamlServiceProvider) 28 | @providers.push(new YamlClassProvider) 29 | @providers.push(new XmlServiceProvider) 30 | @providers.push(new XmlClassProvider) 31 | @providers.push(new ServiceProvider) 32 | @providers.push(new RepositoryProvider) 33 | 34 | deactivate: -> 35 | 36 | getAutocompleteTools: (tools) -> 37 | proxy.providePhpProxy(tools.proxy) 38 | parser.parser = tools.parser 39 | 40 | getPhpPlugin: -> 41 | return plugin 42 | 43 | getProvider: -> 44 | return @providers 45 | -------------------------------------------------------------------------------- /lib/config.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | config: {} 3 | 4 | ###* 5 | * Get plugin configuration 6 | ### 7 | updateConfig: () -> 8 | @config['console'] = atom.config.get('atom-symfony2.console') 9 | 10 | ###* 11 | * Init function called on package activation 12 | * Register config events and write the first config 13 | ### 14 | init: () -> 15 | @updateConfig() 16 | 17 | atom.config.onDidChange 'atom-symfony2.console', () => 18 | @updateConfig() 19 | -------------------------------------------------------------------------------- /lib/plugin.coffee: -------------------------------------------------------------------------------- 1 | proxy = require './services/symfony2-proxy.coffee' 2 | 3 | module.exports = 4 | classes: [ 5 | "Symfony\\Component\\DependencyInjection\\ContainerInterface", 6 | "Symfony\\Component\\DependencyInjection\\Container", 7 | "Doctrine\\Bundle\\DoctrineBundle\\Registry" 8 | ] 9 | 10 | ###* 11 | * Returns a className if something is find for the given parent/method 12 | * @param {string} parent Full parent method 13 | * @param {string} method Full method (with args) 14 | * @return {string|null} 15 | ### 16 | autocomplete: (parent, method) -> 17 | # Get method name 18 | methodName = method.substring(0, method.indexOf("(")) 19 | 20 | return unless @isService(parent, methodName) 21 | 22 | reg = /["(][\s]*[\'\"][\s]*([^\"\']+)[\s]*[\"\'][\s]*[")]/g 23 | result = reg.exec(method) 24 | 25 | return unless result?[1]? 26 | services = proxy.getServices() 27 | 28 | return unless services[result[1]]? 29 | return services[result[1]] 30 | 31 | ###* 32 | * Checks if we should returns completion on services 33 | * @param {string} parent 34 | * @param {string} methodName 35 | * @return {Boolean} 36 | ### 37 | isService: (parent, methodName) -> 38 | if methodName == "get" && (@classes.indexOf(parent) != -1 || parent.endsWith("Controller")) 39 | return true 40 | 41 | return false 42 | 43 | ###* 44 | * Checks if we should returns completion on repositories 45 | * @param {string} parent 46 | * @param {string} methodName 47 | * @return {Boolean} 48 | ### 49 | isRepository: (parent, methodName) -> 50 | if methodName == "getRepository" && @classes.indexOf(parent) != -1 51 | return true 52 | 53 | return false 54 | -------------------------------------------------------------------------------- /lib/providers/repository-provider.coffee: -------------------------------------------------------------------------------- 1 | parser = require '../services/php-parser.coffee' 2 | fuzzaldrin = require 'fuzzaldrin' 3 | proxy = require '../services/symfony2-proxy.coffee' 4 | plugin = require '../plugin.coffee' 5 | 6 | module.exports = 7 | class RepositoryProvider 8 | selector: '.source.php .string' 9 | inclusionPriority: 1 10 | disableForSelector: '.source.php .comment' 11 | 12 | 13 | getSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) -> 14 | @regex = /->getRepository[\s]*[(][\s]*[\"\']([^\"\']*)/g 15 | line = editor.getTextInRange([[bufferPosition.row, 0], bufferPosition]) 16 | 17 | result = @regex.exec(line) 18 | return unless result?[1]? 19 | 20 | # Move the buffer position before the parenthese 21 | newPosition = 22 | row: bufferPosition.row 23 | column: bufferPosition.column 24 | 25 | while newPosition.column > 0 26 | newPosition.column-- 27 | 28 | if line[newPosition.column + 1] == "(" 29 | break 30 | 31 | elements = parser.parser.getStackClasses(editor, newPosition) 32 | return unless elements? 33 | 34 | className = parser.parser.parseElements(editor, newPosition, elements) 35 | return unless className? 36 | 37 | return unless plugin.isRepository(className, 'getRepository') 38 | 39 | suggestions = [] 40 | repositories = proxy.getEntities() 41 | words = fuzzaldrin.filter repositories, result[1] 42 | 43 | for word in words 44 | suggestions.push 45 | text: word 46 | type: 'tag' 47 | 48 | return suggestions 49 | -------------------------------------------------------------------------------- /lib/providers/service-provider.coffee: -------------------------------------------------------------------------------- 1 | parser = require '../services/php-parser.coffee' 2 | fuzzaldrin = require 'fuzzaldrin' 3 | proxy = require '../services/symfony2-proxy.coffee' 4 | plugin = require '../plugin.coffee' 5 | 6 | module.exports = 7 | class ServiceProvider 8 | selector: '.source.php .string' 9 | inclusionPriority: 1 10 | disableForSelector: '.source.php .comment' 11 | 12 | 13 | getSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) -> 14 | @regex = /->get[\s]*[(][\s]*[\"\']([^\"\']*)/g 15 | line = editor.getTextInRange([[bufferPosition.row, 0], bufferPosition]) 16 | 17 | result = @regex.exec(line) 18 | return unless result?[1]? 19 | 20 | # Move the buffer position before the parenthese 21 | newPosition = 22 | row: bufferPosition.row 23 | column: bufferPosition.column 24 | 25 | while newPosition.column > 0 26 | newPosition.column-- 27 | 28 | if line[newPosition.column + 1] == "(" 29 | break 30 | 31 | elements = parser.parser.getStackClasses(editor, newPosition) 32 | return unless elements? 33 | 34 | className = parser.parser.parseElements(editor, newPosition, elements) 35 | return unless className? 36 | 37 | return unless plugin.isService(className, 'get') 38 | 39 | suggestions = [] 40 | services = proxy.getServices() 41 | words = fuzzaldrin.filter Object.keys(services), result[1] 42 | 43 | for word in words 44 | suggestions.push 45 | text: word 46 | type: 'tag' 47 | leftLabel: services[word].split("\\").pop() 48 | 49 | return suggestions 50 | -------------------------------------------------------------------------------- /lib/providers/services/file-class-provider.coffee: -------------------------------------------------------------------------------- 1 | proxy = require '../../services/symfony2-proxy.coffee' 2 | fuzzaldrin = require 'fuzzaldrin' 3 | 4 | module.exports = 5 | class FileClassProvider 6 | inclusionPriority: 1 7 | 8 | fetchSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) -> 9 | line = editor.getTextInRange([[bufferPosition.row, 0], bufferPosition]) 10 | 11 | result = @regex.exec(line) 12 | return unless result?[1]? 13 | 14 | @classes = proxy.phpProxy.classes() 15 | 16 | words = fuzzaldrin.filter @classes.autocomplete, result[1] 17 | suggestions = [] 18 | for word in words 19 | suggestions.push 20 | text: word 21 | type: 'class' 22 | 23 | return suggestions 24 | -------------------------------------------------------------------------------- /lib/providers/services/file-service-provider.coffee: -------------------------------------------------------------------------------- 1 | proxy = require '../../services/symfony2-proxy.coffee' 2 | fuzzaldrin = require 'fuzzaldrin' 3 | 4 | module.exports = 5 | class FileServiceProvider 6 | inclusionPriority: 1 7 | 8 | fetchSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) -> 9 | line = editor.getTextInRange([[bufferPosition.row, 0], bufferPosition]) 10 | 11 | result = @regex.exec(line) 12 | return unless result?[1]? 13 | 14 | services = proxy.getServices() 15 | words = fuzzaldrin.filter Object.keys(services), result[1] 16 | suggestions = [] 17 | for word in words 18 | suggestions.push 19 | text: word 20 | type: 'tag' 21 | leftLabel: services[word].split("\\").pop() 22 | 23 | return suggestions 24 | -------------------------------------------------------------------------------- /lib/providers/services/xml-class-provider.coffee: -------------------------------------------------------------------------------- 1 | FileClassProvider = require './file-class-provider.coffee' 2 | 3 | module.exports = 4 | class XmlClassProvider extends FileClassProvider 5 | selector: '.text.xml' 6 | 7 | getSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) -> 8 | @regex = /class[\s]*=[\s]*[\"]([^\"]*)[\"]*/g 9 | 10 | return @fetchSuggestions({editor, bufferPosition, scopeDescriptor, prefix}) 11 | -------------------------------------------------------------------------------- /lib/providers/services/xml-service-provider.coffee: -------------------------------------------------------------------------------- 1 | FileServiceProvider = require './file-service-provider.coffee' 2 | 3 | module.exports = 4 | class XmlServiceProvider extends FileServiceProvider 5 | selector: '.text.xml' 6 | 7 | getSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) -> 8 | @regex = /argument[^>]+id[\s]*=[\s]*[\"]([^\"]*)[\"]*/g 9 | 10 | return @fetchSuggestions({editor, bufferPosition, scopeDescriptor, prefix}) 11 | -------------------------------------------------------------------------------- /lib/providers/services/yaml-class-provider.coffee: -------------------------------------------------------------------------------- 1 | FileClassProvider = require './file-class-provider.coffee' 2 | 3 | module.exports = 4 | class YamlClassProvider extends FileClassProvider 5 | selector: '.source.yaml' 6 | 7 | getSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) -> 8 | @regex = /class[\s]*:[\s]*([a-zA-Z0-9_\\]*)/g 9 | 10 | return @fetchSuggestions({editor, bufferPosition, scopeDescriptor, prefix}) 11 | -------------------------------------------------------------------------------- /lib/providers/services/yaml-service-provider.coffee: -------------------------------------------------------------------------------- 1 | FileServiceProvider = require './file-service-provider.coffee' 2 | 3 | module.exports = 4 | class YamlServiceProvider extends FileServiceProvider 5 | selector: '.source.yaml' 6 | 7 | getSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) -> 8 | @regex = /@([^\s]*)[\s]*/g 9 | 10 | return @fetchSuggestions({editor, bufferPosition, scopeDescriptor, prefix}) 11 | -------------------------------------------------------------------------------- /lib/services/php-parser.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | parser: null 3 | -------------------------------------------------------------------------------- /lib/services/symfony2-proxy.coffee: -------------------------------------------------------------------------------- 1 | config = require '../config.coffee' 2 | 3 | module.exports = 4 | phpProxy: null 5 | data: 6 | entities: {} 7 | services: {} 8 | routes: {} 9 | 10 | ###* 11 | * Execute a symfony2 command 12 | * @param {array} command 13 | * @return {mixed} 14 | ### 15 | execute: (command) -> 16 | if !@phpProxy 17 | return [] 18 | 19 | for directory in atom.project.getDirectories() 20 | for cons in config.config["console"] 21 | res = @phpProxy.execute([cons].concat(command), false, {cwd: directory.path}, true) 22 | if not res.error? and res.result?.split("\n").length > 2 23 | return res 24 | else 25 | res = [] 26 | 27 | return [] 28 | 29 | ###* 30 | * Adds the atom autocomplete php proxy 31 | * @param {Object} proxy 32 | ### 33 | providePhpProxy: (proxy) -> 34 | @phpProxy = proxy 35 | 36 | ###* 37 | * Returns sf2 services 38 | * 39 | * @param {bool} force Force refresh or not ? 40 | * 41 | * @return {array} 42 | ### 43 | getServices: (force) -> 44 | if Object.keys(@data.services).length == 0 || force 45 | @data.services = {} 46 | 47 | lines = @execute("debug:container") 48 | 49 | if lines != [] and lines.result 50 | lines = lines.result.split("\n") 51 | 52 | list = false 53 | for line in lines 54 | if list == false 55 | if line.indexOf("Service ID") != -1 56 | list = true 57 | continue 58 | 59 | ## @TODO Manage aliases 60 | regex = /([^\s]+)[\s]+([^\s]+)/g 61 | result = regex.exec(line) 62 | 63 | if result and result[1]? and result[2]? and not result[3]? 64 | @data.services[result[1]] = result[2] 65 | 66 | return @data.services 67 | 68 | ###* 69 | * Récupère les entités gérées par doctrine 70 | * 71 | * @param {bool} force Force refresh or not ? 72 | * 73 | * @return {Array} 74 | ### 75 | getEntities: (force) -> 76 | if Object.keys(@data.entities).length == 0 || force 77 | @data.entities = [] 78 | 79 | lines = @execute("doctrine:mapping:info") 80 | 81 | if lines != [] and lines.result 82 | lines = lines.result.split("\n") 83 | 84 | list = false 85 | for line in lines 86 | if list == false 87 | if line.indexOf("mapped entities") != -1 88 | list = true 89 | continue 90 | 91 | regex = /([^\s]+)[\s]+([^\s]+)/g 92 | result = regex.exec(line) 93 | 94 | if result and result[1]? and result[2]? 95 | @data.entities.push result[2].replace(/\\/g, '').replace(/(Entity)/g, ':') 96 | 97 | return @data.entities 98 | 99 | ###* 100 | * Returns SF2 routes 101 | * 102 | * @param {bool} force Force refresh or not ? 103 | * 104 | * @return {array} 105 | ### 106 | getRoutes: (force) -> 107 | if @data.routes.length == 0 || force 108 | @data.routes = @execute("debug:router") 109 | 110 | return @data.routes 111 | 112 | ###* 113 | * Clear all cache of the plugin 114 | ### 115 | clearCache: () -> 116 | # Fill cache asynchronously 117 | (() => 118 | @getEntities(true) 119 | @getServices(true) 120 | @getRoutes(true) 121 | )() 122 | 123 | ###* 124 | * Method called on plugin activation 125 | ### 126 | init: () -> 127 | atom.workspace.observeTextEditors (editor) => 128 | editor.onDidSave((event) => 129 | @clearCache() 130 | ) 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-symfony2", 3 | "main": "./lib/atom-symfony2", 4 | "version": "0.3.0", 5 | "description": "Symfony2 tools and completion", 6 | "keywords": [ 7 | "php", 8 | "symfony2", 9 | "autocomplete" 10 | ], 11 | "repository": "https://github.com/peekmo/atom-symfony2", 12 | "license": "MIT", 13 | "engines": { 14 | "atom": ">=1.0.0 <2.0.0" 15 | }, 16 | "package-deps": [ 17 | "atom-autocomplete-php" 18 | ], 19 | "dependencies": { 20 | "fuzzaldrin": "^2.1.0" 21 | }, 22 | "providedServices": { 23 | "autocomplete.provider": { 24 | "versions": { 25 | "2.0.0": "getProvider" 26 | } 27 | }, 28 | "autocomplete.php.plugin": { 29 | "versions": { 30 | "1.0.0": "getPhpPlugin" 31 | } 32 | } 33 | }, 34 | "consumedServices": { 35 | "php.autocomplete.tools": { 36 | "versions": { 37 | "0.16.0": "getAutocompleteTools" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /spec/atom-symfony2-spec.coffee: -------------------------------------------------------------------------------- 1 | AtomSymfony2 = require '../lib/atom-symfony2' 2 | 3 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 4 | # 5 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` 6 | # or `fdescribe`). Remove the `f` to unfocus the block. 7 | 8 | describe "AtomSymfony2", -> 9 | [workspaceElement, activationPromise] = [] 10 | 11 | beforeEach -> 12 | workspaceElement = atom.views.getView(atom.workspace) 13 | activationPromise = atom.packages.activatePackage('atom-symfony2') 14 | 15 | describe "when the atom-symfony2:toggle event is triggered", -> 16 | it "hides and shows the modal panel", -> 17 | # Before the activation event the view is not on the DOM, and no panel 18 | # has been created 19 | expect(workspaceElement.querySelector('.atom-symfony2')).not.toExist() 20 | 21 | # This is an activation event, triggering it will cause the package to be 22 | # activated. 23 | atom.commands.dispatch workspaceElement, 'atom-symfony2:toggle' 24 | 25 | waitsForPromise -> 26 | activationPromise 27 | 28 | runs -> 29 | expect(workspaceElement.querySelector('.atom-symfony2')).toExist() 30 | 31 | atomSymfony2Element = workspaceElement.querySelector('.atom-symfony2') 32 | expect(atomSymfony2Element).toExist() 33 | 34 | atomSymfony2Panel = atom.workspace.panelForItem(atomSymfony2Element) 35 | expect(atomSymfony2Panel.isVisible()).toBe true 36 | atom.commands.dispatch workspaceElement, 'atom-symfony2:toggle' 37 | expect(atomSymfony2Panel.isVisible()).toBe false 38 | 39 | it "hides and shows the view", -> 40 | # This test shows you an integration test testing at the view level. 41 | 42 | # Attaching the workspaceElement to the DOM is required to allow the 43 | # `toBeVisible()` matchers to work. Anything testing visibility or focus 44 | # requires that the workspaceElement is on the DOM. Tests that attach the 45 | # workspaceElement to the DOM are generally slower than those off DOM. 46 | jasmine.attachToDOM(workspaceElement) 47 | 48 | expect(workspaceElement.querySelector('.atom-symfony2')).not.toExist() 49 | 50 | # This is an activation event, triggering it causes the package to be 51 | # activated. 52 | atom.commands.dispatch workspaceElement, 'atom-symfony2:toggle' 53 | 54 | waitsForPromise -> 55 | activationPromise 56 | 57 | runs -> 58 | # Now we can test for view visibility 59 | atomSymfony2Element = workspaceElement.querySelector('.atom-symfony2') 60 | expect(atomSymfony2Element).toBeVisible() 61 | atom.commands.dispatch workspaceElement, 'atom-symfony2:toggle' 62 | expect(atomSymfony2Element).not.toBeVisible() 63 | -------------------------------------------------------------------------------- /styles/atom-symfony2.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-symfony2 { 8 | } 9 | --------------------------------------------------------------------------------