diff --git a/docs/your-first-package.md b/docs/your-first-package.md index 9f9901229..b09268a7e 100644 --- a/docs/your-first-package.md +++ b/docs/your-first-package.md @@ -1,328 +1,145 @@ -# Creating Your First Package +# Create Your First Package -Let's take a look at creating your first package. +We are going to create a simple package that replaces selected text with ascii +art. So if "cool" was selected the output would be: -To get started, hit `cmd-shift-P`, and start typing "Generate Package" to generate -a package. Once you select the "Generate Package" command, it'll ask you for a -name for your new package. Let's call ours _changer_. +``` + /\_ \ + ___ ___ ___\//\ \ + /'___\ / __`\ / __`\\ \ \ +/\ \__//\ \L\ \/\ \L\ \\_\ \_ +\ \____\ \____/\ \____//\____\ + \/____/\/___/ \/___/ \/____/ +``` -Atom will pop open a new window, showing the _changer_ package with a default set of -folders and files created for us. Hit `cmd-shift-P` and start typing "Changer." You'll -see a new `Changer:Toggle` command which, if selected, pops up a greeting. So far, -so good! +The final package can be viewed at +[https://github.com/atom/ascii-art]([https://github.com/atom/ascii-art]). -In order to demonstrate the capabilities of Atom and its API, our Changer plugin -is going to do two things: +To begin, press `cmd-shift-P` to bring up the Command Palette. Type "generate +package" and select the "Package Generator: Generate Package" command. You will +be prompt for package name, let's call ours _ascii-art_. -1. It'll show only modified files in the file tree -2. It'll append a new pane to the editor with some information about the modified -files +Atom will open a new window with the _ascii-art_ package contents displayed in +the Tree View. This window will already has the Ascii Art package installed. If +you toggle the Command Palette `cmd-shift-P` and type "Ascii Art" you'll see a +new `Ascii Art: Toggle` command. If triggered, this command displays a message +created by the package generator. -Let's get started! - -## Changing Keybindings and Commands - -Since Changer is primarily concerned with the file tree, let's write a -key binding that works only when the tree is focused. Instead of using the -default `toggle`, our keybinding executes a new command called `magic`. - -_keymaps/changer.cson_ should change to look like this: +Now let's edit the package files to make our ascii art package work! Since this +package doesn't need any UI we can remove all view related code. Start by +opening up _lib/ascii-art.coffee_. Remove all view code until the file looks +like this: ```coffeescript -'.tree-view': - 'ctrl-V': 'changer:magic' + module.exports = + activate: -> ``` -Notice that the keybinding is called `ctrl-V` — that's actually `ctrl-shift-v`. -You can use capital letters to denote using `shift` for your binding. +## Create Command -`.tree-view` represents the parent container for the tree view. -Keybindings only work within the context of where they're entered. In this case, -hitting `ctrl-V` anywhere other than tree won't do anything. Obviously, you can -bind to any part of the editor using element, id, or class names. For example, -you can map to `body` if you want to scope to anywhere in Atom, or just `.editor` -for the editor portion. - -To bind keybindings to a command, we'll need to do a bit of association in our -CoffeeScript code using the `atom.workspaceView.command` method. This method takes a command -name and executes a callback function. Open up _lib/changer-view.coffee_, and -change `atom.workspaceView.command "changer:toggle"` to look like this: +Now let's add a command. It's recommended to namespace your commands with your +package name, separated with a colon (`:`). So we'll call this command +`ascii-art:convert`. Register the command in _lib/ascii-art.coffee_: ```coffeescript -atom.workspaceView.command "changer:magic", => @magic() +module.exports = + activate: -> + atom.workspaceView.command "ascii-art:convert", => @convert() + + convert: -> + # This assumes the active pane item is an editor + editor = atom.workspace.activePaneItem. + selection = editor.getSelection() + upperCaseSelectedText = selection.getText().toUpperCase() + selection.insertText(upperCaseSelectedText) ``` -It's common practice to namespace your commands with your package name, separated -with a colon (`:`). Make sure to rename your `toggle` method to `magic` as well. +`atom.workspaceView.command` method takes a command name and a callback. The +callback executes when the command is triggered. In this case, when the command +is triggered the callback will uppercase the selected text. -Every time you reload the Atom editor, changes to your package code will be reevaluated, -just as if you were writing a script for the browser. Reload the editor, click on -the tree, hit your keybinding, and...nothing happens! What the heck?! +## Reload The Package -Open up the _package.json_ file, and find the property called `activationEvents`. -Basically, this key tells Atom to not load a package until it hears a certain event. -Change the event to `changer:magic` and reload the editor: +Before we trigger the `ascii-art:convert` we'll need to reload the window. This +will reevaluate our package code, just as if we were writing a script for the +browser. Trigger `window:reload` command using the command palette or press +`ctrl-alt-cmd-l`. -```json -"activationEvents": ["changer:magic"] -``` +## Trigger the command -Hitting the key binding on the tree now works! - -## Working with Styles - -The next step is to hide elements in the tree that aren't modified. To do that, -we'll first try and get a list of files that have not changed. - -All packages are able to use jQuery in their code. In fact, there's [a list of -the bundled libraries Atom provides by default][bundled-libs]. - -We bring in jQuery by requiring the `atom` package and binding it to the `$` variable: +Open the command panel and search for the `ascii-art:convert` command. Its not +there, what the heck?! To fix this open _package.json_ and find the property +called `activationEvents`. Activation Events speed up load time by allowing an +Atom to delay it's activation until needed. So add the `ascii-art:convert` to +the activationEvents array: ```coffeescript -{$, View} = require 'atom' +"activationEvents": ["ascii-art:convert"], ``` -Now, we can define the `magic` method to query the tree to get us a list of every -file that _wasn't_ modified: +Now reload window `ctrl-alt-cmd-l` and use the command panel to trigger the +`ascii-art:convert` command. It will uppercase any text you have selected. + +## Add Key Binding + +Now let's add a key binding to trigger the `ascii-art:convert` command. Open +_keymaps/ascii-art.cson_ and add a key binding linking `ctrl-alt-a` to the +`ascii-art:convert` command. + +When finished, the file will look like this: ```coffeescript -magic: -> - $('ol.entries li').each (i, el) -> - if !$(el).hasClass("status-modified") - console.log el +'.editor': + 'cmd-alt-a': 'ascii-art:convert' ``` -You can access the dev console by hitting `alt-cmd-i`. Here, you'll see all the -statements from `console` calls. When we execute the `changer:magic` command, the -browser console lists items that are not being modified (_i.e._, those without the -`status-modified` class). Let's add a class to each of these elements called `hide-me`: +Notice the `.editor` line. This limits the key binding to only work when the +focused element matches the selector `.editor`, much like CSS. For example, if +the Tree View has focus, pressing `cmd-alt-a` won't trigger the +`ascii-art:convert` command. But if the editor has focus, the +`ascii-art:convert` method will be triggered. More information on key bindings +can be found in the [keymaps][keymaps] documentation. + +Now reload the window and verify that pressing the key binding works! You can +also verify that it **doesn't** work when the Tree View is focused. + +## Add The Ascii Art + +Now the fun part, we need to convert the selected text to ascii art. To do this +we will use the [figlet node module](https://npmjs.org/package/figlet). Open +_package.json_ add the latest version of figlet to the dependencies: ```coffeescript -magic: -> - $('ol.entries li').each (i, el) -> - if !$(el).hasClass("status-modified") - $(el).addClass("hide-me") + "dependencies": { + "figlet": "1.0.8" + } ``` -With our newly added class, we can manipulate the visibility of the elements -with a simple stylesheet. Open up _changer.css_ in the _stylesheets_ directory, -and add a single entry: +NOW GO TO THE COMMAND LINE AND RUN APM UPDATE BUT REALLY THIS STEP SEEMS LIKE +IT COULD BE AN ATOM COMMAND. -```css -ol.entries .hide-me { - display: none; -} -``` - -Refresh Atom, and run the `changer` command. You'll see all the non-changed -files disappear from the tree. Success! - -![Changer_File_View] - -There are a number of ways you can get the list back; let's just naively iterate -over the same elements and remove the class: +Now you can require the figlet node module in _lib/ascii-art.coffee_ and +instead of uppercasing the text, you can convert it to ascii art! ```coffeescript -magic: -> - $('ol.entries li').each (i, el) -> - if !$(el).hasClass("status-modified") - if !$(el).hasClass("hide-me") - $(el).addClass("hide-me") - else - $(el).removeClass("hide-me") +convert: -> + # This assumes the active pane item is an editor + selection = atom.workspace.activePaneItem.getSelection() + + figlet = require 'figlet' + figlet selection.getText(), {font: "Larry 3D 2"}, (error, asciiArt) -> + if error + console.error(error) + else + selection.insertText("\n" + asciiArt + "\n") ``` -## Creating a New Panel - -The next goal of this package is to append a panel to the Atom editor that lists -some information about the modified files. - -To do that, we're going to first open up [the style guide][styleguide]. The Style -Guide lists every type of UI element that can be created by an Atom package. Aside -from helping you avoid writing fresh code from scratch, it ensures that packages -have the same look and feel no matter how they're built. - -Every package that extends from the `View` class can provide an optional class -method called `content`. The `content` method constructs the DOM that your -package uses as its UI. The principals of `content` are built entirely on -[SpacePen][space-pen], which we'll touch upon only briefly here. - -Our display will simply be an unordered list of the file names, and their -modified times. We'll append this list to a panel on the bottom of the editor. A -basic `panel` element inside a `tool-panel` will work well for us. Let's start by carving out a -`div` to hold the filenames: - -```coffeescript -@content: -> - @div class: "changer tool-panel panel-bottom", => - @div class: "panel", => - @div class: "panel-heading", "Modified Files" - @div class: "panel-body padded", outlet: 'modifiedFilesContainer', => - @ul class: 'modified-files-list', outlet: 'modifiedFilesList', => - @li 'Modified File Test' - @li 'Modified File Test' -``` - -You can add any HTML attribute you like. `outlet` names the variable your -package can use to manipulate the element directly. The fat arrow (`=>`) -indicates that the next DOM set are nested children. - -Once again, you can style `li` elements using your stylesheets. Let's test that -out by adding these lines to the _changer.css_ file: - -```css -ul.modified-files-list { - color: white; -} -``` - -We'll add one more line to the end of the `magic` method to make this pane -appear: - -```coffeescript -atom.workspaceView.prependToBottom(this) -``` - -If you refresh Atom and hit the key command, you'll see a box appear right -underneath the editor: - -![Changer_Panel_Append] - -As you might have guessed, `atom.workspaceView.prependToBottom` tells Atom to -prepend `this` item (_i.e._, whatever is defined by`@content`). If we had called -`atom.workspaceView.appendToBottom`, the pane would be attached below the status -bar. - -Before we populate this panel for real, let's apply some logic to toggle the -pane off and on, just like we did with the tree view. Replace the -`atom.workspaceView.prependToBottom` call with this code: - -```coffeescript -# toggles the pane -if @hasParent() - @remove() -else - atom.workspaceView.prependToBottom(this) -``` - -There are about a hundred different ways to toggle a pane on and off, and this -might not be the most efficient one. If you know your package needs to be -toggled on and off more freely, it might be better to draw the interface during the -initialization, then immediately call `hide()` on the element to remove it from -the view. You can then swap between `show()` and `hide()`, and instead of -forcing Atom to add and remove the element as we're doing here, it'll just set a -CSS property to control your package's visibility. - -Refresh Atom, hit the key combo, and watch your test list appear and disappear. - -## Calling Node.js Code - -Since Atom is built on top of [Node.js][node], you can call any of its libraries, -including other modules that your package requires. - -We'll iterate through our resulting tree, and construct the path to our modified -file based on its depth in the tree. We'll use Node to handle path joining for -directories. - -Add the following Node module to the top of your file: - -```coffeescript -path = require 'path' -``` - -Then, add these lines to your `magic` method, _before_ your pane drawing code: - -```coffeescript -modifiedFiles = [] -# for each single entry... -$('ol.entries li.file.status-modified span.name').each (i, el) -> - filePath = [] - # ...grab its name... - filePath.unshift($(el).text()) - - # ... then find its parent directories, and grab their names - parents = $(el).parents('.directory.status-modified') - parents.each (i, el) -> - filePath.unshift($(el).find('div.header span.name').eq(0).text()) - - modifiedFilePath = path.join(atom.project.rootDirectory.path, filePath.join(path.sep)) - modifiedFiles.push modifiedFilePath -``` - -`modifiedFiles` is an array containing a list of our modified files. We're also -using the node.js [`path` library][path] to get the proper directory separator -for our system. - -Remove the two `@li` elements we added in `@content`, so that we can -populate our `modifiedFilesList` with real information. We'll do that by -iterating over `modifiedFiles`, accessing a file's last modified time, and -appending it to `modifiedFilesList`: - -```coffeescript -# toggles the pane -if @hasParent() - @remove() -else - for file in modifiedFiles - stat = fs.lstatSync(file) - mtime = stat.mtime - @modifiedFilesList.append("
  • #{file} - Modified at #{mtime}") - atom.workspaceView.prependToBottom(this) -``` - -When you toggle the modified files list, your pane is now populated with the -filenames and modified times of files in your project: - -![Changer_Panel_Timestamps] - -You might notice that subsequent calls to this command reduplicate information. -We could provide an elegant way of rechecking files already in the list, but for -this demonstration, we'll just clear the `modifiedFilesList` each time it's closed: - -```coffeescript -# toggles the pane -if @hasParent() - @modifiedFilesList.empty() # added this to clear the list on close - @remove() -else - for file in modifiedFiles - stat = fs.lstatSync(file) - mtime = stat.mtime - @modifiedFilesList.append("
  • #{file} - Modified at #{mtime}") - atom.workspaceView.prependToBottom(this) -``` - -## Coloring UI Elements - -For packages that create new UI elements, adhering to the style guide is just one -part to keeping visual consistency. Packages dealing with color, fonts, padding, -margins, and other visual cues should rely on [Theme Variables][theme-vars], instead -of developing individual styles. Theme variables are variables defined by Atom -for use in packages and themes. They're only available in [`LESS`](http://lesscss.org/) -stylesheets. - -For our package, let's remove the style defined by `ul.modified-files-list` in -_changer.css_. Create a new file under the _stylesheets_ directory called _text-colors.less_. -Here, we'll import the _ui-variables.less_ file, and define some Atom-specific -styles: - -```less -@import "ui-variables"; - -ul.modified-files-list { - color: @text-color; - background-color: @background-color-info; -} -``` - -Using theme variables ensures that packages look great alongside any theme. - ## Further reading For more information on the mechanics of packages, check out [Creating a Package][creating-a-package]. +[keymaps]: advanced/keymaps.html [bundled-libs]: creating-a-package.html#included-libraries [styleguide]: https://github.com/atom/styleguide [space-pen]: https://github.com/atom/space-pen