├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── Clips.json ├── Images ├── RailsLogo │ ├── RailsLogo.png │ ├── RailsLogo@2x.png │ └── metadata.json ├── RailsSidebarLarge │ ├── RailsSidebarLarge.png │ ├── RailsSidebarLarge@2x.png │ └── metadata.json ├── RailsSidebarLargeSelected │ ├── RailsSidebarLargeSelected.png │ ├── RailsSidebarLargeSelected@2x.png │ └── metadata.json ├── RailsSidebarSmall │ ├── RailsSidebarSmall.png │ ├── RailsSidebarSmall@2x.png │ └── metadata.json ├── RailsSidebarSmallSelected │ ├── RailsSidebarSmallSelected.png │ ├── RailsSidebarSmallSelected@2x.png │ └── metadata.json ├── task-bridgetown │ ├── task-bridgetown.png │ └── task-bridgetown@2x.png └── task-rails │ ├── task-rails.png │ └── task-rails@2x.png ├── LICENSE ├── README.md ├── Scripts ├── commands.js ├── commands │ ├── MailerPreview.js │ ├── RailsAlternateFile.js │ ├── RailsDocumentation.js │ ├── RailsImportmap.js │ ├── RailsInfo.js │ ├── RailsMigrations.js │ ├── RailsRelatedFiles.js │ ├── RailsServer.js │ ├── RailsStimulus.js │ └── erbTagSwitcher.js ├── helpers.js ├── main.js ├── other │ └── VersionChecker.js ├── settings.js ├── settings │ └── general │ │ └── statusNotifications.js ├── sidebars.js ├── sidebars │ ├── AboutView.js │ └── NotesView.js └── types │ └── nova.d.ts ├── Tests ├── ruby.rb ├── stimulus_controller.js ├── test.html.erb └── test_controller.rb ├── docs └── images │ └── stimulus-clips.png ├── extension.json ├── extension.png ├── extension@2x.png ├── jsconfig.json └── misc ├── extension.afdesign └── extension.png /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nova 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Version 8.0 2 | 3 | ### FEAT 4 | 5 | - Add Preview mailer command 6 | 7 | ### IMPROVE 8 | 9 | - Improve related files command by looking for mailers and locales files 10 | 11 | ## Version 7.0 12 | 13 | ### FEAT 14 | 15 | - Add Show Related Files command 16 | - Add StimulusJS outlets Clips 17 | 18 | ### IMPROVE 19 | 20 | - Remove Solargraph and Rubocop in favor of the new and improved [Solargraph](nova://extension/?id=com.tommasonegri.solargraph&name=Solargraph) extension 21 | - Remove Bridgetown template task in favor of the new and improved [Bridgetown](nova://extension/?id=com.tommasonegri.bridgetown&name=Bridgetown) extension 22 | 23 | ### FIX 24 | 25 | - Change Ruby clip shortcut with an available one 26 | - Use the new `hotwired.dev` domain for documentation by @gobijan in #33 and #34 27 | 28 | ## Version 6.2 29 | 30 | ### IMPROVE 31 | 32 | - Update task-bridgetown icons to avoid a resizing issue 33 | 34 | ## Version 6.1 35 | 36 | ### FIX 37 | 38 | - Prevent "do" and "do (yield)" clips to popup when typing "end" 39 | - Update noteParts regexp to handle Notes with empty comments by @hmaddocks 40 | 41 | ## Version 6.0 42 | 43 | ### FEAT 44 | 45 | - Improved RuboCop support with autocorrect commands, fix on save and offenses list 46 | 47 | ### REFACTOR 48 | 49 | - Update extension structure to not use npm 50 | - Update extension icons 51 | 52 | ### Fix 53 | 54 | - Resolved a parsing issue of Rails Notes with "/" in it 55 | - Removed an error-prone option from task templates 56 | 57 | ## Version 5.2 58 | 59 | ### FIX 60 | 61 | - When rails notes/about fails to run, save the error output and return it in the Extension error logs 62 | 63 | ## Version 5.1 64 | 65 | ### FIX 66 | 67 | - Fixed an issue with Rails Notes regex pattern 68 | 69 | ## Version 5.0 70 | 71 | ### FEATURES 72 | 73 | - Replaced the automatically provided Rails server task with an optional Task template 74 | - Added new Rails server task template (equivalent to bin/dev) 75 | - Added new Bridgetown task template with Run, Build and Clean command 76 | 77 | ### DOCS 78 | 79 | - Added a tutorial for installing and configuring Solargraph 80 | 81 | ## Version 4.0 82 | 83 | ### FEATURES 84 | 85 | - Added new Rails Notes sidebar 86 | 87 | ### REFACTOR 88 | 89 | - Improved code quality and style 90 | 91 | ## Version 3.3 92 | 93 | ### FIX 94 | 95 | - Honor both Gemfile as well as gems.rb for Rails project detection 96 | 97 | ## Version 3.2 98 | 99 | ### FIX 100 | 101 | - Remove JS private methods in classes for compatibility reasons 102 | 103 | ## Version 3.1 104 | 105 | ### FIX 106 | 107 | - Fixed an issue with the importmaps commands and multiple packages 108 | 109 | ## Version 3.0 110 | 111 | ### FEATURES 112 | 113 | - Added new commands for pinning and unpinning packages from importmap 114 | - Added new Clips for ERB, Ruby and Rails based on *DHH Ruby* bundle for TextMate 115 | 116 | ### DOCS 117 | 118 | - Streamlined and updated the README. 119 | 120 | ## Version 2.0 121 | 122 | ### FEATURES 123 | 124 | - Added new Clips and updated existing ones to follow Stimulus v3.0.0 125 | - Added new command for updating Stimulus manifest (./bin/rails stimulus:manifest:update) 126 | - Added new commands for opening rails/info/routes and rails/info/properties 127 | 128 | ### FIX 129 | 130 | - Fixed an issue where isRailsProject wasn't detecting projects created with Rails 7.0 131 | 132 | ## Version 1.0 133 | 134 | ### FEATURES 135 | 136 | - Added Solargraph support with relative settings 137 | 138 | ## Version 0.9 139 | 140 | ### DOCS 141 | 142 | - Added extension icon in the README 143 | - Added StimulusJS as keyword 144 | - Exited from Alpha stage 145 | 146 | ## Version 0.8 - Alpha 147 | 148 | ### FEATURES 149 | 150 | - Added a Version Checker for notify user of background updates 151 | 152 | ## Version 0.7 - Alpha 153 | 154 | ### FEATURES 155 | 156 | - Added new Clips for Stimulus in HTML (100% coverage). Every Clip uses official naming conventions for placeholders, suggesting you the correct text format (camelCase, kebab-case, etc). 157 | ![Stimulus Clips](https://raw.githubusercontent.com/nova-ruby/rails/main/docs/images/stimulus-clips.png) 158 | 159 | ### IMPROVE 160 | 161 | - Clean up console logs of the ERB Tag Switcher for production 162 | 163 | ## Version 0.6 - Alpha 164 | 165 | ### FEATURES 166 | 167 | - Added a command for killing Puma server processes 168 | - Added a command for applying the latest migration 169 | - Added a command for applying a rollback 170 | - Added a command for opening the Extension Wiki 171 | 172 | ## Version 0.5 - Alpha 173 | 174 | ### FEATURES 175 | 176 | - Added Rails project detection 177 | - Added Rails Server Task 178 | - Added Rails About sidebar 179 | - Added Rails Documentation search 180 | - Added Rails list and last migration commands 181 | - Added Rails alternate file command 182 | - Added Erb Tag Switcher 183 | - Added Status Notifications 184 | -------------------------------------------------------------------------------- /Clips.json: -------------------------------------------------------------------------------- 1 | { 2 | "clips": [ 3 | { 4 | "name": "ERB", 5 | "children": [ 6 | { 7 | "content": "<% $0 %>", 8 | "name": "ERB expression", 9 | "scope": "editor", 10 | "shortcut": "ctrl-x", 11 | "syntax": "html+erb", 12 | "trigger": "<%" 13 | }, 14 | { 15 | "content": "<%= $0 %>", 16 | "name": "ERB insert", 17 | "scope": "editor", 18 | "shortcut": "ctrl-z", 19 | "syntax": "html+erb", 20 | "trigger": "<%=" 21 | } 22 | ] 23 | }, 24 | { 25 | "name": "Rails", 26 | "children": [ 27 | { 28 | "content": "module $0\n\textend ActiveSupport::Concern\n\n\t$1\nend", 29 | "name": "concern", 30 | "scope": "editor", 31 | "syntax": "ruby", 32 | "trigger": "concern" 33 | }, 34 | { 35 | "content": "params[:${:id}]", 36 | "name": "params", 37 | "scope": "editor", 38 | "shortcut": "ctrl-p", 39 | "syntax": "ruby", 40 | "trigger": "params" 41 | }, 42 | { 43 | "content": "test \"$0\" do\n\t$1\nend", 44 | "name": "test case", 45 | "scope": "editor", 46 | "syntax": "ruby", 47 | "trigger": "test" 48 | } 49 | ] 50 | }, 51 | { 52 | "name": "Ruby", 53 | "children": [ 54 | { 55 | "content": "{ |${0:e}| $1 ", 56 | "name": "block", 57 | "scope": "editor", 58 | "syntax": "ruby", 59 | "trigger": "{" 60 | }, 61 | { 62 | "content": "do\n\t$0\nend", 63 | "name": "do", 64 | "scope": "editor", 65 | "syntax": "ruby", 66 | "trigger": "do" 67 | }, 68 | { 69 | "content": "do |$0|\n\t$1\nend", 70 | "name": "do (yield)", 71 | "scope": "editor", 72 | "syntax": "ruby", 73 | "trigger": "doo" 74 | }, 75 | { 76 | "content": "def $0\n\t$1\nend", 77 | "name": "New method", 78 | "scope": "editor", 79 | "shortcut": "command-option-return", 80 | "syntax": "ruby", 81 | "trigger": "def" 82 | } 83 | ] 84 | }, 85 | { 86 | "name": "Stimulus (HTML)", 87 | "children": [ 88 | { 89 | "content": "data-controller=\"${:controller-identifier}\"", 90 | "name": "Stimulus Controller", 91 | "scope": "editor", 92 | "syntax": "html", 93 | "trigger": "stimulus controller" 94 | }, 95 | { 96 | "content": "data-action=\"${:event}->${:controller-identifier}#${:actionName}\"", 97 | "name": "Stimulus Action", 98 | "scope": "editor", 99 | "syntax": "html", 100 | "trigger": "stimulus action" 101 | }, 102 | { 103 | "content": "data-action=\"${:event}->${:controller-identifier}#${:actionName}:${:option}\"", 104 | "name": "Stimulus Action + Option", 105 | "scope": "editor", 106 | "syntax": "html", 107 | "trigger": "stimulus action option" 108 | }, 109 | { 110 | "content": "data-${:controller-identifier}-${:param-name}-param=\"${:param}\"", 111 | "name": "Stimulus Param", 112 | "scope": "editor", 113 | "syntax": "html", 114 | "trigger": "stimulus param" 115 | }, 116 | { 117 | "content": "data-${:controller-identifier}-target=\"${:targetName}\"", 118 | "name": "Stimulus Target", 119 | "scope": "editor", 120 | "syntax": "html", 121 | "trigger": "stimulus target" 122 | }, 123 | { 124 | "content": "data-${:controller-identifier}-${:outlet-name}-target=\"${:css-selector}\"", 125 | "name": "Stimulus Outlet", 126 | "scope": "editor", 127 | "syntax": "html", 128 | "trigger": "stimulus outlet" 129 | }, 130 | { 131 | "content": "data-${:controller-identifier}-${:value-name}-value=\"${:value}\"", 132 | "name": "Stimulus Value", 133 | "scope": "editor", 134 | "syntax": "html", 135 | "trigger": "stimulus value" 136 | }, 137 | { 138 | "content": "data-${:controller-identifier}-${:class-name}-class=\"${:css-class}\"", 139 | "name": "Stimulus Class", 140 | "scope": "editor", 141 | "syntax": "html", 142 | "trigger": "stimulus class" 143 | } 144 | ] 145 | }, 146 | { 147 | "name": "Stimulus (JS)", 148 | "children": [ 149 | { 150 | "content": "import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n\t${:Code}\n}", 151 | "name": "Stimulus Controller", 152 | "scope": "editor", 153 | "syntax": "javascript", 154 | "trigger": "stimulus controller" 155 | }, 156 | { 157 | "content": "static targets = [\"${:targetName}\"]", 158 | "name": "Stimulus Targets", 159 | "scope": "editor", 160 | "syntax": "javascript", 161 | "trigger": "stimulus targets" 162 | }, 163 | { 164 | "content": "static outlets = [\"${:outletName}\"]", 165 | "name": "Stimulus Outlets", 166 | "scope": "editor", 167 | "syntax": "javascript", 168 | "trigger": "stimulus outlets" 169 | }, 170 | { 171 | "content": "static values = {\n\t${:valueName}: ${:Type}\n}", 172 | "name": "Stimulus Values", 173 | "scope": "editor", 174 | "syntax": "javascript", 175 | "trigger": "stimulus values" 176 | }, 177 | { 178 | "content": "static values = {\n\t${:valueName}: {\n\t\ttype: ${:Type},\n\t\tdefault: ${:value}\n\t}\n}", 179 | "name": "Stimulus Values + Defaults", 180 | "scope": "editor", 181 | "syntax": "javascript", 182 | "trigger": "stimulus values defaults" 183 | }, 184 | { 185 | "content": "static classes = [\"${:className}\"]", 186 | "name": "Stimulus Classes", 187 | "scope": "editor", 188 | "syntax": "javascript", 189 | "trigger": "stimulus classes" 190 | }, 191 | { 192 | "content": "this.dispatch(\"${:eventName}\", { detail: ${:payloadObject} })", 193 | "name": "Stimulus Dispatch", 194 | "scope": "editor", 195 | "syntax": "javascript", 196 | "trigger": "stimulus dispatch" 197 | } 198 | ] 199 | } 200 | ] 201 | } 202 | -------------------------------------------------------------------------------- /Images/RailsLogo/RailsLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/Images/RailsLogo/RailsLogo.png -------------------------------------------------------------------------------- /Images/RailsLogo/RailsLogo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/Images/RailsLogo/RailsLogo@2x.png -------------------------------------------------------------------------------- /Images/RailsLogo/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": true 3 | } 4 | -------------------------------------------------------------------------------- /Images/RailsSidebarLarge/RailsSidebarLarge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/Images/RailsSidebarLarge/RailsSidebarLarge.png -------------------------------------------------------------------------------- /Images/RailsSidebarLarge/RailsSidebarLarge@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/Images/RailsSidebarLarge/RailsSidebarLarge@2x.png -------------------------------------------------------------------------------- /Images/RailsSidebarLarge/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": true 3 | } 4 | -------------------------------------------------------------------------------- /Images/RailsSidebarLargeSelected/RailsSidebarLargeSelected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/Images/RailsSidebarLargeSelected/RailsSidebarLargeSelected.png -------------------------------------------------------------------------------- /Images/RailsSidebarLargeSelected/RailsSidebarLargeSelected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/Images/RailsSidebarLargeSelected/RailsSidebarLargeSelected@2x.png -------------------------------------------------------------------------------- /Images/RailsSidebarLargeSelected/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": true 3 | } 4 | -------------------------------------------------------------------------------- /Images/RailsSidebarSmall/RailsSidebarSmall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/Images/RailsSidebarSmall/RailsSidebarSmall.png -------------------------------------------------------------------------------- /Images/RailsSidebarSmall/RailsSidebarSmall@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/Images/RailsSidebarSmall/RailsSidebarSmall@2x.png -------------------------------------------------------------------------------- /Images/RailsSidebarSmall/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": true 3 | } 4 | -------------------------------------------------------------------------------- /Images/RailsSidebarSmallSelected/RailsSidebarSmallSelected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/Images/RailsSidebarSmallSelected/RailsSidebarSmallSelected.png -------------------------------------------------------------------------------- /Images/RailsSidebarSmallSelected/RailsSidebarSmallSelected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/Images/RailsSidebarSmallSelected/RailsSidebarSmallSelected@2x.png -------------------------------------------------------------------------------- /Images/RailsSidebarSmallSelected/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": true 3 | } 4 | -------------------------------------------------------------------------------- /Images/task-bridgetown/task-bridgetown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/Images/task-bridgetown/task-bridgetown.png -------------------------------------------------------------------------------- /Images/task-bridgetown/task-bridgetown@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/Images/task-bridgetown/task-bridgetown@2x.png -------------------------------------------------------------------------------- /Images/task-rails/task-rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/Images/task-rails/task-rails.png -------------------------------------------------------------------------------- /Images/task-rails/task-rails@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/Images/task-rails/task-rails@2x.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tommaso Negri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

