├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── keymaps └── css-quick-editor.cson ├── lib ├── add-selector-view.coffee ├── directory-css-searcher.coffee ├── markup-parser.coffee ├── quick-editor-cache.coffee ├── quick-editor-view.coffee ├── quick-editor.coffee └── searcher-cache.coffee ├── menus └── quick-editor.cson ├── package.json ├── quick-edit.gif ├── spec ├── directory-css-searcher-spec.coffee ├── false-fixtures │ └── false-test.less ├── fixtures │ ├── test.html │ └── test.less ├── markup-parser-spec.coffee ├── quick-editor-spec.coffee └── quick-editor-view-spec.coffee └── styles └── quick-editor.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | 3 | notifications: 4 | email: 5 | on_success: never 6 | on_failure: change 7 | 8 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh' 9 | 10 | git: 11 | depth: 10 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.0 2 | * Added the ability to specify a styles directory to improve performance on larger projects 3 | * Fixed bug with context menu not working 4 | * Fixed bug with markup parsing 5 | 6 | ## 0.3.0 7 | * Added the ability to add a new selector 8 | 9 | ## 0.2.0 - Second Release 10 | * Improved selector parsing accuracy, refactored into parser 11 | * Gutter numbers reflect the style's actual position in their respective file 12 | * Wrote more tests 13 | 14 | ## 0.1.0 - First Release 15 | * Every feature added 16 | * Every bug fixed 17 | -------------------------------------------------------------------------------- /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 | # Quick Editor 2 | [![Build Status](https://travis-ci.org/Maushundb/quick-editor.svg?branch=master)](https://travis-ci.org/Maushundb/quick-editor) 3 | 4 | Quickly select and edit CSS/LESS/SCSS styles from the context of your markup instead of cluttering up your coding environment with extra panes. 5 | Inspired by [Brackets](http://brackets.io/)'s quick edit feature. 6 | 7 | ![Quick Edit Demo](https://github.com/Maushundb/quick-editor/blob/master/quick-edit.gif?raw=true) 8 | 9 | # Install 10 | ``` 11 | apm install quick-editor 12 | ``` 13 | 14 | Or search for quick-editor in Atom settings view. 15 | 16 | # Key Bindings 17 | The default shift-cmd-e or shift-ctrl-ewill toggle quick-edit while the cursor is over a CSS id or class 18 | 19 | This can be edited by defining a keybinding as follows 20 | 21 | ```coffee 22 | '.platform-darwin atom-text-editor': 23 | 'shift-cmd-e': 'quick-editor:quick-edit' 24 | 25 | '.platform-linux atom-text-editor, .platform-win32 atom-text-editor': 26 | 'shift-ctrl-e': 'quick-editor:quick-edit' 27 | ``` 28 | 29 | # Settings 30 | #### Styles Directory 31 | Specify an absolute path to your styles directory when working in a large project to improve performance. 32 | 33 | 34 | # Release Notes: 35 | 36 | ## 0.4.1 37 | * Added keybindings for Windows and Linux 38 | 39 | ## 0.4.0 40 | * Added the ability to specify a styles directory to improve performance on larger projects 41 | * Fixed bug with context menu not working 42 | * Fixed bug with markup parsing 43 | 44 | ## 0.3.0 45 | * Added the ability to add a new selector 46 | 47 | ### Full change log [here](./CHANGELOG.md) 48 | 49 | # Coming Soon: 50 | * Multiple files with same selector 51 | * Caching for larger projects 52 | * Show the name of CSS file 53 | * Link to open in new tab 54 | * Editor updates when changed in another file at the same time 55 | * Support SASS and Stylus 56 | * Multiple matches in the same file 57 | * Support Jade 58 | * Add support for javascript functions 59 | * Add support for color picking 60 | -------------------------------------------------------------------------------- /keymaps/css-quick-editor.cson: -------------------------------------------------------------------------------- 1 | # Keybindings require three things to be fully defined: A selector that is 2 | # matched against the focused element, the keystroke and the command to 3 | # execute. 4 | # 5 | # Below is a basic keybinding which registers on all platforms by applying to 6 | # the root workspace element. 7 | 8 | # For more detailed documentation see 9 | # https://atom.io/docs/latest/behind-atom-keymaps-in-depth 10 | '.platform-darwin atom-text-editor': 11 | 'shift-cmd-e': 'quick-editor:quick-edit' 12 | 13 | '.platform-linux atom-text-editor, .platform-win32 atom-text-editor': 14 | 'shift-ctrl-e': 'quick-editor:quick-edit' 15 | -------------------------------------------------------------------------------- /lib/add-selector-view.coffee: -------------------------------------------------------------------------------- 1 | {$, View, TextEditorView} = require 'atom-space-pen-views' 2 | {File, Directory} = require 'atom' 3 | 4 | module.exports = 5 | class AddSelectorView extends View 6 | 7 | selector: null 8 | onSelectorAdded: null 9 | 10 | @content: ()-> 11 | @div class: "add-selector-view", => 12 | @div class: "top-container", => 13 | @div class: "top-container-left", => 14 | @h2 "Add New Selector" 15 | @div class: "no-selector-container", => 16 | @div class: "no-selector-text", "No selector was found for: " 17 | @div class: "selector" 18 | @div class: "top-container-right", => 19 | @div class: "btn", click: "onAddClick", "Add" 20 | @div class: "add-new-style-container", => 21 | @div class: "add-new-selector-text", "Add new selector in:" 22 | @subview 'pathEditorView', new TextEditorView(mini: true) 23 | 24 | initialize: -> 25 | @pathEditor = @pathEditorView.getModel() 26 | 27 | attached: -> 28 | $(".selector").html(@selector) 29 | 30 | setSelector: (selector) -> 31 | @selector = selector 32 | 33 | setInitialPath: (path) -> 34 | @pathEditor.setText path 35 | 36 | onAddClick: -> 37 | path = @pathEditor.getText() 38 | file = new File(path) 39 | # TODO could get weird behavior if directory. Refer to 40 | # https://iojs.org/api/fs.html#fs_fs_statsync_path 41 | file.exists().then (exists) => 42 | if exists 43 | @onSelectorAdded(path, @selector) 44 | else 45 | atom.beep() 46 | atom.confirm(message: "File does not exist:\n" + path) 47 | -------------------------------------------------------------------------------- /lib/directory-css-searcher.coffee: -------------------------------------------------------------------------------- 1 | {File, Directory} = require 'atom' 2 | SearcherCache = require './searcher-cache' 3 | chokidar = require('chokidar') 4 | 5 | module.exports = 6 | class DirectoryCSSSearcher 7 | 8 | constructor: -> 9 | @searchResults = [] 10 | @file = null 11 | @matchStartLine = null 12 | @cache = new SearcherCache 13 | 14 | supportedFileExtensions: [ 15 | "*.css" 16 | "*.scss" 17 | "*.less" 18 | ] 19 | 20 | clear: -> 21 | @searchResults = [] 22 | @file = null 23 | @matchStartLine = null 24 | 25 | findFilesThatContain:(selector) -> 26 | re = selector + '\\s*\\{' 27 | id_reg = new RegExp(re) 28 | 29 | #TODO check the cache 30 | 31 | if atom.config.get('quick-editor.stylesDirectory') is '' 32 | paths = atom.project.getDirectories() 33 | else 34 | paths = [new Directory(atom.config.get('quick-editor.stylesDirectory'))] 35 | 36 | atom.workspace.defaultDirectorySearcher.search( 37 | paths, 38 | id_reg, 39 | { 40 | didMatch: @matchCallback.bind(@), 41 | didError: (e) -> console.error e.stack 42 | didSearchPaths: (n) -> return 43 | inclusions: @supportedFileExtensions 44 | } 45 | ) 46 | .then () => 47 | return unless @searchResults.length 48 | @cacheResult() 49 | filePath = @searchResults[0].filePath 50 | @matchStartLine = @searchResults[0].matches[0].range[0][0] 51 | @file = new File filePath, false 52 | 53 | cacheResult: -> 54 | markupFilePath = atom.workspace.getActiveTextEditor().getPath() 55 | for result in @searchResults 56 | @cache.put(markupFilePath, result.filePath) 57 | 58 | 59 | searchCache: -> 60 | #TODO Implement me 61 | 62 | 63 | matchCallback: (match) -> 64 | @searchResults.push(match) 65 | 66 | getSelectorText: () -> 67 | if @file 68 | @file.read().then (content) => 69 | lineNumber = 0 70 | text = "" 71 | for ch in content 72 | if lineNumber >= @matchStartLine 73 | text += ch 74 | if ch is "{" 75 | if not openBraces? then openBraces = 1 else openBraces++ 76 | if ch is "}" and openBraces 77 | openBraces-- 78 | break if openBraces is 0 79 | lineNumber++ if ch is "\n" or ch is "\r\n" 80 | return [true, 81 | {text: text, 82 | start: @matchStartLine, 83 | end: lineNumber, 84 | file: @file}] 85 | else 86 | return new Promise (resolve, reject) -> 87 | resolve([false, {text: null, start: null, end: null, file: null}]) 88 | -------------------------------------------------------------------------------- /lib/markup-parser.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | class MarkupParser 3 | constructor: (@textEditor = null) -> 4 | 5 | setEditor: (editor) -> 6 | @textEditor = editor 7 | 8 | parse: -> 9 | [bufferPos, line] = @getCurrentLine() 10 | @parseLine(bufferPos.column, line) 11 | 12 | getCurrentLine: -> 13 | bufferPos = @textEditor.getCursorBufferPosition() 14 | line = @textEditor.getBuffer().lineForRow(bufferPos.row) 15 | return [bufferPos, line] 16 | 17 | parseLine: (startCol, line) -> 18 | ### 19 | Does not support the following id or class types: 20 | id = "id1 i|d2", which isn't valid CSS 21 | class = "class='Th|is", which is technically valid CSS, but stupid 22 | Where "|" is the specified cursor position, if it matters 23 | ### 24 | outOfSelector = false 25 | lastQuote = null 26 | outerQuote = null 27 | 28 | # Determine is selection is a class or id and what the outer delim is 29 | `outer: //` 30 | for i in [startCol..-1] 31 | @textNotCSSIdentifier() if i is "-1" 32 | # covers the case where multiple types of quotations are used 33 | lastQuote = line[i] if line[i] is "\"" or line[i] is "\'" 34 | if outOfSelector 35 | switch line[i] 36 | when "s" 37 | prefix = "." 38 | `break outer` 39 | when "d" 40 | prefix = "#" 41 | `break outer` 42 | else continue 43 | if (line[i] is "=" and lastQuote) 44 | outOfSelector = true 45 | outerQuote = lastQuote 46 | 47 | @textNotCSSIdentifier() if not prefix? 48 | 49 | # set i to beginning of the selected class or id 50 | for i in [startCol..0] 51 | if line[i] is outerQuote or line[i] is " " 52 | i++ 53 | break 54 | 55 | # capture the class or id 56 | selector="" 57 | for j in [i..line.length] 58 | @textNotCSSIdentifier() if j is line.length 59 | break if line[j] is outerQuote or line[j] is " " 60 | selector += line[j] 61 | 62 | return prefix + selector 63 | 64 | textNotCSSIdentifier: -> 65 | e = new Error "Selected text is not a CSS identifier" 66 | throw e 67 | -------------------------------------------------------------------------------- /lib/quick-editor-cache.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = 3 | class QuickEditorCache 4 | ### 5 | A cache for storing a mapping between: 6 | html files -> most recently edited css file that styles it 7 | 8 | TODO: when used for both css and js, extend the class to validate file types 9 | ### 10 | 11 | constructor: -> 12 | @cache = {} 13 | 14 | put: (key, value) -> 15 | @cache[key] = value 16 | 17 | get: (key) -> 18 | if @cache[key] isnt undefined then @cache[key] else null 19 | 20 | containsKey: (key) -> 21 | if @cache[key] isnt undefined then return true else return false 22 | 23 | invalidate: -> 24 | @cache = {} 25 | -------------------------------------------------------------------------------- /lib/quick-editor-view.coffee: -------------------------------------------------------------------------------- 1 | {Range, Point, CompositeDisposable} = require 'atom' 2 | {$, View} = require 'atom-space-pen-views' 3 | AddSelectorView = require './add-selector-view' 4 | 5 | 6 | module.exports = 7 | class QuickEditorView extends View 8 | 9 | ### Life Cycle Methods ### 10 | @content: -> 11 | @div class: "quick-editor" 12 | 13 | initialize: -> 14 | [@file, @text, @editRange, @lastObj] = [] 15 | @lineDelta = 0 16 | 17 | @textEditorView = document.createElement 'atom-text-editor' 18 | @textEditor = @textEditorView.getModel() 19 | 20 | @addSelectorView = new AddSelectorView 21 | 22 | @grammarReg = atom.grammars 23 | @subscriptions = new CompositeDisposable 24 | 25 | attached: -> 26 | 27 | destroy: -> 28 | @subscriptions.dispose() 29 | 30 | ### State Getter / Setter Methods ### 31 | 32 | setFile: (file) -> 33 | @file = file 34 | 35 | setText: (text) -> 36 | @text = text 37 | @textEditor.setText(@text) 38 | 39 | setGrammar: -> 40 | throw new Error "Must set text & file" if @fill is null or @text is null 41 | grammar = @grammarReg.selectGrammar @file.getPath(), @text 42 | @textEditor.setGrammar grammar 43 | 44 | setEditRange: (range) -> 45 | @editRange = range 46 | 47 | setHeight: (edit)-> 48 | if edit 49 | lineHeight = atom.workspace.getActiveTextEditor().getLineHeightInPixels() 50 | numLines = @editRange.end.row - @editRange.start.row + 1 + @lineDelta 51 | @element.style.height = (lineHeight * numLines) + "px" 52 | else 53 | @element.style.height = "90px" 54 | 55 | setOnSelectorAdded: (callback) -> 56 | @addSelectorView.onSelectorAdded = callback 57 | 58 | ### View Methods ### 59 | attachEditorView: -> 60 | throw new Error "Must choose a file to quick-edit" if @file is null 61 | this.append @textEditorView 62 | @setGutterNumbers(-1) 63 | @subscriptions.add @textEditor.getBuffer().onDidChange( 64 | @onBufferChangeCallback.bind(@) 65 | ) 66 | 67 | @lineDelta = 0 68 | @setHeight(true) 69 | 70 | attachAddSelectorView: (selector, path)-> 71 | @addSelectorView.setSelector(selector) 72 | @addSelectorView.setInitialPath(path) 73 | this.append @addSelectorView 74 | @setHeight(false) 75 | 76 | detachEditorView: -> 77 | @detachPreviousView() 78 | @subscriptions.remove @textEditor.getBuffer().onDidChange( 79 | @onBufferChangeCallback.bind(@) 80 | ) 81 | @file.read().then (content) => 82 | modifyingTextEditor = document.createElement('atom-text-editor').getModel() 83 | modifyingTextEditor.getBuffer().setPath @file.getPath() 84 | modifyingTextEditor.setText content 85 | 86 | modifiedSelector = @textEditor.getText() 87 | modifyingTextEditor.setTextInBufferRange(@editRange, modifiedSelector) 88 | modifyingTextEditor.save() 89 | 90 | detachAddSelectorView: -> 91 | @detachPreviousView() 92 | return 93 | 94 | detachPreviousView: -> #might want to make this remove isntead 95 | previousView = $(@element).children() 96 | if previousView.length 97 | previousView.detach() 98 | 99 | setGutterNumbers: (num) -> 100 | i = 0 101 | for j in [@editRange.start.row + 1..(@editRange.end.row + @lineDelta + 1)] 102 | @setRowNumber(@getRowElementByLineNumber(i), j) 103 | i++ 104 | 105 | if num > 0 106 | # This is s horrible hack. There is no callback to listen to 107 | # when gutter numbers change, so this had to be done to keep the 108 | # gutter numbers updated. 109 | cb = -> @setGutterNumbers(num - 1) 110 | window.setTimeout(cb.bind(@), 5) 111 | 112 | getRowElementByLineNumber: (lineNumber) -> 113 | $(@textEditorView.shadowRoot).find('.line-number[data-screen-row="'+ lineNumber+ '"]') 114 | 115 | setRowNumber: (rowElement, newNumber) -> 116 | $(rowElement).html("#{newNumber}") 117 | 118 | onBufferChangeCallback: (obj) -> 119 | if obj.newRange.isEqual @lastObj?.newRange 120 | if obj.oldRange.isEqual @lastObj?.oldRange 121 | return 122 | @lastObj = obj 123 | # Sometimes the textBuffer likes to call this callback twice with the same obj 124 | 125 | newRows = obj.newRange.end.row - obj.newRange.start.row 126 | oldRows = obj.oldRange.end.row - obj.oldRange.start.row 127 | if newRows isnt oldRows 128 | if newRows > oldRows then @lineDelta++ else @lineDelta-- 129 | @setHeight(true) 130 | @setGutterNumbers(5) 131 | -------------------------------------------------------------------------------- /lib/quick-editor.coffee: -------------------------------------------------------------------------------- 1 | quickEditorView = require './quick-editor-view' 2 | DirectoryCSSSearcher = require './directory-css-searcher' 3 | MarkupParser = require './markup-parser' 4 | QuickEditorCache = require './quick-editor-cache' 5 | {Range, Point, CompositeDisposable, TextBuffer, File} = require 'atom' 6 | 7 | module.exports = QuickEditor = 8 | 9 | config: 10 | stylesDirectory: 11 | type: 'string' 12 | default: '' 13 | 14 | ### Life Cycle Methods ### 15 | quickEditorView : null 16 | panel : null 17 | subscriptions : null 18 | searcher : null 19 | parser: null 20 | 21 | activate: -> 22 | @quickEditorView = new quickEditorView() 23 | @quickEditorView.setOnSelectorAdded(@selectorAdded.bind(@)) 24 | @panel = atom.workspace.addBottomPanel(item: @quickEditorView, visible: false) 25 | 26 | @cssCache = new QuickEditorCache 27 | @searcher = new DirectoryCSSSearcher 28 | @parser = new MarkupParser 29 | 30 | # Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable 31 | @subscriptions = new CompositeDisposable 32 | 33 | # Register commands 34 | @subscriptions.add atom.commands.add 'atom-text-editor', 'quick-editor:quick-edit': => 35 | @quickEdit() 36 | 37 | deactivate: -> 38 | @panel.destroy() 39 | @subscriptions.dispose() 40 | @quickEditorView.destroy() 41 | 42 | ### Functionality Methods ### 43 | 44 | quickEdit: -> 45 | if @panel.isVisible() 46 | @closeView(@found) 47 | @panel.hide() 48 | else 49 | try 50 | @selector = @parseSelectedCSSSelector() 51 | catch e 52 | atom.beep() 53 | console.warn(e.message) 54 | return 55 | @findFilesFromCSSIdentifier(@selector) 56 | .then ([found, result]) => 57 | if found 58 | @setupForEditing(result.text, result.start, result.end, result.file) 59 | @edit() 60 | else 61 | @addNewSelector(@selector) 62 | .catch (e) -> 63 | console.error(e.message, e.stack) 64 | 65 | findFilesFromCSSIdentifier:(identifier) -> 66 | @searcher.findFilesThatContain identifier 67 | .then () => @searcher.getSelectorText().then ([found, result]) => 68 | @found = found 69 | @searcher.clear() 70 | if found 71 | path = atom.workspace.getActiveTextEditor().getPath() 72 | @cssCache.put(path, result.file.getPath()) 73 | return [found, result] 74 | .catch (e) -> 75 | console.error(e.message, e.stack) 76 | 77 | parseSelectedCSSSelector: -> 78 | editor = atom.workspace.getActiveTextEditor() 79 | @parser.setEditor(editor) 80 | @parser.parse() 81 | 82 | setupForEditing: (text, start, end, file) -> 83 | @quickEditorView.setText text 84 | @quickEditorView.setFile file 85 | @quickEditorView.setGrammar() 86 | 87 | range = new Range(new Point(start, 0), new Point(end, Infinity)) 88 | @quickEditorView.setEditRange(range) 89 | 90 | edit: -> 91 | @panel.show() 92 | @quickEditorView.attachEditorView() 93 | 94 | addNewSelector: (selector) -> 95 | unless path = @cssCache.get(atom.workspace.getActiveTextEditor().getPath()) 96 | path = atom.project.getPaths()[0] 97 | @quickEditorView.attachAddSelectorView(selector, path) 98 | @panel.show() 99 | 100 | selectorAdded: (path, selector) -> 101 | file = new File(path, false) 102 | buffer = new TextBuffer() 103 | buffer.setPath(path) 104 | file.read() 105 | .then (text) => 106 | buffer.setText(text) #TODO more efficent way to do this without researching 107 | buffer.append("\n" + selector + " {\n\n}") 108 | buffer.save() 109 | @closeView(false) 110 | @panel.hide() 111 | @findFilesFromCSSIdentifier(selector) 112 | .then ([found, result]) => 113 | @setupForEditing(result.text, result.start, result.end, result.file) 114 | @edit() 115 | 116 | 117 | closeView: (edit) -> 118 | if edit 119 | @quickEditorView.detachEditorView() 120 | else 121 | @quickEditorView.detachAddSelectorView() 122 | 123 | ### Methods for testing ### 124 | getView: -> 125 | return @quickEditorView 126 | -------------------------------------------------------------------------------- /lib/searcher-cache.coffee: -------------------------------------------------------------------------------- 1 | QuickEditorCache = require './quick-editor-cache' 2 | 3 | module.exports = 4 | class SearcherCache extends QuickEditorCache 5 | ### 6 | A cache for storing a mapping between: 7 | html files -> a list of all the style files that reference selectors used in 8 | the html files used as a key 9 | ### 10 | put: (key, value) -> 11 | unless @containsKey(key) 12 | @cache[key] = new Set() 13 | @cache[key].add(value) 14 | 15 | get: (key) -> 16 | ### 17 | Returns an array of all the values in the key's associated set. 18 | ### 19 | if @cache[key] isnt undefined 20 | valueArray = [] 21 | it = @cache[key].values() 22 | next = it.next() 23 | while not next.done 24 | valueArray.push(next.value) 25 | next = it.next() 26 | return valueArray 27 | else null 28 | -------------------------------------------------------------------------------- /menus/quick-editor.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': 'Quick-Edit' 6 | 'command': 'quick-editor:quick-edit' 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quick-editor", 3 | "main": "./lib/quick-editor", 4 | "version": "0.4.3", 5 | "description": "Quickly select and edit CSS/LESS/SCSS styles from the context of your markup.", 6 | "keywords": [ 7 | "css", 8 | "scss", 9 | "quick", 10 | "edit", 11 | "quick-editor" 12 | ], 13 | "activationCommands": { 14 | "atom-text-editor": "quick-editor:quick-edit" 15 | }, 16 | "repository": "https://github.com/Maushundb/quick-editor", 17 | "license": "MIT", 18 | "engines": { 19 | "atom": ">=1.0.0 <2.0.0" 20 | }, 21 | "dependencies": { 22 | "atom-space-pen-views": "^2.0.5", 23 | "chokidar": "^1.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /quick-edit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maushundb/quick-editor/1b39e4a07130b3ad37afeaeb48e4e26a0feab69f/quick-edit.gif -------------------------------------------------------------------------------- /spec/directory-css-searcher-spec.coffee: -------------------------------------------------------------------------------- 1 | DirectoryCSSSearcher = require '../lib/directory-css-searcher' 2 | File = require 'atom' 3 | 4 | describe "DirectoryCSSSearcher", -> 5 | dcs = null 6 | 7 | 8 | beforeEach -> 9 | waitsForPromise -> 10 | atom.workspace.open() 11 | runs -> 12 | atom.config.set('quick-editor.stylesDirectory', atom.project.getPaths()[0]) 13 | dcs = new DirectoryCSSSearcher 14 | 15 | it "finds files containing a specific Regex", -> 16 | waitsForPromise -> dcs.findFilesThatContain(".test-class") 17 | runs -> 18 | expect(dcs.file.getBaseName()).toBe("test.less") 19 | 20 | it "returns the proper range of a selector", -> 21 | waitsForPromise -> dcs.findFilesThatContain(".test-class") 22 | runs -> 23 | dcs.getSelectorText().then (success, result) -> 24 | expect(result.start).toBe(1) 25 | expect(result.end).toBe(5) 26 | 27 | it "only searches the specified styles directory", -> 28 | waitsForPromise -> dcs.findFilesThatContain(".false-class") 29 | runs -> 30 | dcs.getSelectorText().then (success, result) -> 31 | expect(result.success).toBe(false) 32 | -------------------------------------------------------------------------------- /spec/false-fixtures/false-test.less: -------------------------------------------------------------------------------- 1 | .false-class { 2 | width: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 |
8 |
9 |
10 |
11 |
12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/fixtures/test.less: -------------------------------------------------------------------------------- 1 | ////////// DO NOT CHANGE ////////// 2 | .test-class { 3 | height: 200px; 4 | margin: 0px auto; 5 | width: 100px; 6 | } 7 | 8 | .test-class-with-nesting { 9 | height: 200px; 10 | margin: 0px auto; 11 | width: 100px; 12 | 13 | .test-nested-class { 14 | height: 200px; 15 | margin: 0px auto; 16 | width: 100px; 17 | } 18 | } 19 | ////////// END DO NOT CHANGE ////////// 20 | 21 | #really-long-id { 22 | height: 200px; 23 | margin: 0px auto; 24 | width: 100px; 25 | color: red; 26 | 27 | .nestedName { 28 | display: flex; 29 | top: 20px; 30 | } 31 | } 32 | 33 | .other-id-3 { 34 | height: 200px; 35 | margin: 0px auto; 36 | width: 100px; 37 | animation-name: none; 38 | display: inline-block; 39 | } 40 | -------------------------------------------------------------------------------- /spec/markup-parser-spec.coffee: -------------------------------------------------------------------------------- 1 | MarkupParser = require '../lib/markup-parser' 2 | 3 | describe "MarkupParser", -> 4 | [textEditor, parser] = [] 5 | 6 | beforeEach -> 7 | waitsForPromise -> 8 | atom.workspace.open() 9 | runs -> 10 | textEditor = atom.workspace.getActiveTextEditor() 11 | parser = new MarkupParser 12 | parser.setEditor(textEditor) 13 | 14 | setupTextEditor = (text, pos) -> 15 | textEditor.setText(text) 16 | textEditor.setCursorBufferPosition(pos) 17 | 18 | it "gets the current line and buffer position", -> 19 | testLine= "
" 20 | testPos = [0, 0] 21 | setupTextEditor(testLine, testPos) 22 | [pos, line] = parser.getCurrentLine() 23 | expect(pos).toEqual(testPos) 24 | expect(line).toEqual(testLine) 25 | 26 | it "determines if a selector is an id or class", -> 27 | testLine = "
" 28 | testPos = [0, 11] 29 | setupTextEditor(testLine, testPos) 30 | idResult = parser.parse() 31 | 32 | testLine = "
" 33 | testPos = [0, 13] 34 | setupTextEditor(testLine, testPos) 35 | classResult = parser.parse() 36 | 37 | expect(idResult[0]).toBe("#") 38 | expect(classResult[0..1]).toBe("\\.") 39 | 40 | 41 | it "returns the proper selector name for a lone selector", -> 42 | testLine = "
" 43 | testPos = [0, 11] 44 | setupTextEditor(testLine, testPos) 45 | result = parser.parse() 46 | expect(result).toBe("#test") 47 | 48 | it "returns the class the cursor is over when two are separated by a space", -> 49 | testLine = "
" 50 | testPos1 = [0, 17] 51 | testPos2 = [0, 20] 52 | 53 | setupTextEditor(testLine, testPos1) 54 | result1 = parser.parse() 55 | 56 | setupTextEditor(testLine, testPos2) 57 | result2 = parser.parse() 58 | 59 | expect(result1).toBe("\\.test_1") 60 | expect(result2).toBe("\\.test_2") 61 | 62 | it "returns the class or id the cursor is over when the element has both", -> 63 | testLine = "
" 64 | testPos1 = [0, 14] 65 | testPos2 = [0, 27] 66 | 67 | setupTextEditor(testLine, testPos1) 68 | result1 = parser.parse() 69 | 70 | setupTextEditor(testLine, testPos2) 71 | result2 = parser.parse() 72 | 73 | expect(result1).toBe("\\.testc") 74 | expect(result2).toBe("#testi") 75 | 76 | it "throws an error when class or id is not present", -> 77 | testLine = "

