├── .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 | 
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
--------------------------------------------------------------------------------