Ruby on Rails for Nova

5 | 6 | Provides Ruby on Rails support for Panic's Nova text editor. 7 | 8 | > NOTICE: From `v7.0` the extension has joined the [**Nova Ruby**](https://github.com/nova-ruby) project (see below). 9 | > That means it does not come with Solargraph and Rubocop support anymore. 10 | > Check the new and improved [Solargraph](nova://extension/?id=com.tommasonegri.solargraph&name=Solargraph) extension for that. 11 | > 12 | > From now on various parts of this project, not directly related with Rails, will be extracted into dedicated extensions. 13 | 14 | ## Features 15 | 16 | - erb tag switching 17 | - quick list and jump to migrations 18 | - jump to latest migration 19 | - migrate and rollback database 20 | - open alternate files 21 | - go to alternate files 22 | - preview mailers 23 | - search on various documentations 24 | - Rails task templates 25 | - kill puma server (useful after crash) 26 | - update Stimulus manifest 27 | - pin and unpin from importmap 28 | - quick jump to Rails routes and properties pages 29 | - sidebar with Rails notes and info 30 | - clips for Ruby, Rails and erb 31 | 32 | Check out the [Wiki](https://github.com/nova-ruby/rails/wiki) for a complete reference and user guide. 33 | 34 | ## The Nova Ruby project 35 | 36 | The extension is part of the [**Nova Ruby**](https://github.com/nova-ruby) project. 37 | A set of extensions specifically designed to work well together, with the goal of improving the Ruby experience in Nova editor. 38 | 39 | Project extensions: 40 | 41 | - [Ruby on Rails](nova://extension/?id=com.tommasonegri.Rails&name=Ruby%20on%20Rails) (this very extension) 42 | - [Solargraph](nova://extension/?id=com.tommasonegri.solargraph&name=Solargraph) 43 | - [Ruby Debug](nova://extension/?id=com.tommasonegri.rdbg&name=Ruby%20Debug) 44 | - [HTML Beautifier](nova://extension/?id=com.tommasonegri.htmlbeautifier&name=HTML%20Beautifier) 45 | - [Bridgetown](nova://extension/?id=com.tommasonegri.bridgetown&name=Bridgetown) 46 | 47 | ## Special Thanks 48 | 49 | Thanks to @devjah, @jonathanpike and @Wylan for their work on different extensions which have been integrated in this suite. 50 | -------------------------------------------------------------------------------- /Scripts/commands.js: -------------------------------------------------------------------------------- 1 | const { erbTagSwitcher } = require("./commands/erbTagSwitcher") 2 | const { RailsAlternateFile } = require("./commands/RailsAlternateFile") 3 | const { RailsDocumentation } = require("./commands/RailsDocumentation") 4 | const { RailsImportmap } = require("./commands/RailsImportmap") 5 | const { RailsInfo } = require("./commands/RailsInfo") 6 | const { RailsMigrations } = require("./commands/RailsMigrations") 7 | const { RailsServer } = require("./commands/RailsServer") 8 | const { RailsStimulus } = require("./commands/RailsStimulus") 9 | const RailsRelatedFiles = require("./commands/RailsRelatedFiles") 10 | const MailerPreview = require("./commands/MailerPreview") 11 | 12 | module.exports = { 13 | erbTagSwitcher, 14 | RailsAlternateFile, 15 | RailsDocumentation, 16 | RailsImportmap, 17 | RailsInfo, 18 | RailsMigrations, 19 | RailsServer, 20 | RailsStimulus, 21 | RailsRelatedFiles, 22 | MailerPreview 23 | } 24 | -------------------------------------------------------------------------------- /Scripts/commands/MailerPreview.js: -------------------------------------------------------------------------------- 1 | class MailerPreview { 2 | preview(path) { 3 | const match = path.match(/.+\/views\/([^\.]*).+/) 4 | 5 | nova.openURL(`http://localhost:3000/rails/mailers/${match[1]}`) 6 | } 7 | } 8 | 9 | module.exports = MailerPreview 10 | -------------------------------------------------------------------------------- /Scripts/commands/RailsAlternateFile.js: -------------------------------------------------------------------------------- 1 | exports.RailsAlternateFile = class RailsAlternateFile { 2 | constructor() { 3 | // FIXME: Handle running the command while something else than a file is active (like a terminal) 4 | this.currentPath = nova.workspace.activeTextEditor.document.path 5 | this.splitPath = nova.path.split(this.currentPath) 6 | } 7 | 8 | // TODO: Refactor and improve code quality 9 | alternate() { 10 | // If neither app nor test are found in the path, can't find associated file 11 | if (this.splitPath.indexOf("app") === -1 && this.splitPath.indexOf("test") === -1) { 12 | this.showError("Couldn't find alternate files. Does the workspace contain a Rails project?") 13 | return 14 | } 15 | 16 | const rootIndex = this.splitPath.indexOf("app") === -1 ? this.splitPath.indexOf("test") : this.splitPath.indexOf("app") 17 | const rootDir = this.splitPath[rootIndex] 18 | const associatedDir = rootDir === "app" ? "test" : "app" 19 | 20 | const currentFileName = this.splitPath[this.splitPath.length - 1] 21 | let associatedFileName 22 | 23 | if (associatedDir === "test") { 24 | associatedFileName = currentFileName.slice(0, -3).concat("_test.rb") 25 | } else { 26 | associatedFileName = currentFileName.replace("_test", "") 27 | } 28 | 29 | let newSplitPath = this.splitPath 30 | newSplitPath[rootIndex] = associatedDir 31 | newSplitPath[newSplitPath.length - 1] = associatedFileName 32 | const newPath = newSplitPath.slice(1).join("/") 33 | 34 | nova.workspace.openFile(newPath) 35 | } 36 | 37 | showError(msg) { 38 | nova.workspace.showErrorMessage(msg) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Scripts/commands/RailsDocumentation.js: -------------------------------------------------------------------------------- 1 | exports.RailsDocumentation = class RailsDocumentation { 2 | constructor() { 3 | this.availableDocs = { 4 | railsAPI: "Rails API", 5 | railsGuides: "Rails Guides", 6 | turbo: "Turbo", 7 | stimulus: "Stimulus", 8 | rubyDoc: "Ruby Doc", 9 | } 10 | 11 | this.docsSearchLinks = { 12 | railsAPI: "https://duckduckgo.com/?t=ffab&q=site%3Aapi.rubyonrails.org+", 13 | railsGuides: "https://duckduckgo.com/?t=ffab&q=site%3Aguides.rubyonrails.org+", 14 | turbo: "https://duckduckgo.com/?t=ffab&q=site%3Aturbo.hotwired.dev+", 15 | stimulus: "https://duckduckgo.com/?t=ffab&q=site%3Astimulus.hotwired.dev+", 16 | rubyDoc: "https://duckduckgo.com/?t=ffab&q=site%3Aruby-doc.org+", 17 | } 18 | } 19 | 20 | /** 21 | * @param {string} link Documentation Page Link 22 | */ 23 | openDocs(link) { 24 | nova.openURL(link) 25 | } 26 | 27 | searchDocs() { 28 | const documentations = Object.values(this.availableDocs) 29 | 30 | nova.workspace.showChoicePalette( 31 | documentations, 32 | { 33 | placeholder: "Choose Documentation", 34 | }, 35 | this.askWhatToSearch.bind(this) 36 | ) 37 | } 38 | 39 | /** 40 | * @param {string} documentation Documentation Page Link 41 | */ 42 | askWhatToSearch(documentation) { 43 | if (!documentation) return 44 | 45 | const docs = this.availableDocs 46 | const docsLinks = this.docsSearchLinks 47 | const message = "Search in " + documentation 48 | let docLink = null 49 | 50 | switch (documentation) { 51 | case docs.railsAPI: 52 | docLink = docsLinks.railsAPI 53 | break 54 | case docs.railsGuides: 55 | docLink = docsLinks.railsGuides 56 | break 57 | case docs.turbo: 58 | docLink = docsLinks.turbo 59 | break 60 | case docs.stimulus: 61 | docLink = docsLinks.stimulus 62 | break 63 | case docs.rubyDoc: 64 | docLink = docsLinks.rubyDoc 65 | break 66 | } 67 | 68 | nova.workspace.showInputPalette( 69 | message, 70 | { 71 | placeholder: message, 72 | }, 73 | function (query) { 74 | this.performDocumentationSearch(docLink, query) 75 | }.bind(this) 76 | ) 77 | } 78 | 79 | /** 80 | * @param {string} searchURL Documentation Search URL 81 | * @param {string} query Search terms 82 | */ 83 | performDocumentationSearch(searchURL, query) { 84 | if (!query) { 85 | return 86 | } 87 | 88 | nova.openURL(searchURL + encodeURIComponent(query)) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Scripts/commands/RailsImportmap.js: -------------------------------------------------------------------------------- 1 | const { showNotification } = require("../helpers") 2 | 3 | exports.RailsImportmap = class RailsImportmap { 4 | constructor() {} 5 | 6 | pin() { 7 | const message = "Pin one or more packages to Rails importmap. Separate packages with a blank space." 8 | 9 | nova.workspace.showInputPanel(message, { 10 | label: "Packages", 11 | placeholder: "lodash local-time react@17.0.1", 12 | prompt: "Pin" 13 | }, packages => this.pin_or_unpin(packages, { method: "pin" })) 14 | } 15 | 16 | unpin() { 17 | const message = "Unpin one or more packages from Rails importmap. Separate packages with a blank space." 18 | 19 | nova.workspace.showInputPanel(message, { 20 | label: "Packages", 21 | placeholder: "lodash local-time react@17.0.1", 22 | prompt: "Unpin" 23 | }, packages => this.pin_or_unpin(packages, { method: "unpin" })) 24 | } 25 | 26 | // Private 27 | 28 | pin_or_unpin(packages, options) { 29 | const process = new Process("usr/bin/env", { 30 | cwd: nova.workspace.path, 31 | args: ["bin/importmap", options.method, ...packages.split(" ")], 32 | stdio: ["ignore", "pipe", "pipe"], 33 | }) 34 | 35 | let str = "" 36 | let err = "" 37 | 38 | process.onStdout(output => str += output.trim()) 39 | process.onStderr(error_output => err += error_output) 40 | 41 | process.onDidExit((status) => { 42 | if (status === 0) { 43 | if (str.includes("Couldn't find any packages")) { 44 | console.warn("Couldn't find packages:", packages) 45 | 46 | nova.workspace.showWarningMessage(`Couldn"t find the specified packages. Check out for typos or other reference errors.`) 47 | } else { 48 | console.log(`Successfully ${options.method}ned packages:`, packages) 49 | 50 | showNotification( 51 | `rails-importmap-${options.method}`, 52 | `Successfully ${options.method}ned packages`, 53 | false, 54 | `Packages have been ${options.method}ned correctly.` 55 | ) 56 | } 57 | } else { 58 | console.error(`Importmap ${options.method} command exited with error:`, err) 59 | 60 | nova.workspace.showErrorMessage(`Something went wrong with the importmap ${options.method} command. Check out the Extension Console for more information.`) 61 | } 62 | }) 63 | 64 | process.start() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Scripts/commands/RailsInfo.js: -------------------------------------------------------------------------------- 1 | exports.RailsInfo = class RailsInfo { 2 | // TODO: Verify if is it possible to get the host from the active process 3 | showRoutes() { 4 | nova.openURL("http://localhost:3000/rails/info/routes") 5 | } 6 | 7 | showProperties() { 8 | nova.openURL("http://localhost:3000/rails/info/properties") 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Scripts/commands/RailsMigrations.js: -------------------------------------------------------------------------------- 1 | const { showNotification } = require("../helpers") 2 | 3 | exports.RailsMigrations = class RailsMigrations { 4 | openLatestMigration() { 5 | this.maybeShowMigrations((migrations) => { 6 | const migration = migrations[0] 7 | this.openMigration(migration) 8 | }) 9 | } 10 | 11 | listMigrations() { 12 | this.maybeShowMigrations((migrations) => { 13 | nova.workspace.showChoicePalette( 14 | migrations, 15 | { placeholder: "Choose Migration" }, 16 | this.openMigration.bind(this) 17 | ) 18 | }) 19 | } 20 | 21 | maybeShowMigrations(callbackAction) { 22 | if (this.migrationsFolderExists) { 23 | const migrations = this.migrationsList 24 | if (migrations && migrations.length > 0) { 25 | callbackAction(migrations) 26 | } else { 27 | this.showError("No files found in the migrations folder.") 28 | } 29 | } else { 30 | this.showError( 31 | "Couldn't find the migrations folder. Does the workspace contain a Rails project?" 32 | ) 33 | } 34 | } 35 | 36 | openMigration(name) { 37 | nova.workspace.openFile(nova.path.join(this.migrationsPath, name)) 38 | } 39 | 40 | async migrate() { 41 | if (!nova.workspace.railsDetected) { 42 | console.error("Something went wrong with the migrate command") 43 | 44 | this.showError("Something went wrong with the migrate command. Does the workspace contain a Rails project?") 45 | 46 | return 47 | } 48 | 49 | const process = new Process("usr/bin/env", { 50 | cwd: nova.workspace.path, 51 | args: ["rails", "db:migrate"], 52 | stdio: ["ignore", "pipe", "ignore"], 53 | }) 54 | let str = "" 55 | 56 | process.onStdout((output) => { 57 | str += output.trim() 58 | }) 59 | 60 | process.onDidExit((status) => { 61 | if (status === 0 && str.length > 0) { 62 | console.log("Migration applied") 63 | 64 | showNotification( 65 | "rails-database-migrated", 66 | "Migration applied", 67 | false, 68 | "The latest migration has been applied correctly." 69 | ) 70 | } else if (status === 0) { 71 | console.log("Nothing to migrate") 72 | 73 | showNotification( 74 | "rails-database-no-migrated", 75 | "Nothing new to migrate", 76 | false, 77 | "All the migrations are already applied." 78 | ) 79 | } else { 80 | console.error("Migrate command", str) 81 | this.showError("Something went wrong with the migrate command. Check out the Extension Console for more information.") 82 | } 83 | }) 84 | 85 | process.start() 86 | } 87 | 88 | async rollback() { 89 | if (!nova.workspace.railsDetected) { 90 | console.error("Something went wrong with the rollback command") 91 | this.showError("Something went wrong with the rollback command. Does the workspace contain a Rails project?") 92 | 93 | return 94 | } 95 | 96 | const process = new Process("usr/bin/env", { 97 | cwd: nova.workspace.path, 98 | args: ["rails", "db:rollback"], 99 | stdio: ["ignore", "pipe", "ignore"], 100 | }) 101 | let str = "" 102 | 103 | process.onStdout((output) => { 104 | str += output.trim() 105 | }) 106 | 107 | process.onDidExit((status) => { 108 | console.log(status) 109 | console.log(str) 110 | 111 | if (status === 0 && str.length > 0) { 112 | console.log("Rollback applied") 113 | 114 | showNotification( 115 | "rails-database-rollbacked", 116 | "Rollback applied", 117 | false, 118 | "The rollback has been applied correctly." 119 | ) 120 | } else if (status === 0) { 121 | console.log("Nothing to rollback to") 122 | 123 | showNotification( 124 | "rails-database-no-rollbacked", 125 | "Nothing to rollback", 126 | false, 127 | "No migrations to rollback." 128 | ) 129 | } else { 130 | console.error("Rollback command:", str) 131 | 132 | this.showError("Something went wrong with the rollback command. Check out the Extension Console for more information.") 133 | } 134 | }) 135 | 136 | process.start() 137 | } 138 | 139 | showError(msg) { 140 | nova.workspace.showErrorMessage(msg) 141 | } 142 | 143 | get migrationsList() { 144 | return nova.fs 145 | .listdir(this.migrationsPath) 146 | .filter((fileName) => fileName.endsWith(".rb")) 147 | .sort() 148 | .reverse() 149 | } 150 | 151 | get migrationsFolderExists() { 152 | const migrationsFs = nova.fs.stat(this.migrationsPath) 153 | return migrationsFs && migrationsFs.isDirectory() 154 | } 155 | 156 | get migrationsPath() { 157 | return nova.path.join(nova.workspace.path, "db", "migrate") 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Scripts/commands/RailsRelatedFiles.js: -------------------------------------------------------------------------------- 1 | class RailsRelatedFiles { 2 | /** 3 | * Show a choice palette with the related file paths 4 | * @param {string} filePath 5 | */ 6 | run(filePath) { 7 | const related = new Related(filePath, this._patterns) 8 | 9 | nova.workspace.showChoicePalette(related.files, { 10 | placeholder: "Go to related file..." 11 | }, (choice) => { 12 | if (choice) { 13 | nova.workspace.openFile(nova.path.join(nova.workspace.path, choice)) 14 | } 15 | }) 16 | } 17 | 18 | get _patterns() { 19 | return PATTERNS 20 | } 21 | } 22 | 23 | class Related { 24 | constructor(filePath, patterns) { 25 | this._filePath = filePath 26 | this._patterns = patterns 27 | 28 | this.files = [] 29 | 30 | this._build() 31 | } 32 | 33 | _build() { 34 | this._patterns.forEach(({ regex, paths }) => { 35 | const match = this._filePath.match(regex) 36 | if (match) { 37 | this.files = [...this.files, ...this._filesForPath(match, paths)] 38 | } 39 | }) 40 | } 41 | 42 | /** 43 | * Retrieves a list of files for the given match and paths 44 | * @param {string[]} match 45 | * @param {string[]} paths 46 | * @returns {string[]} 47 | */ 48 | _filesForPath(match, paths) { 49 | return paths 50 | .map(path => this._replacePath(match, path)) 51 | .flatMap(path => this._expandGlob(path)) 52 | .filter(path => nova.workspace.contains(nova.path.join(nova.workspace.path, path))) 53 | .filter(path => nova.path.join(nova.workspace.path, path) != this._filePath) 54 | } 55 | 56 | /** 57 | * Retrieves a path with its interpolation vars replaces by the found groups on match 58 | * @param {string[]} match 59 | * @param {string} path 60 | * @returns {string} 61 | */ 62 | _replacePath(match, path) { 63 | let replacedPath = path 64 | 65 | match.forEach((value, index) => { 66 | replacedPath = replacedPath.replace(`$${index}`, value) 67 | }) 68 | 69 | return replacedPath 70 | } 71 | 72 | /** 73 | * Retrieves a path or collection of expanded paths from glob 74 | * @param {string} path 75 | * @returns {string|string[]} 76 | */ 77 | _expandGlob(path) { 78 | if (!path.includes("**")) return path 79 | 80 | path = path.replace("**", "") 81 | const basePath = nova.path.join(nova.workspace.path, path) 82 | 83 | if (!nova.workspace.contains(basePath)) return "NOT_A_FILE" 84 | 85 | return nova.fs.listdir(basePath) 86 | .filter(item => nova.fs.stat(nova.path.join(basePath, item)).isFile()) 87 | .map(item => nova.path.join(path, item)) 88 | } 89 | } 90 | 91 | const PATTERNS = [ 92 | { 93 | "regex": ".+\/(app|lib)\/(.+).rb", 94 | "paths": [ 95 | "spec/$2_spec.rb", 96 | "test/$2_test.rb", 97 | "config/locales/$2/**" 98 | ] 99 | }, 100 | { 101 | "regex": ".+\/(test|spec)\/(.+)_(test|spec).rb", 102 | "paths": [ 103 | "app/$2.rb", 104 | "lib/$2.rb" 105 | ] 106 | }, 107 | { 108 | "regex": ".+\/app\/controllers\/(.+)_controller.rb", 109 | "paths": [ 110 | "app/views/$1/**", 111 | "app/helpers/$1_helper.rb", 112 | "config/routes.rb", 113 | "spec/requests/$1_spec.rb", 114 | "spec/routing/$1_routing_spec.rb", 115 | "config/locales/views/$1/**" 116 | ] 117 | }, 118 | { 119 | "regex": ".+\/app\/helpers\/(.+)_helper.rb", 120 | "paths": [ 121 | "app/views/$1/**", 122 | "app/controllers/$1_controller.rb", 123 | "config/routes.rb", 124 | "spec/requests/$1_spec.rb", 125 | "config/locales/helpers/$1/**" 126 | ] 127 | }, 128 | { 129 | "regex": ".+\/app\/views\/(.+)\/[^\/].+", 130 | "paths": [ 131 | "app/views/$1/**", 132 | "app/controllers/$1_controller.rb", 133 | "app/mailers/$1.rb", 134 | "app/helpers/$1_helper.rb", 135 | "config/routes.rb", 136 | "spec/controllers/$1_spec.rb", 137 | "spec/requests/$1_spec.rb", 138 | "config/locales/views/$1/**" 139 | ] 140 | }, 141 | { 142 | "regex": ".+\/app/mailers\/(.+)_mailer.rb", 143 | "paths": [ 144 | "app/views/$1_mailer/**", 145 | "test/mailers/previews/$1_mailer_preview.rb", 146 | "spec/mailers/$1_spec.rb", 147 | "config/locales/mailers/$1/**" 148 | ] 149 | }, 150 | { 151 | "regex": ".+\/config\/locales\/helpers\/(.*)\/[^\/].+", 152 | "paths": [ 153 | "app/helpers/$1_helper.rb", 154 | "config/locales/helpers/$1/**" 155 | ] 156 | }, 157 | { 158 | "regex": ".+\/config\/locales\/models\/(.*)\/[^\/].+", 159 | "paths": [ 160 | "app/models/$1.rb", 161 | "config/locales/models/$1/**" 162 | ] 163 | }, 164 | { 165 | "regex": ".+\/config\/locales\/views\/(.*)\/[^\/].+", 166 | "paths": [ 167 | "app/views/$1/**", 168 | "app/controllers/$1_controller.rb", 169 | "config/locales/views/$1/**" 170 | ] 171 | }, 172 | { 173 | "regex": ".+\/config\/locales\/mailers\/(.*)\/[^\/].+", 174 | "paths": [ 175 | "app/mailers/$1.rb", 176 | "config/locales/mailers/$1/**" 177 | ] 178 | }, 179 | { 180 | "regex": ".+\/config\/routes.rb", 181 | "paths": [ 182 | "spec/routing/**" 183 | ] 184 | }, 185 | { 186 | "regex": ".+\/(lib)\/(.+).rb", 187 | "paths": [ 188 | "spec/lib/$2_spec.rb", 189 | "test/lib/$2_test.rb" 190 | ] 191 | }, 192 | { 193 | "regex": ".+/spec/controllers/(.+)_controller_spec.rb", 194 | "paths": [ 195 | "app/controllers/$1_controller.rb", 196 | "app/helpers/$1_helper.rb", 197 | "app/views/$1/**", 198 | "config/routes.rb" 199 | ] 200 | }, 201 | { 202 | "regex": ".+/spec/requests/(.+)_spec.rb", 203 | "paths": [ 204 | "app/controllers/$1_controller.rb", 205 | "app/helpers/$1_helper.rb", 206 | "app/views/$1/**", 207 | "config/routes.rb" 208 | ] 209 | }, 210 | { 211 | "regex": ".+/spec/lib/(.+)_spec.rb", 212 | "paths": [ 213 | "lib/$1.rb" 214 | ] 215 | } 216 | ] 217 | 218 | module.exports = RailsRelatedFiles 219 | -------------------------------------------------------------------------------- /Scripts/commands/RailsServer.js: -------------------------------------------------------------------------------- 1 | exports.RailsServer = class RailsServer { 2 | kill() { 3 | const process = new Process("usr/bin/env", { 4 | args: ["pkill", "-9", "-f", "rb-fsevent|rails|spring|puma"], 5 | stdio: ["ignore", "ignore", "ignore"], 6 | }) 7 | 8 | process.start() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Scripts/commands/RailsStimulus.js: -------------------------------------------------------------------------------- 1 | const { showNotification } = require("../helpers") 2 | 3 | exports.RailsStimulus = class RailsStimulus { 4 | updateManifest() { 5 | // TODO: Handle case when importmaps or other system are used 6 | if (!nova.workspace.railsDetected) { 7 | console.error("Something went wrong with the update Stimulus manifest command") 8 | 9 | this.showError("Something went wrong with the update Stimulus manifest command. Does the workspace contain a Rails project?") 10 | 11 | return 12 | } 13 | 14 | const process = new Process("usr/bin/env", { 15 | cwd: nova.workspace.path, 16 | args: ["rails", "stimulus:manifest:update"], 17 | stdio: ["ignore", "pipe", "ignore"], 18 | }) 19 | let str = "" 20 | 21 | process.onStdout((output) => { 22 | str += output.trim() 23 | }) 24 | 25 | process.onDidExit((status) => { 26 | if (status === 0) { 27 | console.log("Stimulus manifest updated") 28 | 29 | showNotification( 30 | "rails-stimulus-manifest-updated", 31 | "Stimulus manifest updated", 32 | false, 33 | "The Stimulus manifest file has been updated correctly." 34 | ) 35 | } else { 36 | console.error("Update Stimulus manifest command", str) 37 | 38 | this.showError("Something went wrong with the update Stimulus manifest command. Check out the Extension Console for more information.") 39 | } 40 | }) 41 | 42 | process.start() 43 | } 44 | 45 | showError(msg) { 46 | nova.workspace.showErrorMessage(msg) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Scripts/commands/erbTagSwitcher.js: -------------------------------------------------------------------------------- 1 | exports.erbTagSwitcher = async function(editor) { 2 | if (nova.inDevMode()) { 3 | console.log("———— ERB TAG SWITCHER ————") 4 | } 5 | 6 | let selectedRanges = editor.selectedRanges 7 | const originalSelection = selectedRanges 8 | 9 | const brackets = [ 10 | ["<%= ", " %>"], 11 | ["<% ", " %>"], 12 | ["<%# ", " %>"], 13 | ] 14 | 15 | var last_tag_repacement 16 | var bracketsToUseIndex = 0 17 | var bracketsToUse = [] 18 | var hasSelection = [] 19 | var entireLineRanges = [] 20 | var linePositions = [] 21 | 22 | // Selects the whole line for all "empty" ranges (ranges without selection, only a cursor position) 23 | // selectedRanges = selectedRanges.map((r) => 24 | // // r.empty ? editor.getLineRangeForRange(r) : r 25 | // ) 26 | 27 | selectedRanges = selectedRanges.map(function (r, index) { 28 | if (r.empty) { 29 | hasSelection[index] = false 30 | entireLineRanges[index] = editor.getLineRangeForRange(r) 31 | if (entireLineRanges) { 32 | selectedRanges.map(function (r, index) { 33 | linePositions[index] = r.start - entireLineRanges[index].start 34 | }) 35 | } 36 | } else { 37 | hasSelection[index] = true 38 | } 39 | return r 40 | }) 41 | 42 | if (nova.inDevMode()) { 43 | console.log("(ERB TAG) hasSelection:", hasSelection) 44 | console.log("(ERB TAG) selectedRanges:", selectedRanges) 45 | console.log("(ERB TAG) entireLineRanges:", entireLineRanges) 46 | console.log("(ERB TAG) linePositions:", linePositions) 47 | } 48 | 49 | const lengths = selectedRanges.map((r) => r.length) 50 | let newSelection = [] 51 | await editor.edit(function (e) { 52 | for (const [index, range] of selectedRanges.entries()) { 53 | var existingBrackets = [] 54 | var cursorMove = 0 55 | // const beforeCursorRange = new Range(entireLineRanges[index].start, entireLineRanges[index].start + linePositions[index]) 56 | const beforeCursorRange = new Range(0, selectedRanges[index].start) 57 | const textBeforeCursor = editor.getTextInRange(beforeCursorRange) 58 | const text = editor.getTextInRange(range) 59 | 60 | if (nova.inDevMode()) { 61 | console.log("(ERB TAG) beforeCursorRange:", beforeCursorRange) 62 | console.log("(ERB TAG) selected text:", text) 63 | console.log("(ERB TAG) selected text length:", text.length) 64 | } 65 | 66 | const openingTagsToTheLeft = textBeforeCursor.match(/<%.?/g) || [] 67 | const openingTagsToTheLeftCount = openingTagsToTheLeft.length 68 | const lastOpeningTagBeforeCursor = openingTagsToTheLeft.pop() 69 | const closingTagsToTheLeftCount = (textBeforeCursor.match(/%>/g) || []).length 70 | 71 | if (nova.inDevMode()) { 72 | console.log("(ERB TAG) <% occurence", openingTagsToTheLeftCount) 73 | console.log("(ERB TAG) %> occurence", closingTagsToTheLeftCount) 74 | } 75 | 76 | if (hasSelection[index] == false) { 77 | if (nova.inDevMode()) { 78 | console.log("(ERB TAG) no selection") 79 | } 80 | 81 | if (openingTagsToTheLeftCount != closingTagsToTheLeftCount) { 82 | if (nova.inDevMode()) { 83 | console.log("(ERB TAG) it's inside another tag") 84 | console.log("(ERB TAG) lastOpeningTagBeforeCursor:", lastOpeningTagBeforeCursor) 85 | } 86 | 87 | for (var i = 0; i < brackets.length; i++) { 88 | if ((lastOpeningTagBeforeCursor + " ").includes(brackets[i][0])) { 89 | existingBrackets = [brackets[i][0], brackets[i][1]] 90 | bracketsToUseIndex = i + 1 91 | if (bracketsToUseIndex + 1 > brackets.length) { 92 | bracketsToUseIndex = 0 93 | } 94 | cursorMove = brackets[bracketsToUseIndex][0].length - existingBrackets[0].length 95 | 96 | if (nova.inDevMode()) { 97 | console.log("(ERB TAG) changing bracket:", bracketsToUseIndex) 98 | } 99 | } 100 | } 101 | } else { 102 | if (nova.inDevMode()) { 103 | console.log("(ERB TAG) not inside a tag") 104 | } 105 | } 106 | // e.replace(originalRange, bracketsToUse[0] + text + bracketsToUse[1]) 107 | // cursorMove = bracketsToUse[0].length + text.length 108 | } else { 109 | if (nova.inDevMode()) { 110 | console.log("(ERB TAG) has selection") 111 | } 112 | } 113 | 114 | if (nova.inDevMode()) { 115 | console.log("(ERB TAG) bracketsToUseIndex:", bracketsToUseIndex) 116 | } 117 | 118 | bracketsToUse = brackets[bracketsToUseIndex] 119 | const originalRange = originalSelection[index] 120 | 121 | if (existingBrackets.length > 0) { 122 | // TODO: has to know what "var text" to replace 123 | // var new_text = text.replace(existingBrackets[0], bracketsToUse[0]) 124 | 125 | var fromLastTagToCursor = (textBeforeCursor.match(/<%([^<%]*)$/) || [])[0] 126 | 127 | if (nova.inDevMode()) { 128 | console.log("(ERB TAG) editor range:", range) 129 | console.log("(ERB TAG) textBeforeCursor:", textBeforeCursor) 130 | console.log("(ERB TAG) fromLastTagToCursor:", fromLastTagToCursor) 131 | } 132 | 133 | if (fromLastTagToCursor == undefined) { 134 | if (nova.inDevMode()) { 135 | console.error("(ERB TAG) ERB tags error: FROM LAST TAG TO CURSOR") 136 | } 137 | return 138 | } 139 | last_tag_repacement = fromLastTagToCursor.replace( 140 | existingBrackets[0].replace(" ", ""), 141 | bracketsToUse[0].replace(" ", "") 142 | ) 143 | var new_text = textBeforeCursor.replace(/<%([^<%]*)$/, last_tag_repacement) 144 | 145 | if (nova.inDevMode()) { 146 | console.log("(ERB TAG) new text:", new_text) 147 | console.log("(ERB TAG) new text length:", new_text.length) 148 | console.log("(ERB TAG) textBeforeCursor length:", textBeforeCursor.length) 149 | } 150 | 151 | e.replace(beforeCursorRange, new_text) 152 | } else { 153 | if (nova.inDevMode()) { 154 | console.log("(ERB TAG) original range length:", originalRange.length) 155 | } 156 | 157 | if (originalRange.length > 0) { 158 | e.replace(originalRange, bracketsToUse[0] + text + bracketsToUse[1]) 159 | cursorMove = bracketsToUse[0].length + text.length 160 | } else { 161 | e.replace(originalRange, bracketsToUse[0] + "" + bracketsToUse[1]) 162 | cursorMove = bracketsToUse[0].length 163 | } 164 | } 165 | 166 | newSelection.push(new Range(originalRange.start + cursorMove, originalRange.start + cursorMove)) 167 | } 168 | }) 169 | 170 | editor.selectedRanges = newSelection 171 | } 172 | -------------------------------------------------------------------------------- /Scripts/helpers.js: -------------------------------------------------------------------------------- 1 | const SETTINGS = require("./settings") 2 | 3 | /** 4 | * @param {string} id Notification ID 5 | * @param {string} title Notification Title 6 | * @param {boolean} showAlways Whether to override the user notifications settings 7 | * @param {string=} body Notification Body 8 | * @param {[string]=} actions Notification Action 9 | * @param {function(any)=} handler Notification Handler 10 | */ 11 | exports.showNotification = function(id, title, showAlways, body, actions, handler) { 12 | if (showAlways || SETTINGS.statusNotifications()) { 13 | let request = new NotificationRequest(id) 14 | 15 | request.title = title 16 | if (body) request.body = body 17 | if (actions) request.actions = actions 18 | 19 | nova.notifications 20 | .add(request) 21 | .then((reply) => { 22 | if (handler) { 23 | handler(reply) 24 | } 25 | }) 26 | .catch((err) => console.error(err, err.stack)) 27 | } 28 | } 29 | 30 | exports.isRailsInProject = function() { 31 | let gemfilePath 32 | 33 | if (nova.fs.access(nova.workspace.path + '/Gemfile', nova.fs.F_OK)) { 34 | gemfilePath = nova.workspace.path + '/Gemfile' 35 | } else if (nova.fs.access(nova.workspace.path + '/gems.rb', nova.fs.F_OK)) { 36 | gemfilePath = nova.workspace.path + '/gems.rb' 37 | } else { 38 | return false 39 | } 40 | 41 | const gemfile = nova.fs.open(gemfilePath).read() 42 | 43 | if (gemfile.indexOf("gem 'rails'") !== -1 || gemfile.indexOf('gem "rails"') !== -1) { 44 | return true 45 | } else { 46 | return false 47 | } 48 | } 49 | 50 | exports.aboutRails = async function() { 51 | if (!nova.workspace.railsDetected) return [] 52 | 53 | return new Promise((resolve, reject) => { 54 | const process = new Process('/usr/bin/env', { 55 | cwd: nova.workspace.path, 56 | args: ['rails', 'about'], 57 | stdio: ['ignore', 'pipe', 'pipe'], 58 | shell: true, 59 | }) 60 | let str = '' 61 | let err = '' 62 | let strings = [] 63 | 64 | process.onStdout((output) => { 65 | str += output 66 | }) 67 | 68 | process.onStderr((error_output) => { 69 | err += error_output 70 | }) 71 | 72 | process.onDidExit((status) => { 73 | if (status == 1){ return reject(err) } 74 | if (str.length == 0) { return reject("rails about is empty") } 75 | 76 | // Split each line of the output in the strings array 77 | strings = str.match(/[^\r\n]+/g) 78 | 79 | if (status === 0 && strings[0] == "About your application's environment") { 80 | resolve(strings) 81 | } else { 82 | reject(status) 83 | } 84 | }) 85 | 86 | process.start() 87 | }) 88 | } 89 | 90 | exports.railsNotes = async function() { 91 | if (!nova.workspace.railsDetected) return [] 92 | 93 | return new Promise((resolve, reject) => { 94 | const process = new Process('/usr/bin/env', { 95 | cwd: nova.workspace.path, 96 | args: ['rails', 'notes'], 97 | stdio: ['ignore', 'pipe', 'pipe'], 98 | shell: true, 99 | }) 100 | let str = '' 101 | let err = '' 102 | let strings = [] 103 | 104 | process.onStdout((output) => { 105 | str += output 106 | }) 107 | 108 | process.onStderr((error_output) => { 109 | err += error_output 110 | }) 111 | 112 | process.onDidExit((status) => { 113 | if (status == 1){ return reject(err) } 114 | if (str.length == 0) { return reject("rails notes is empty") } 115 | 116 | const notes = [] 117 | // Split each line of the output in the strings array 118 | strings = str.match(/[^\r\n]+/g) 119 | 120 | strings.forEach((str, index) => { 121 | const possibleFilePath = str.slice(0, -1) 122 | 123 | if (nova.fs.access(`${nova.workspace.path}/${possibleFilePath}`, nova.fs.F_OK)) { 124 | const notesGroup = { 125 | filename: possibleFilePath.split("/").pop(), 126 | path: possibleFilePath, 127 | notes: [] 128 | } 129 | notes.push(notesGroup) 130 | } else { 131 | const noteParts = /\s{2}\*\s\[\s*(\d+)\]\s\[(.*)\]\s?(.*)/.exec(str) 132 | 133 | const note = { 134 | line: noteParts[1], 135 | annotation: noteParts[2], 136 | comment: noteParts[3] 137 | } 138 | 139 | notes[notes.length - 1].notes.push(note) 140 | } 141 | }) 142 | 143 | if (status === 0) { 144 | resolve(notes) 145 | } else { 146 | reject(status) 147 | } 148 | }) 149 | 150 | process.start() 151 | }) 152 | } 153 | -------------------------------------------------------------------------------- /Scripts/main.js: -------------------------------------------------------------------------------- 1 | const COMMANDS = require("./commands") 2 | const SETTINGS = require("./settings") 3 | const { VersionChecker } = require("./other/VersionChecker") 4 | const { RailsSidebar } = require("./sidebars") 5 | const { isRailsInProject, showNotification } = require("./helpers") 6 | 7 | const versionChecker = new VersionChecker() 8 | versionChecker.check() 9 | 10 | let sidebar = null 11 | 12 | exports.activate = function() { 13 | if (nova.inDevMode()) console.log("Hello from Ruby on Rails 🚂 (DEV mode)") 14 | else console.log("Hello from Ruby on Rails 🚂") 15 | 16 | if (isRailsInProject()) { 17 | nova.workspace.railsDetected = true 18 | 19 | showNotification( 20 | "rails-detected", 21 | "Ruby on Rails Found in Project 🚂", 22 | false, 23 | "Specific features will be enabled..." 24 | ) 25 | } else { 26 | nova.workspace.railsDetected = false 27 | } 28 | 29 | sidebar = new RailsSidebar() 30 | } 31 | 32 | exports.deactivate = function() { 33 | // Clean up state before the extension is deactivated 34 | if (sidebar) { 35 | sidebar.deactivate() 36 | sidebar = null 37 | } 38 | 39 | console.log("Goodbye from Ruby on Rails 🚂") 40 | } 41 | 42 | function reload() { 43 | exports.deactivate() 44 | exports.activate() 45 | } 46 | 47 | // Register Nova commands 48 | 49 | nova.commands.register("tommasonegri.rails.commands.reload", reload) 50 | 51 | nova.commands.register("tommasonegri.rails.commands.erb.tagSwitcher", async (editor) => { 52 | COMMANDS.erbTagSwitcher(editor) 53 | }) 54 | 55 | nova.commands.register("tommasonegri.rails.commands.migrations.openLatest", () => { 56 | const railsMigrations = new COMMANDS.RailsMigrations() 57 | railsMigrations.openLatestMigration() 58 | }) 59 | 60 | nova.commands.register("tommasonegri.rails.commands.migrations.list", () => { 61 | const railsMigrations = new COMMANDS.RailsMigrations() 62 | railsMigrations.listMigrations() 63 | }) 64 | 65 | nova.commands.register("tommasonegri.rails.commands.openAlternateFile", () => { 66 | const railsAlternateFile = new COMMANDS.RailsAlternateFile() 67 | railsAlternateFile.alternate() 68 | }) 69 | 70 | // Register Nova commands for opening Documentations 71 | nova.commands.register("tommasonegri.rails.commands.documentation.openRailsGuides", () => { 72 | const railsDocumentation = new COMMANDS.RailsDocumentation() 73 | railsDocumentation.openDocs("https://guides.rubyonrails.org") 74 | }) 75 | nova.commands.register("tommasonegri.rails.commands.documentation.openRailsAPI", () => { 76 | const railsDocumentation = new COMMANDS.RailsDocumentation() 77 | railsDocumentation.openDocs("https://api.rubyonrails.org") 78 | }) 79 | nova.commands.register("tommasonegri.rails.commands.documentation.openRailsForum", () => { 80 | const railsDocumentation = new COMMANDS.RailsDocumentation() 81 | railsDocumentation.openDocs("https://discuss.rubyonrails.org") 82 | }) 83 | nova.commands.register("tommasonegri.rails.commands.documentation.openTurboReference", () => { 84 | const railsDocumentation = new COMMANDS.RailsDocumentation() 85 | railsDocumentation.openDocs("https://turbo.hotwired.dev/reference/drive") 86 | }) 87 | nova.commands.register("tommasonegri.rails.commands.documentation.openStimulusReference", () => { 88 | const railsDocumentation = new COMMANDS.RailsDocumentation() 89 | railsDocumentation.openDocs("https://stimulus.hotwired.dev/reference/controllers") 90 | }) 91 | nova.commands.register("tommasonegri.rails.commands.documentation.openExtensionWiki", () => { 92 | const railsDocumentation = new COMMANDS.RailsDocumentation() 93 | railsDocumentation.openDocs("https://github.com/nova-ruby/rails/wiki") 94 | }) 95 | 96 | // Register a Nova command for Searching the Documentation with the Command Palette 97 | nova.commands.register("tommasonegri.rails.commands.documentation.search", () => { 98 | const railsDocumentation = new COMMANDS.RailsDocumentation() 99 | railsDocumentation.searchDocs() 100 | }) 101 | 102 | // Register a Nova command for Killing Puma Server. 103 | // Useful for recovering from a not properly stopped server 104 | // for example after a Nova crash. 105 | nova.commands.register("tommasonegri.rails.commands.pumaServer.kill", () => { 106 | const railsServer = new COMMANDS.RailsServer() 107 | railsServer.kill() 108 | }) 109 | 110 | // Register a Nova command for Applying the latest Migration 111 | nova.commands.register("tommasonegri.rails.commands.migrations.migrate", () => { 112 | const railsMigrations = new COMMANDS.RailsMigrations() 113 | railsMigrations.migrate() 114 | }) 115 | 116 | // Register a Nova command for Applying a Rollback 117 | nova.commands.register("tommasonegri.rails.commands.migrations.rollback", () => { 118 | const railsMigrations = new COMMANDS.RailsMigrations() 119 | railsMigrations.rollback() 120 | }) 121 | 122 | // Register a Nova command for Updating Stimulus manifest 123 | nova.commands.register("tommasonegri.rails.commands.stimulus.manifest.update", () => { 124 | const railsStimulus = new COMMANDS.RailsStimulus() 125 | railsStimulus.updateManifest() 126 | }) 127 | 128 | // Register Nova commands for showing project infos 129 | nova.commands.register("tommasonegri.rails.commands.info.routes", () => { 130 | const railsInfo = new COMMANDS.RailsInfo() 131 | railsInfo.showRoutes() 132 | }) 133 | nova.commands.register("tommasonegri.rails.commands.info.properties", () => { 134 | const railsInfo = new COMMANDS.RailsInfo() 135 | railsInfo.showProperties() 136 | }) 137 | 138 | // Register Nova commands for pinning and unpinning packages from importmap 139 | nova.commands.register("tommasonegri.rails.commands.importmap.pin", () => { 140 | const railsImportmap = new COMMANDS.RailsImportmap() 141 | railsImportmap.pin() 142 | }) 143 | nova.commands.register("tommasonegri.rails.commands.importmap.unpin", () => { 144 | const railsImportmap = new COMMANDS.RailsImportmap() 145 | railsImportmap.unpin() 146 | }) 147 | 148 | // Register Nova command for showing related files 149 | nova.commands.register("tommasonegri.rails.commands.showRelatedFiles", (editor) => { 150 | const relatedFiles = new COMMANDS.RailsRelatedFiles() 151 | relatedFiles.run(editor.document.path) 152 | }) 153 | 154 | // Register Nova command for previewing mailers 155 | nova.commands.register("tommasonegri.rails.commands.previewMailer", (editor) => { 156 | const mailerPreview = new COMMANDS.MailerPreview() 157 | mailerPreview.preview(editor.document.path) 158 | }) 159 | -------------------------------------------------------------------------------- /Scripts/other/VersionChecker.js: -------------------------------------------------------------------------------- 1 | const { showNotification } = require("../helpers") 2 | 3 | exports.VersionChecker = class VersionChecker { 4 | constructor() { 5 | if (nova.inDevMode()) { 6 | console.log("————— VERSION CHECKER —————") 7 | console.log("(CHECKER) Extension Identifier:", nova.extension.identifier) 8 | console.log("(CHECKER) Extension Name:", nova.extension.name) 9 | console.log("(CHECKER) Current Version:", nova.extension.version) 10 | console.log("(CHECKER) Stored Version:", this.getLatestStoredVersion()) 11 | } 12 | } 13 | 14 | check() { 15 | switch (this.getLatestStoredVersion()) { 16 | case null: 17 | this.storeLatestVersion() 18 | break 19 | case nova.extension.version: 20 | break 21 | default: 22 | this.notifyVersionChanged() 23 | this.storeLatestVersion() 24 | } 25 | } 26 | 27 | getLatestStoredVersion() { 28 | const key = `${nova.extension.identifier}.extension.version` 29 | 30 | return nova.config.get(key, "string") 31 | } 32 | 33 | storeLatestVersion() { 34 | const key = `${nova.extension.identifier}.extension.version` 35 | 36 | nova.config.set(key, nova.extension.version) 37 | } 38 | 39 | notifyVersionChanged() { 40 | showNotification( 41 | `${nova.extension.identifier}-new-version`, 42 | `${nova.extension.name} Updated`, 43 | false, 44 | `${nova.extension.name} has been updated to v${nova.extension.version}`, 45 | ["Open Changelog", "Ignore"], 46 | (reply) => { 47 | switch (reply.actionIdx) { 48 | case 0: 49 | this.openChangelog() 50 | break 51 | case 1: 52 | break 53 | } 54 | } 55 | ) 56 | } 57 | 58 | openChangelog() { 59 | nova.extension.openChangelog() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Scripts/settings.js: -------------------------------------------------------------------------------- 1 | // EXTENSION 2 | const { statusNotifications } = require("./settings/general/statusNotifications") 3 | 4 | module.exports = { 5 | statusNotifications 6 | } 7 | -------------------------------------------------------------------------------- /Scripts/settings/general/statusNotifications.js: -------------------------------------------------------------------------------- 1 | function reload() { 2 | nova.commands.invoke("tommasonegri.rails.commands.reload") 3 | } 4 | 5 | nova.config.onDidChange("tommasonegri.rails.config.general.statusNotifications", reload) 6 | nova.workspace.config.onDidChange("tommasonegri.rails.config.general.statusNotifications", reload) 7 | 8 | function getExtensionSetting() { 9 | return nova.config.get("tommasonegri.rails.config.general.statusNotifications", "boolean") 10 | } 11 | 12 | function getWorkspaceSetting() { 13 | const str = nova.workspace.config.get("tommasonegri.rails.config.general.statusNotifications", "string") 14 | 15 | switch (str) { 16 | case "Global Default": 17 | return null 18 | case "Enabled": 19 | return true 20 | case "Disabled": 21 | return false 22 | default: 23 | return null 24 | } 25 | } 26 | 27 | exports.statusNotifications = function() { 28 | const workspaceConfig = getWorkspaceSetting() 29 | const extensionConfig = getExtensionSetting() 30 | 31 | return workspaceConfig === null ? extensionConfig : workspaceConfig 32 | } 33 | -------------------------------------------------------------------------------- /Scripts/sidebars.js: -------------------------------------------------------------------------------- 1 | const { NotesView } = require("./sidebars/NotesView") 2 | const { AboutView } = require("./sidebars/AboutView") 3 | 4 | exports.RailsSidebar = class RailsSidebar { 5 | constructor() { 6 | this.start() 7 | } 8 | 9 | deactivate() { 10 | this.stop() 11 | } 12 | 13 | start() { 14 | if (this.notesView) { 15 | this.notesView.deactivate() 16 | nova.subscriptions.remove(this.notesView) 17 | } 18 | if (this.aboutView) { 19 | this.aboutView.deactivate() 20 | nova.subscriptions.remove(this.aboutView) 21 | } 22 | 23 | this.notesView = new NotesView() 24 | nova.subscriptions.add(this.notesView) 25 | 26 | this.aboutView = new AboutView() 27 | nova.subscriptions.add(this.aboutView) 28 | } 29 | 30 | stop() { 31 | if (this.notesView) { 32 | this.notesView.deactivate() 33 | nova.subscriptions.remove(this.notesView) 34 | this.notesView = null 35 | } 36 | if (this.aboutView) { 37 | this.aboutView.deactivate() 38 | nova.subscriptions.remove(this.aboutView) 39 | this.aboutView = null 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Scripts/sidebars/AboutView.js: -------------------------------------------------------------------------------- 1 | const { aboutRails } = require("../helpers") 2 | 3 | exports.AboutView = class AboutView { 4 | constructor() { 5 | this.tree = new TreeView("tommasonegri.rails.sidebar.about", { 6 | dataProvider: this 7 | }) 8 | 9 | this._railsVersionElement = { 10 | title: "Rails Version", 11 | value: "...", 12 | identifier: "railsVersion", 13 | } 14 | this._rubyVersionElement = { 15 | title: "Ruby Version", 16 | value: "...", 17 | identifier: "rubyVersion", 18 | } 19 | this._rubyGemsVersionElement = { 20 | title: "RubyGems Version", 21 | value: "...", 22 | identifier: "rubyGemsVersion", 23 | } 24 | this._rackVersionElement = { 25 | title: "Rack Version", 26 | value: "...", 27 | identifier: "rackVersion", 28 | } 29 | this._applicationRootElement = { 30 | title: "Application Root", 31 | value: "...", 32 | identifier: "applicationRoot", 33 | } 34 | this._environmentElement = { 35 | title: "Environment", 36 | value: "...", 37 | identifier: "environment", 38 | } 39 | this._databaseAdapterElement = { 40 | title: "Database Adapter", 41 | value: "...", 42 | identifier: "databaseAdapter", 43 | } 44 | this._databaseSchemaVersionElement = { 45 | title: "Database Schema Version", 46 | value: "...", 47 | identifier: "databaseSchemaVersion", 48 | } 49 | 50 | this.fetchAbout() 51 | 52 | this.getChildren = this.getChildren.bind(this) 53 | this.getTreeItem = this.getTreeItem.bind(this) 54 | 55 | nova.commands.register("tommasonegri.rails.commands.sidebar.aboutRails.reload", () => { 56 | this.fetchAbout() 57 | }) 58 | } 59 | 60 | deactivate() { 61 | this.dispose() 62 | } 63 | 64 | reload() { 65 | this.tree.reload() 66 | } 67 | 68 | dispose() { 69 | this.tree.dispose() 70 | } 71 | 72 | // Private 73 | 74 | getChildren(element) { 75 | if (element == null && this.isRailsDetected) { 76 | return [ 77 | this._railsVersionElement, 78 | this._rubyVersionElement, 79 | this._rubyGemsVersionElement, 80 | this._rackVersionElement, 81 | this._applicationRootElement, 82 | this._environmentElement, 83 | this._databaseAdapterElement, 84 | this._databaseSchemaVersionElement, 85 | ] 86 | } 87 | return [] 88 | } 89 | 90 | getTreeItem(element) { 91 | const item = new TreeItem(element.title) 92 | 93 | item.collapsibleState = TreeItemCollapsibleState.None 94 | item.descriptiveText = element.value 95 | item.identifier = element.identifier 96 | item.tooltip = element.tooltip 97 | 98 | return item 99 | } 100 | 101 | async fetchAbout() { 102 | try { 103 | const about = await aboutRails() 104 | 105 | if (!nova.workspace.railsDetected) return 106 | 107 | this.isRailsDetected = true 108 | 109 | const railsVersion = about[1]?.split(" ")?.pop() 110 | const rubyVersion = about[2]?.slice(12)?.trim() 111 | const rubyGemsVersion = about[3]?.split(" ")?.pop() 112 | const rackVersion = about[4]?.split(" ")?.pop() 113 | const applicationRoot = about[6]?.split(" ")?.pop() 114 | const environment = about[7]?.split(" ")?.pop() 115 | const databaseAdapter = about[8]?.split(" ")?.pop() 116 | const databaseSchemaVersion = about[9]?.split(" ")?.pop() 117 | 118 | this._railsVersionElement.value = railsVersion 119 | this._rubyVersionElement.value = rubyVersion 120 | this._rubyGemsVersionElement.value = rubyGemsVersion 121 | this._rackVersionElement.value = rackVersion 122 | this._applicationRootElement.value = applicationRoot 123 | this._environmentElement.value = environment 124 | this._databaseAdapterElement.value = databaseAdapter 125 | this._databaseSchemaVersionElement.value = databaseSchemaVersion 126 | 127 | this.reload() 128 | } catch (err) { 129 | console.error("Something went wrong trying to fetch Rails About:", err) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Scripts/sidebars/NotesView.js: -------------------------------------------------------------------------------- 1 | const { railsNotes } = require("../helpers") 2 | 3 | exports.NotesView = class NotesView { 4 | constructor() { 5 | this.tree = new TreeView("tommasonegri.rails.sidebar.notes", { 6 | dataProvider: this 7 | }) 8 | this.rootItems = [] 9 | 10 | this.fetchNotes() 11 | 12 | this.getChildren = this.getChildren.bind(this) 13 | this.getTreeItem = this.getTreeItem.bind(this) 14 | 15 | this.registerCommands() 16 | } 17 | 18 | deactivate() { 19 | this.dispose() 20 | } 21 | 22 | reload() { 23 | this.tree.reload() 24 | } 25 | 26 | dispose() { 27 | this.tree.dispose() 28 | } 29 | 30 | // Private 31 | 32 | getChildren(element) { 33 | if (!this.rootItems?.length > 0) return [] 34 | 35 | if (!element) { 36 | return this.rootItems 37 | } else { 38 | return element.children 39 | } 40 | } 41 | 42 | getParent(element) { 43 | return element.parent 44 | } 45 | 46 | getTreeItem(element) { 47 | let item = new TreeItem(element.name) 48 | 49 | item.collapsibleState = element.collapsibleState 50 | item.command = element.command 51 | item.color = element.color 52 | item.contextValue = element.contextValue 53 | item.descriptiveText = element.descriptiveText 54 | item.identifier = element.identifier 55 | item.image = element.image 56 | item.path = element.path 57 | item.tooltip = element.tooltip 58 | 59 | item.line = element.line 60 | 61 | return item 62 | } 63 | 64 | async fetchNotes() { 65 | const rootItems = [] 66 | 67 | try { 68 | const notes = await railsNotes() 69 | 70 | notes.forEach(notesGroup => { 71 | const element = new NotesItem(notesGroup.filename) 72 | 73 | element.collapsibleState = TreeItemCollapsibleState.Expanded 74 | element.descriptiveText = `(${notesGroup.notes.length})` 75 | element.path = notesGroup.path 76 | element.tooltip = notesGroup.path 77 | 78 | notesGroup.notes.forEach(note => { 79 | const n = new NotesItem(note.annotation) 80 | 81 | n.descriptiveText = note.comment 82 | n.line = note.line 83 | 84 | element.addChild(n) 85 | }) 86 | 87 | rootItems.push(element) 88 | }) 89 | 90 | this.rootItems = rootItems 91 | 92 | this.reload() 93 | } catch (err) { 94 | console.error("Something went wrong trying to fetch Rails Notes:", err) 95 | } 96 | } 97 | 98 | registerCommands() { 99 | nova.commands.register("tommasonegri.rails.commands.sidebar.notes.openFile", () => { 100 | let selection = this.tree.selection[0] 101 | 102 | let path 103 | if (selection.path) { 104 | path = `${nova.workspace.path}/${selection.path}` 105 | } else { 106 | path = `${nova.workspace.path}/${selection.parent.path}` 107 | } 108 | 109 | nova.workspace.openFile(path, { 110 | line: +selection.line 111 | }) 112 | }) 113 | 114 | nova.commands.register("tommasonegri.rails.commands.sidebar.notes.reload", () => { 115 | this.fetchNotes() 116 | }) 117 | } 118 | } 119 | 120 | class NotesItem { 121 | constructor(name) { 122 | this.name = name 123 | this.collapsibleState = TreeItemCollapsibleState.None 124 | this.command = "tommasonegri.rails.commands.sidebar.notes.openFile" 125 | this.color = null 126 | this.contextValue = null 127 | this.descriptiveText = "" 128 | this.identifier = null 129 | this.image = null 130 | this.path = null 131 | this.tooltip = "" 132 | 133 | this.line = null 134 | 135 | this.children = [] 136 | this.parent = null 137 | } 138 | 139 | addChild(element) { 140 | element.parent = this 141 | this.children = [...this.children, element] 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Tests/ruby.rb: -------------------------------------------------------------------------------- 1 | chars = 'a,b,c' 2 | 3 | chars.split(',').length 4 | 5 | chars.split(',').length 6 | 7 | # Arity check example 8 | def example(arg1, arg2 = 0); end 9 | 10 | example(1) # OK 11 | example(1, 2) # OK 12 | example(1, 2, 3) # Error: Too many arguments 13 | -------------------------------------------------------------------------------- /Tests/stimulus_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | } 5 | -------------------------------------------------------------------------------- /Tests/test.html.erb: -------------------------------------------------------------------------------- 1 | Pinned calendar 2 | 3 |

Home#index

4 |

Find me in app/views/home/index.html.erb

5 | -------------------------------------------------------------------------------- /Tests/test_controller.rb: -------------------------------------------------------------------------------- 1 | class EventsController < ApplicationController 2 | before_action :set_event, only: %i[ show edit update destroy ] 3 | 4 | # GET /events or /events.json 5 | def index 6 | @events = Event.all 7 | end 8 | 9 | # GET /events/1 or /events/1.json 10 | def show 11 | end 12 | 13 | # GET /events/new 14 | def new 15 | @event = Event.new 16 | end 17 | 18 | # GET /events/1/edit 19 | def edit 20 | end 21 | 22 | # POST /events or /events.json 23 | def create 24 | @event = Event.new(event_params) 25 | 26 | respond_to do |format| 27 | if @event.save 28 | format.html { redirect_to @event, notice: "Event was successfully created." } 29 | format.json { render :show, status: :created, location: @event } 30 | else 31 | format.html { render :new, status: :unprocessable_entity } 32 | format.json { render json: @event.errors, status: :unprocessable_entity } 33 | end 34 | end 35 | end 36 | 37 | # PATCH/PUT /events/1 or /events/1.json 38 | def update 39 | respond_to do |format| 40 | if @event.update(event_params) 41 | format.html { redirect_to @event, notice: "Event was successfully updated." } 42 | format.json { render :show, status: :ok, location: @event } 43 | else 44 | format.html { render :edit, status: :unprocessable_entity } 45 | format.json { render json: @event.errors, status: :unprocessable_entity } 46 | end 47 | end 48 | end 49 | 50 | # DELETE /events/1 or /events/1.json 51 | def destroy 52 | @event.destroy 53 | respond_to do |format| 54 | format.html { redirect_to events_url, notice: "Event was successfully destroyed." } 55 | format.json { head :no_content } 56 | end 57 | end 58 | 59 | private 60 | # Use callbacks to share common setup or constraints between actions. 61 | def set_event 62 | @event = Event.find(params[:id]) 63 | end 64 | 65 | # Only allow a list of trusted parameters through. 66 | def event_params 67 | params.require(:event).permit(:summary, :description, :ip_class) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /docs/images/stimulus-clips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/docs/images/stimulus-clips.png -------------------------------------------------------------------------------- /extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "com.tommasonegri.Rails", 3 | "name": "Ruby on Rails", 4 | "organization": "Tommaso Negri", 5 | "description": "Ruby on Rails and Ruby support for Nova editor.", 6 | "version": "8.0", 7 | "main": "main.js", 8 | "license": "MIT", 9 | "keywords": [ 10 | "ruby", 11 | "rails", 12 | "nova", 13 | "syntax", 14 | "ruby on rails", 15 | "migrations", 16 | "documentation", 17 | "stimulus" 18 | ], 19 | "repository": "https://github.com/nova-ruby/rails", 20 | "homepage": "https://tommasonegri.com/?ref=nova-rails", 21 | "funding": "https://www.paypal.com/paypalme/tommasonegri/25EUR", 22 | "bugs": "https://github.com/nova-ruby/rails/issues", 23 | "categories": [ 24 | "clips", 25 | "commands", 26 | "completions", 27 | "sidebars", 28 | "tasks" 29 | ], 30 | "entitlements": { 31 | "filesystem": "readwrite", 32 | "process": true 33 | }, 34 | "activationEvents": [ 35 | "onLanguage:html+erb", 36 | "onLanguage:ruby", 37 | "onWorkspaceContains:*.rb", 38 | "onWorkspaceContains:*.erb" 39 | ], 40 | "taskTemplates": { 41 | "railsDev": { 42 | "name": "Rails Dev", 43 | "description": "Runs the Rails development environment (bin/dev).", 44 | "persistent": true, 45 | "image": "task-rails", 46 | "tasks": { 47 | "run": { 48 | "shell": true, 49 | "command": "bin/dev" 50 | } 51 | } 52 | }, 53 | "railsServer": { 54 | "name": "Rails Server", 55 | "description": "Runs the Rails development server.", 56 | "persistent": true, 57 | "image": "task-rails", 58 | "tasks": { 59 | "run": { 60 | "shell": true, 61 | "command": "bin/rails", 62 | "args": [ 63 | "server", 64 | "--environment=$(Config:railsServer.env)", 65 | "--port=$(Config:railsServer.port)", 66 | "--config=$(Config:railsServer.config)", 67 | "--using=$(Config:railsServer.rackServer)", 68 | "--pid=$(Config:railsServer.pid)" 69 | ] 70 | } 71 | }, 72 | "config": [ 73 | { 74 | "key": "railsServer.env", 75 | "title": "Environment", 76 | "description": "Specifies the environment to run this server under", 77 | "type": "enum", 78 | "values": ["test", "development", "production"], 79 | "default": "development", 80 | "allowsCustom": true, 81 | "required": true 82 | }, 83 | { 84 | "key": "railsServer.port", 85 | "title": "Port", 86 | "description": "Runs Rails on the specified port", 87 | "type": "number", 88 | "placeholder": "3000", 89 | "default": 3000, 90 | "min": 1, 91 | "max": 65535, 92 | "required": true 93 | }, 94 | { 95 | "title": "Advanced", 96 | "type": "section", 97 | "children": [ 98 | { 99 | "key": "railsServer.config", 100 | "title": "Config File", 101 | "description": "Uses a custom rackup configuration", 102 | "type": "path", 103 | "default": "config.ru", 104 | "required": true 105 | }, 106 | { 107 | "key": "railsServer.pid", 108 | "title": "PID File", 109 | "description": "Specifies the PID file", 110 | "type": "path", 111 | "default": "tmp/pids/server.pid", 112 | "required": true 113 | }, 114 | { 115 | "key": "railsServer.rackServer", 116 | "title": "Rack Server", 117 | "description": "Specifies the Rack server used to run the application", 118 | "type": "enum", 119 | "values": ["puma", "thin", "webrick"], 120 | "default": "puma", 121 | "required": true 122 | } 123 | ] 124 | } 125 | ] 126 | } 127 | }, 128 | "commands": { 129 | "extensions": [ 130 | { 131 | "title": "Open Alternate File", 132 | "command": "tommasonegri.rails.commands.openAlternateFile" 133 | }, 134 | { "separator": true }, 135 | { 136 | "title": "Open Latest Migration", 137 | "command": "tommasonegri.rails.commands.migrations.openLatest" 138 | }, 139 | { 140 | "title": "List Migrations", 141 | "command": "tommasonegri.rails.commands.migrations.list" 142 | }, 143 | { 144 | "title": "Migrate", 145 | "command": "tommasonegri.rails.commands.migrations.migrate" 146 | }, 147 | { 148 | "title": "Rollback", 149 | "command": "tommasonegri.rails.commands.migrations.rollback" 150 | }, 151 | { "separator": true }, 152 | { 153 | "title": "Search Documentation", 154 | "command": "tommasonegri.rails.commands.documentation.search" 155 | }, 156 | { "separator": true }, 157 | { 158 | "title": "Routes", 159 | "command": "tommasonegri.rails.commands.info.routes" 160 | }, 161 | { 162 | "title": "Project properties", 163 | "command": "tommasonegri.rails.commands.info.properties" 164 | }, 165 | { "separator": true }, 166 | { 167 | "title": "Rails Guides", 168 | "command": "tommasonegri.rails.commands.documentation.openRailsGuides" 169 | }, 170 | { 171 | "title": "Rails API", 172 | "command": "tommasonegri.rails.commands.documentation.openRailsAPI" 173 | }, 174 | { 175 | "title": "Rails Forum", 176 | "command": "tommasonegri.rails.commands.documentation.openRailsForum" 177 | }, 178 | { 179 | "title": "Turbo Reference", 180 | "command": "tommasonegri.rails.commands.documentation.openTurboReference" 181 | }, 182 | { 183 | "title": "Stimulus Reference", 184 | "command": "tommasonegri.rails.commands.documentation.openStimulusReference" 185 | }, 186 | { "separator": true }, 187 | { 188 | "title": "Extension Wiki", 189 | "command": "tommasonegri.rails.commands.documentation.openExtensionWiki" 190 | } 191 | ], 192 | "editor": [ 193 | { 194 | "title": "Show related files", 195 | "command": "tommasonegri.rails.commands.showRelatedFiles", 196 | "shortcut": "ctrl-opt-p" 197 | }, 198 | { 199 | "title": "ERB Tag Switcher", 200 | "command": "tommasonegri.rails.commands.erb.tagSwitcher", 201 | "shortcut": "cmd-shift->", 202 | "when": "editorHasFocus", 203 | "filters": { 204 | "syntaxes": ["erb", "html+erb"] 205 | } 206 | }, 207 | { "separator": true }, 208 | { 209 | "title": "Preview mailer", 210 | "command": "tommasonegri.rails.commands.previewMailer" 211 | } 212 | ], 213 | "command-palette": [ 214 | { 215 | "title": "Reload Extension", 216 | "command": "tommasonegri.rails.commands.reload" 217 | }, 218 | { 219 | "title": "Reload About Rails Sidebar", 220 | "command": "tommasonegri.rails.commands.sidebar.aboutRails.reload" 221 | }, 222 | { 223 | "title": "Reload Rails Notes Sidebar", 224 | "command": "tommasonegri.rails.commands.sidebar.notes.reload" 225 | }, 226 | { 227 | "title": "Kill Puma Server", 228 | "command": "tommasonegri.rails.commands.pumaServer.kill" 229 | }, 230 | { 231 | "title": "Update Stimulus manifest", 232 | "command": "tommasonegri.rails.commands.stimulus.manifest.update" 233 | }, 234 | { 235 | "title": "Pin package to importmap", 236 | "command": "tommasonegri.rails.commands.importmap.pin" 237 | }, 238 | { 239 | "title": "Unpin package from importmap", 240 | "command": "tommasonegri.rails.commands.importmap.unpin" 241 | } 242 | ] 243 | }, 244 | "sidebars": [ 245 | { 246 | "id": "tommasonegri.rails.sidebar", 247 | "name": "Rails", 248 | "smallImage": "RailsSidebarSmall", 249 | "smallSelectedImage": "RailsSidebarSmallSelected", 250 | "largeImage": "RailsSidebarLarge", 251 | "largeSelectedImage": "RailsSidebarLargeSelected", 252 | "sections": [ 253 | { 254 | "id": "tommasonegri.rails.sidebar.notes", 255 | "name": "Notes", 256 | "placeholderImage": "RailsLogo", 257 | "placeholderText": "There are no Rails Notes in the current workspace", 258 | "allowMultiple": false, 259 | "headerCommands": [ 260 | { 261 | "title": "Refresh", 262 | "image": "__builtin.refresh", 263 | "command": "tommasonegri.rails.commands.sidebar.notes.reload" 264 | } 265 | ] 266 | }, 267 | { 268 | "id": "tommasonegri.rails.sidebar.about", 269 | "name": "About", 270 | "placeholderImage": "RailsLogo", 271 | "placeholderText": "Ruby on Rails not detected in the current workspace", 272 | "headerCommands": [ 273 | { 274 | "title": "Refresh", 275 | "image": "__builtin.refresh", 276 | "command": "tommasonegri.rails.commands.sidebar.aboutRails.reload" 277 | } 278 | ] 279 | } 280 | ] 281 | } 282 | ], 283 | "config": [ 284 | { 285 | "key": "tommasonegri.rails.config.general.statusNotifications", 286 | "title": "Status Notifications", 287 | "description": "Show a notification when the server changes status. For example on realod, stop and start. If disabled you will only be notified on crashes or errors.", 288 | "type": "boolean", 289 | "default": true 290 | } 291 | ], 292 | "configWorkspace": [ 293 | { 294 | "key": "tommasonegri.rails.config.general.statusNotifications", 295 | "title": "Status Notifications", 296 | "description": "Show a notification when the server changes status. For example on realod, stop and start. If disabled you will only be notified on crashes or errors.", 297 | "type": "enum", 298 | "values": ["Global Default", "Enabled", "Disabled"], 299 | "radio": false, 300 | "default": "Global Default" 301 | } 302 | ] 303 | } 304 | -------------------------------------------------------------------------------- /extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/extension.png -------------------------------------------------------------------------------- /extension@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/extension@2x.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "target": "es6" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /misc/extension.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/misc/extension.afdesign -------------------------------------------------------------------------------- /misc/extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-ruby/rails/f2b9fd6eef8bf054de9837d34f59dde0c3cc3758/misc/extension.png --------------------------------------------------------------------------------