<%= link_to \"Sign In\", some_path %>

" 78 | testPos = [0, 20] 79 | setupTextEditor(testLine, testPos) 80 | expect(parser.parse).toThrow() 81 | -------------------------------------------------------------------------------- /spec/quick-editor-spec.coffee: -------------------------------------------------------------------------------- 1 | QuickEditor = require '../lib/quick-editor' 2 | File = require 'atom' 3 | 4 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 5 | # 6 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` 7 | # or `fdescribe`). Remove the `f` to unfocus the block. 8 | 9 | describe "QuickEditor", -> 10 | [activationPromise, editor, editorView, workspaceView] = [] 11 | 12 | beforeEach -> 13 | waitsForPromise -> 14 | atom.workspace.open() 15 | 16 | runs -> 17 | workspaceView = atom.views.getView atom.workspace 18 | editor = atom.workspace.getActiveTextEditor() 19 | editorView = atom.views.getView editor 20 | editor.setText "id=\"test-class\"" 21 | editor.setCursorScreenPosition [0, 7] 22 | 23 | activationPromise = atom.packages.activatePackage 'quick-editor' 24 | 25 | activateAndThen = (callback) -> 26 | atom.commands.dispatch(editorView, 'quick-editor:quick-edit') 27 | waitsForPromise -> activationPromise 28 | runs(callback) 29 | 30 | describe "when a quick-editor:quick-edit event is triggered", -> 31 | it "activates", -> 32 | activateAndThen -> 33 | expect(workspaceView.querySelector(".quick-editor")).toExist() 34 | 35 | it "shows a panel", -> 36 | activateAndThen -> 37 | waitsFor -> workspaceView.querySelector ".quick-editor" 38 | 39 | it "correctly parses it's editors selected css identifier", -> 40 | activateAndThen -> 41 | selector = QuickEditor.parseSelectedCSSSelector() 42 | expect(selector).toBe("#test-class") 43 | -------------------------------------------------------------------------------- /spec/quick-editor-view-spec.coffee: -------------------------------------------------------------------------------- 1 | QuickEditorView = require '../lib/quick-editor-view' 2 | 3 | xdescribe "QuickEditorView", -> 4 | it "test", -> 5 | return 6 | -------------------------------------------------------------------------------- /styles/quick-editor.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 | @import "buttons"; 7 | 8 | .quick-editor { 9 | overflow: hidden; 10 | 11 | atom-text-editor { 12 | position: absolute; 13 | width: 100%; 14 | } 15 | } 16 | 17 | .add-selector-view { 18 | padding: @component-padding 5px; 19 | 20 | h2 { 21 | margin: 0px 0px 0px 0px; 22 | } 23 | .top-container { 24 | display: flex; 25 | flex-direction: row; 26 | 27 | .top-container-left { 28 | margin-right: 20px; 29 | .no-selector-container { 30 | display: flex; 31 | margin-bottom: 10px; 32 | 33 | .no-selector-text { 34 | margin-right: 10px; 35 | margin-top: auto; 36 | 37 | color: @text-color-subtle; 38 | } 39 | 40 | .selector { 41 | display: inline-block; 42 | font-size: @font-size+2; 43 | max-width: 60% 44 | } 45 | } 46 | } 47 | 48 | .top-container-right { 49 | display: flex; 50 | flex-direction: row; 51 | margin: auto 30px; 52 | .btn { 53 | width: 90px; 54 | height: 25px; 55 | background-color: @text-color-subtle; 56 | } 57 | } 58 | 59 | } 60 | 61 | 62 | .add-new-style-container { 63 | display: flex; 64 | flex-direction: row; 65 | margin-top: auto; 66 | height: @component-line-height; 67 | 68 | .add-new-selector-text { 69 | font-size: @font-size; 70 | margin: auto 5px; 71 | 72 | } 73 | atom-text-editor { 74 | margin-left: 2px; 75 | } 76 | } 77 | 78 | 79 | } 80 | --------------------------------------------------------------------------------