├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ └── stale.yml ├── .gitignore ├── .node-version ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── docs ├── debugger.md ├── developing.md ├── formatting.md ├── language-server.md ├── legacy.md ├── linting.md └── troubleshooting.md ├── images ├── badge.png ├── debug-ruby-script.gif ├── syntax_after.png └── syntax_before.png ├── lerna.json ├── package.json ├── packages ├── .eslintrc.base.json ├── language-server-ruby │ ├── .eslintrc.json │ ├── .gitignore │ ├── esbuild.js │ ├── package.json │ ├── spec │ │ ├── analyzers │ │ │ └── FoldingRangeAnalyzer.spec.ts │ │ ├── fixtures │ │ │ └── folds.rb │ │ ├── helper.ts │ │ ├── hooks.js │ │ └── util │ │ │ └── Position.spec.ts │ ├── src │ │ ├── Analyzer.ts │ │ ├── CapabilityCalculator.ts │ │ ├── DocumentManager.ts │ │ ├── Forest.ts │ │ ├── Formatter.ts │ │ ├── Linter.ts │ │ ├── Server.ts │ │ ├── SettingsCache.ts │ │ ├── analyzers │ │ │ ├── BaseAnalyzer.ts │ │ │ ├── DocumentHighlightAnalyzer.ts │ │ │ ├── DocumentSymbolAnalyzer.ts │ │ │ ├── FoldingRangeAnalyzer.ts │ │ │ └── queries │ │ │ │ └── folds.ts │ │ ├── formatters │ │ │ ├── BaseFormatter.ts │ │ │ ├── NullFormatter.ts │ │ │ ├── Prettier.ts │ │ │ ├── RuboCop.ts │ │ │ ├── RubyFMT.ts │ │ │ ├── Rufo.ts │ │ │ ├── Standard.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── linters │ │ │ ├── BaseLinter.ts │ │ │ ├── NullLinter.ts │ │ │ ├── Reek.ts │ │ │ ├── RuboCop.ts │ │ │ ├── Standard.ts │ │ │ └── index.ts │ │ ├── providers │ │ │ ├── ConfigurationProvider.ts │ │ │ ├── DocumentFormattingProvider.ts │ │ │ ├── DocumentHighlightProvider.ts │ │ │ ├── DocumentSymbolProvider.ts │ │ │ ├── FoldingRangeProvider.ts │ │ │ ├── Provider.ts │ │ │ └── WorkspaceProvider.ts │ │ └── util │ │ │ ├── Position.ts │ │ │ ├── RubyDocumentSymbol.ts │ │ │ ├── Stack.ts │ │ │ ├── TreeSitterFactory.ts │ │ │ ├── index.ts │ │ │ └── spawn.ts │ └── tsconfig.json ├── tsconfig.base.json ├── vscode-ruby-client │ ├── .eslintrc.json │ ├── README.md │ ├── images │ │ └── ruby.png │ ├── package.json │ ├── scripts │ │ ├── build-dist.sh │ │ └── link-dist.sh │ ├── src │ │ ├── client.ts │ │ ├── format │ │ │ ├── RuboCop.ts │ │ │ └── rubyFormat.ts │ │ ├── languageConfiguration.test.ts │ │ ├── languageConfiguration.ts │ │ ├── lint │ │ │ ├── lib │ │ │ │ ├── fsPromise.js │ │ │ │ ├── lintResults.js │ │ │ │ ├── linter.js │ │ │ │ └── linters │ │ │ │ │ ├── RuboCop.js │ │ │ │ │ ├── Ruby.js │ │ │ │ │ ├── debride.js │ │ │ │ │ ├── fasterer.js │ │ │ │ │ ├── reek.js │ │ │ │ │ └── ruby-lint.js │ │ │ ├── lintCollection.ts │ │ │ └── lintConfig.ts │ │ ├── locate │ │ │ └── locate.js │ │ ├── providers │ │ │ ├── completion.ts │ │ │ ├── configuration.ts │ │ │ ├── formatter.ts │ │ │ ├── highlight.ts │ │ │ ├── intellisense.ts │ │ │ └── linters.ts │ │ ├── ruby.ts │ │ ├── task │ │ │ └── rake.ts │ │ ├── util │ │ │ └── env.ts │ │ └── utils.ts │ └── tsconfig.json ├── vscode-ruby-common │ ├── .eslintrc.json │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── environment.ts │ │ └── index.ts │ ├── test │ │ ├── environment.spec.ts │ │ ├── fixtures │ │ │ └── environment │ │ │ │ ├── environment.fish.txt │ │ │ │ ├── environment.posix.txt │ │ │ │ └── environment.win32.txt │ │ └── setup.ts │ └── tsconfig.json ├── vscode-ruby-debugger │ ├── .eslintrc.json │ ├── package.json │ ├── spec │ │ ├── adapter.test.ts │ │ └── data │ │ │ └── basic.rb │ ├── src │ │ ├── common.ts │ │ ├── helper.ts │ │ ├── interface.ts │ │ ├── main.ts │ │ └── ruby.ts │ └── tsconfig.json ├── vscode-ruby │ ├── README.md │ ├── images │ │ ├── logo.png │ │ └── logo.svg │ ├── language-configuration-erb.json │ ├── language-configuration-ruby.json │ ├── package.json │ ├── snippets │ │ ├── erb.json │ │ └── ruby.json │ ├── syntaxes │ │ ├── erb.cson.json │ │ ├── gemfile.cson.json │ │ └── ruby.cson.json │ └── test │ │ ├── Gemfile │ │ ├── hash.rb │ │ ├── heredoc.rb │ │ ├── methods.rb │ │ ├── simple_syntax.rb │ │ ├── syntax.erb │ │ ├── syntax.rb │ │ └── yard.rb └── web-tree-sitter-ruby │ ├── README.md │ ├── index.d.ts │ ├── index.js │ ├── package.json │ └── tree-sitter-ruby.wasm └── yarn.lock /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Feel free to open issues or PRs! We welcome all contributions, even from beginners. If you want to get started with a PR, please do the following: 2 | 3 | ## Prereqs 4 | 5 | 1. Check out the [VS Code Extension Docs](https://code.visualstudio.com/docs/extensions/overview), especially [Running and Debugging Extensions](https://code.visualstudio.com/docs/extensions/debugging-extensions). 6 | 1. Read the [development docs](https://github.com/rubyide/vscode-ruby/blob/main/docs/developing.md) 7 | 8 | ## Contributing 9 | 10 | 1. Fork this repo. 11 | 1. Create a feature branch for your work (eg `git checkout -b `) 12 | 1. Install dependencies with `yarn install` 13 | 1. Run `yarn watch`. This symlinks the `dist` directories from the server, extension, and debugger packages and starts webpack in each directory 14 | 1. Open the repo directory in VS Code. 15 | 1. Make a code change and test it. The debug tab and the various launch tasks will help! 16 | 1. Ensure your changes have test coverage 17 | 1. Submit a PR! 18 | 19 | ## Other Stuff 20 | 21 | - Please do not update the version or the `CHANGELOG` 22 | - Please make sure your addition does not regress functionality for other users 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Your environment 2 | 3 | - `vscode-ruby` version: 4 | - Ruby version: 5 | - Ruby version manager (if any): 6 | - VS Code version: 7 | - Operating System: 8 | - Using language server? (eg `useLanguageServer` is true in your configuration?) 9 | 10 | ### Expected behavior 11 | 12 | ### Actual behavior 13 | 14 | *This and the next section should include screenshots, code samples, console output, etc. The more information we have to reproduce the better!* 15 | *If this is a syntax highlighting report please include a code sample that can be quickly copied and pasted!* 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Description of change and why it was needed here* 2 | 3 | - [] The build passes 4 | - [] TSLint is mostly happy 5 | - [] Prettier has been run -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues' 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v5.0.0 11 | with: 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | stale-issue-message: 'This issue has not had activity for 30 days. It will be automatically closed in 30 days.' 14 | stale-pr-message: 'This PR has not had activity for 30 days. It will be automatically closed in 30 days.' 15 | exempt-issue-labels: 'bug,enhancement,feature-request,help-wanted,upstream' 16 | exempt-pr-labels: 'bug,enhancement,feature-request,help-wanted,upstream' 17 | exempt-draft-pr: true 18 | stale-issue-label: 'stale' 19 | stale-pr-label: 'stale' 20 | days-before-stale: 120 21 | days-before-close: 30 22 | remove-stale-when-updated: true 23 | 24 | permissions: 25 | issues: write 26 | pull-requests: write 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | out/ 4 | dist/ 5 | npm-debug.log 6 | *.vsix 7 | *.zip 8 | *.tar.gz 9 | .nyc_output 10 | coverage 11 | .cache 12 | lerna-debug.log 13 | build/* 14 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 16.13.1 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "sourceMaps": true, 10 | "args": [ 11 | "--extensionDevelopmentPath=${workspaceFolder}/packages/vscode-ruby-client", 12 | "--extensionDevelopmentPath=${workspaceFolder}/packages/vscode-ruby" 13 | ], 14 | "outFiles": ["${workspaceFolder}/packages/vscode-ruby-client/dist/client/**/*.js"] 15 | }, 16 | { 17 | "type": "node", 18 | "request": "launch", 19 | "name": "Debug Server", 20 | "cwd": "${workspaceRoot}", 21 | "program": "${workspaceRoot}/packages/vscode-ruby-debugger/src/main.ts", 22 | "args": ["--server=4711"], 23 | "sourceMaps": true, 24 | "outFiles": ["${workspaceRoot}/packages/vscode-ruby-debugger/dist/**/*.js"] 25 | }, 26 | { 27 | "name": "Attach to Language Server", 28 | "type": "node", 29 | "request": "attach", 30 | "port": 6009, 31 | "sourceMaps": true, 32 | "outFiles": ["${workspaceRoot}/packages/language-server-ruby/dist/**/*.js"] 33 | } 34 | ], 35 | "compounds": [ 36 | { 37 | "name": "Extension w/ Debug Server", 38 | "configurations": ["Launch Extension", "Debug Server"] 39 | }, 40 | { 41 | "name": "Launch Extension and attach to Language Server", 42 | "configurations": ["Launch Extension", "Attach to Language Server"] 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | // ESLint 4 | "eslint.enable": true, 5 | "eslint.packageManager": "yarn", 6 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], 7 | "eslint.lintTask.enable": true, 8 | "eslint.workingDirectories": [ 9 | { "directory": "./packages/language-server-ruby", "changeProcessCWD": true }, 10 | { "directory": "./packages/vscode-ruby-client", "changeProcessCWD": true }, 11 | { "directory": "./packages/vscode-ruby-common", "changeProcessCWD": true }, 12 | { "directory": "./packages/vscode-ruby-debugger", "changeProcessCWD": true } 13 | ], 14 | 15 | // Format with Prettier 16 | "[typescript]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode", 18 | "editor.formatOnSave": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "watch", 7 | "problemMatcher": "$tsc-watch", 8 | "isBackground": true, 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/**/* 2 | .gitignore 3 | packages/**/* 4 | *.vsix 5 | appveyor.yml 6 | .travis.yml 7 | .prettierrc 8 | tsconfig.base.json 9 | tsconfig.json 10 | tslint.json 11 | lerna.json 12 | *.zip 13 | *.tar.gz 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Peng Lv 4 | Copyright (c) 2017-2019 Stafford Brunk 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Visual Studio Code Ruby Extensions 2 | 3 | 4 | 5 | 6 | # DEPRECATED 7 | 8 | Shopify's [ruby-lsp](https://github.com/Shopify/ruby-lsp) and associated [vscode-ruby-lsp](https://github.com/Shopify/vscode-ruby-lsp) are recommended alternatives to this extension. It is substantially easier to produce a high-quality LSP implementation using Ruby itself vs relying on another language such as TypeScript. 9 | 10 | In addition, sponsorship of a project by a company like Shopify could help to ensure a high-quality developer experience going forward. Even with multiple people helping on this project, keeping up with Microsoft's development of VSCode plus the wide array of Ruby community tooling is a tall undertaking. 11 | 12 | As of 4/2/2023, the VSCode extensions are not marked deprecated. They will be within the next few weeks. 13 | 14 | ## Readme 15 | 16 | This is the monorepo for the Visual Studio Code Ruby extensions. 17 | 18 | Head on over to the [Ruby extension README](https://github.com/rubyide/vscode-ruby/blob/main/packages/vscode-ruby-client/README.md) to get started! 19 | 20 | ## Packages 21 | 22 | - [`vscode-ruby`](https://github.com/rubyide/vscode-ruby/blob/main/packages/vscode-ruby) - extension providing syntax highlighting, language configuration, and snippets for Ruby 23 | - [`vscode-ruby-client`](https://github.com/rubyide/vscode-ruby/blob/main/packages/vscode-ruby-client) - extension logic including the language server client 24 | - [`vscode-ruby-common`](https://github.com/rubyide/vscode-ruby/blob/main/packages/vscode-ruby-common) - common utilities that are shared among several other packages (eg environment detection) 25 | - [`vscode-ruby-debugger`](https://github.com/rubyide/vscode-ruby/blob/main/packages/vscode-ruby-debugger) - implementation of the debugger 26 | - [`language-server-ruby`](https://github.com/rubyide/vscode-ruby/blob/main/packages/language-server-ruby) - language server implementation 27 | 28 | 29 | ## Docs 30 | 31 | Documentation is available in the [docs](https://github.com/rubyide/vscode-ruby/tree/main/docs) folder 32 | 33 | ## Troubleshooting 34 | 35 | Troubleshooting documentation is available [here](https://github.com/rubyide/vscode-ruby/tree/main/docs/troubleshooting.md) folder 36 | 37 | ## Developing 38 | 39 | See the [developing](https://github.com/rubyide/vscode-ruby/blob/main/docs/developing.md) docs 40 | -------------------------------------------------------------------------------- /docs/debugger.md: -------------------------------------------------------------------------------- 1 | # Debugger 2 | 3 | ## Install Ruby Dependencies 4 | 5 | In this extension, we implement [ruby debug ide protocol](https://github.com/ruby-debug/ruby-debug-ide/blob/main/doc/protocol-spec.texi) to allow VS Code to communicate with ruby debug, it requires `ruby-debug-ide` to be installed on your machine. This is also how RubyMine/NetBeans does by default. 6 | 7 | - If you are using JRuby or Ruby v1.8.x (`jruby`, `ruby_18`, `mingw_18`), run `gem install ruby-debug-ide`. 8 | - If you are using Ruby v1.9.x (`ruby_19`, `mingw_19`), run `gem install ruby-debug-ide`. Make sure `ruby-debug-base19x` is installed together with `ruby-debug-ide`. 9 | - If you are using Ruby v2.x 10 | - `gem install ruby-debug-ide` 11 | - `gem install debase` (or `gem install byebug`) 12 | 13 | ## Add VS Code config to your project 14 | 15 | Go to the debugger view of VS Code and hit the gear icon. Choose Ruby or Ruby Debugger from the prompt window, then you'll get the sample launch config in `.vscode/launch.json`. The sample launch configurations include debuggers for RSpec (complete, and active spec file) and Cucumber runs. These examples expect that `bundle install --binstubs` has been called. 16 | 17 | ## Detailed instruction for debugging Ruby Scripts/Rails/etc 18 | 19 | Read following instructions about how to debug ruby/rails/etc locally or remotely 20 | 21 | - [Debugger installation](https://github.com/rubyide/vscode-ruby/wiki/1.-Debugger-Installation) 22 | - [Launching from VS Code](https://github.com/rubyide/vscode-ruby/wiki/2.-Launching-from-VS-Code) 23 | - [Attaching to a debugger](https://github.com/rubyide/vscode-ruby/wiki/3.-Attaching-to-a-debugger) 24 | - [Running gem scripts](https://github.com/rubyide/vscode-ruby/wiki/4.-Running-gem-scripts) 25 | - [Example configurations](https://github.com/rubyide/vscode-ruby/wiki/5.-Example-configurations) 26 | 27 | ## Debugger F.A.Q. 28 | 29 | Q: What's the difference between 'skipFiles' and 'finishFiles'? 30 | A: The debugger will automatically step through skipFiles, whereas it will automatically step out of finishFiles. 31 | 32 | Q: What are skipFiles and finishFiles useful for? 33 | A: They are two different tools for avoiding parts of the code while stepping. skipFiles helps you skip stack frames that sit between frames that you are interested in (e.g. [https://sorbet.org/](sorbet)). finishFiles, on the other hand, help you avoid stack frames (and everything deeper than them) entirely. 34 | 35 | Q: Can I see an example? 36 | A: In the listing below, assume the debugger is suspended at line 2. Here's what happens if you 'Step Into' (F11) when... 37 | 38 | * `b.rb` is in neither: the debugger suspends at line 5. 39 | * `b.rb` is in `skipFiles`: the program prints "1" then the debugger suspends at line 9. 40 | * `b.rb` is in `finishFiles`: the program prints "12" then the debugger suspends at line 3. 41 | 42 | ``` 43 | 1: def methodA 44 | 2: => methodB 45 | 3: puts "3" 46 | # Assume methodB is in b.rb 47 | 4: def methodB: 48 | 5: puts "1" 49 | 6: methodC 50 | # Assume methodC is in c.rb 51 | 8: def methodC: 52 | 9: puts "2" 53 | ``` 54 | 55 | ### Conditional breakpoint doesn't work 56 | 57 | You need use Ruby `2.0` or above and you need to update `debase` to latest beta version `gem install debase -v 0.2.2.beta10`. 58 | -------------------------------------------------------------------------------- /docs/developing.md: -------------------------------------------------------------------------------- 1 | # Developing 2 | 3 | ## Repository Structure 4 | 5 | This repository uses [`lerna`](https://github.com/lerna/lerna) combined with [yarn workspaces](https://yarnpkg.com/lang/en/docs/workspaces/) to organize itself. 6 | 7 | The overall extension is broken out into several packages within the `packages` directory: 8 | 9 | - [`vscode-ruby`](https://github.com/rubyide/vscode-ruby/blob/main/packages/vscode-ruby) - extension providing syntax highlighting, language configuration, and snippets for Ruby 10 | - [`vscode-ruby-client`](https://github.com/rubyide/vscode-ruby/blob/main/packages/vscode-ruby-client) - extension logic including the language server client 11 | - [`vscode-ruby-common`](https://github.com/rubyide/vscode-ruby/blob/main/packages/vscode-ruby-common) - common utilities that are shared among several other packages (eg environment detection) 12 | - [`vscode-ruby-debugger`](https://github.com/rubyide/vscode-ruby/blob/main/packages/vscode-ruby-debugger) - implementation of the debugger 13 | - [`language-server-ruby`](https://github.com/rubyide/vscode-ruby/blob/main/packages/language-server-ruby) - language server implementation 14 | 15 | 16 | Each package utilizes `esbuild` or `tsc` to build the `dist` directory. 17 | 18 | Packages that use `esbuild` are consumed directly by VS Code. VS Code does not run `npm install` for packages and thus any code must make sure its dependencies are bundled alongside it. 19 | 20 | Packages that just use `tsc` are dependencies of other packages. 21 | 22 | ## Getting Started 23 | 24 | - Clone the repo 25 | - run `yarn install` 26 | - run `yarn watch` 27 | 28 | `yarn watch` will symlink the `dist` directories from the required packages and start esbuild (via `lerna`) in each package. You can look at the `scripts/link-dist.sh` script to see what is happening there. 29 | 30 | ## Developing 31 | 32 | The root `package.json` for the extension depends on the appropriate code being present in the root `dist` directory. The `yarn watch` script above will take care of that for you. 33 | 34 | ## Debugging 35 | 36 | There are several debug profiles defined in `launch.json`: 37 | 38 | - `Launch Extension` - this will launch the VS Code extension in a new isntance of VS Code. This will temporarily overwrite any extensions by the same name that are installed 39 | - `Attach to Language Server` - this will attached to the running language server which will allow you to set breakpoints 40 | - `Debugger Server` - this will start a server that will allow you to debug the debugger package 41 | 42 | ## Running Tests 43 | 44 | Each package should have a `yarn test` script defined. You can run all tests for the whole repository with the root `yarn test` command. 45 | 46 | ## Resources 47 | 48 | - [VS Code Extension Docs](https://code.visualstudio.com/api) 49 | - [VS Code Language Server Extension Guide](https://code.visualstudio.com/api/language-extensions/language-server-extension-guide) 50 | - [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) 51 | - [vscode-languageserver-node](https://github.com/microsoft/vscode-languageserver-node) 52 | -------------------------------------------------------------------------------- /docs/formatting.md: -------------------------------------------------------------------------------- 1 | # Formatting 2 | 3 | The language server currently supports formatting via the following formatters: 4 | 5 | - [RuboCop](https://github.com/rubocop-hq/rubocop) 6 | - [Standard](https://github.com/testdouble/standard) 7 | - [Rufo](https://github.com/ruby-formatter/rufo) 8 | - [Rubyfmt](https://github.com/penelopezone/rubyfmt) 9 | - [Prettier](https://github.com/prettier/plugin-ruby) 10 | 11 | ## Configuration 12 | 13 | Configuration for formatting is provided by the `ruby.format` key 14 | 15 | The global `useBundler` setting is used to determine if the format command should be prefixed with `bundle exec` 16 | -------------------------------------------------------------------------------- /docs/language-server.md: -------------------------------------------------------------------------------- 1 | # Language Server 2 | 3 | `language-server-ruby` is an implementation of the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) in TypeScript with the intention of targetting the Ruby programming language. 4 | 5 | The server is built to be extensible, accurate, and performant with such features as: 6 | 7 | - Automatic Ruby environment detection with support for rvm, rbenv, chruby, and asdf 8 | - Robust language feature extraction powered by the [tree-sitter](https://tree-sitter.github.io/tree-sitter/) project 9 | - Lint support via RuboCop, Reek, and Standard 10 | - Format support via RuboCop, Standard, Rubyfmt, and Rufo 11 | - Semantic code folding support 12 | - Semantic highlighting support 13 | - Intellisense support 14 | 15 | Note that the language server is currently under active development! 16 | 17 | ## Environment Detection 18 | 19 | The language server supports a feature that attempts to properly detect your shell configuration for users of rvm, rbenv, chruby, and the asdf version management tools in addition to Rubies that are globally installed. Note that other management tools may work if they correctly expose themselves via the `PATH`, but any tool specific variables will not be imported (see below on why). 20 | 21 | ### How it works 22 | 23 | Most Ruby version managers expose their functionality via shell functions. This requires spawning an interactive login shell to ensure all of your dotfiles are correctly loaded and an accurate environment can be captured. 24 | 25 | When a file is opened in your editor, the workspace folder in which that file resides will be used as the directory in which the Ruby environment will be loaded. For non-multi-root workspaces, the root folder will be used instead. 26 | 27 | If you are utilizing a monorepo with multiple `Gemfiles`, it is highly recommended you configure that repository as a multi-root workspace, with each directory owning a `Gemfile` configured as a workspace folder. 28 | 29 | If you are using multiple versions of Ruby within a monorepo, it is required that you configure your repository as multi-root as otherwise the language server will properly detect your sub-environments. 30 | 31 | #### Environment Variable Whitelist 32 | 33 | In order to try and minimize processing environment variables that are sensitive or out of scope for the language server, a whitelist is used to filter out variables we don't care about. See `More Information` below to check out this whitelist. 34 | 35 | ### Limitations 36 | 37 | Due to limitations in how node's internal `spawn` function works, a shim file needs to be used to ensure your shell is properly loaded. Right now, POSIX compliant shells, the Fish shell, and `cmd.exe` on Windows are supported. 38 | 39 | ### More Information 40 | 41 | The [`vscode-ruby-common`](https://github.com/rubyide/vscode-ruby/blob/main/packages/vscode-ruby-common) package contains the utilities and shims that facilitate this functionality. See [`environment.ts`](https://github.com/rubyide/vscode-ruby/blob/main/packages/vscode-ruby-common/src/environment.ts) 42 | 43 | ## Logs 44 | 45 | The extension exposes a command via the [VSCode Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) to view the Language Server's logs. Just use the `Ruby: Show Language Server Output Channel` command to view the server's logs 46 | -------------------------------------------------------------------------------- /docs/linting.md: -------------------------------------------------------------------------------- 1 | # Linting 2 | 3 | The language server currently supports linting via the following linters: 4 | 5 | 8 | 9 | - [RuboCop](#rubocop) 10 | - [Standard](#standard) 11 | - [Reek](#reek) 12 | 13 | Additional linter support can be provided but they must conform to the following rules: 14 | 15 | - Input must be provided via `stdin` 16 | - Output must be provided via a machine parsable format (ideally json) 17 | 18 | Linter support for linters that run over a whole project (eg `brakeman`) is being investigated (see [#512](https://github.com/rubyide/vscode-ruby/issues/512)) 19 | 20 | ## Configuration 21 | 22 | Configuration for linting is provided under the `ruby.lint` key. 23 | 24 | The global `useBundler` flag does not apply to linters. This is an outstanding issue and will be corrected later. Each linter allows the option of overriding `useBundler`. 25 | 26 | ## RuboCop 27 | 28 | [RuboCop](https://github.com/rubocop-hq/rubocop) is a Ruby static code analyzer and formatter, based on the community Ruby style guide 29 | 30 | ### Configuration Options 31 | 32 | See the [RuboCop CLI args](https://docs.rubocop.org/rubocop/usage/basic_usage.html#command-line-flags) for more details on the support configuration options 33 | 34 | ```json 35 | "ruby.lint": { 36 | "rubocop": true 37 | } 38 | ``` 39 | 40 | or 41 | 42 | ```json 43 | "ruby.lint": { 44 | "rubocop": { 45 | "command": "rubocop", // setting this will override automatic detection 46 | "useBundler": true, 47 | "lint": true, // enable lint cops 48 | "only": ["array", "of", "cops", "to", "run"], 49 | "except": ["array", "of", "cops", "not", "to", "run"], 50 | "require": ["array", "of", "ruby", "files", "to", "require"], 51 | "rails": true, // requires rubocop-rails gem for RuboCop >= 0.72.0 52 | "forceExclusion": true // for ignoring the excluded files from rubocop.yml 53 | } 54 | } 55 | ``` 56 | 57 | ## Standard 58 | 59 | [Standard](https://github.com/testdouble/standard) is the Ruby Style Guide with linter and automatic code fixer 60 | 61 | ### Configuration Options 62 | 63 | See the [standard docs](https://github.com/testdouble/standard#what-you-might-do-if-youre-really-clever) for more details on these configuration options 64 | 65 | ```json 66 | "ruby.lint": { 67 | "standard": true 68 | } 69 | ``` 70 | 71 | or 72 | 73 | ```json 74 | "ruby.lint": { 75 | "standard": { 76 | "command": "standard", // setting this will override automatic detection 77 | "useBundler": true, 78 | "only": ["array", "of", "cops", "to", "run"], 79 | "except": ["array", "of", "cops", "not", "to", "run"], 80 | "require": ["array", "of", "ruby", "files", "to, "require"] 81 | } 82 | } 83 | ``` 84 | 85 | ## Reek 86 | 87 | [Reek](https://github.com/troessner/reek) is a code smell detector for Ruby 88 | 89 | ### Configuration Options 90 | 91 | ```json 92 | "ruby.lint": { 93 | "reek": true 94 | } 95 | ``` 96 | 97 | or 98 | 99 | ```json 100 | "ruby.lint": { 101 | "reek": { 102 | "command": "reek", // setting this will override automatic detection 103 | "useBundler": true 104 | } 105 | } 106 | ``` 107 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ## Viewing Logs 4 | 5 | The extension exposes two commands via the [VSCode Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) to view logs: 6 | 7 | - `Ruby: Show Output Channel` - opens output channel for the Ruby extension 8 | - `Ruby: Show Language Server Output Channel` - opens output channel for the language server 9 | 10 | ## Syntax Highlighting is incorrect 11 | 12 | VSCode relies on static language grammars to facilitate syntax highlighting. These grammars are stored in the [`syntaxes`]() directory in the root of the project. 13 | 14 | VSCode has good documentation on how grammars work and how to troubleshoot grammars available [here](https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide#scope-inspector). 15 | 16 | If you believe the extension is incorrectly assigning TextMate Scopes, please fix the grammar and open a PR! 17 | 18 | ## Environment is incorrectly detected 19 | 20 | Please read over the [documentation](https://github.com/rubyide/vscode-ruby/blob/main/docs/language-server.md) on Environment Detection in the language server to ensure you understand its features and limitations. 21 | 22 | ## Linting or Formatting is incorrect 23 | 24 | ### Language Server 25 | 26 | The language server will log all commands it attempts to run as well as any errors that command generates. Each type of command is prefixed with it's type (eg `Lint` or `Format`). All commands are run with a `cwd` of the currently open document. This is important if you attempt to run the command in your own shell. 27 | 28 | > Note: running commands from the directory of the open document, as opposed to the workspace root, is intentional. This allows people to put multiple linter configuration files in the project. We expect linters to look recursively up the directory tree to find their configuration file. 29 | 30 | The one thing to keep in mind is that all of those commands are configured to accept editor content via `stdin` and cannot be run verbatim in your terminal. 31 | 32 | For example, if you see the following in the logs: 33 | 34 | ``` 35 | Lint: executing rubocop -s /Users/wingrunr21/someproject/subdir/rubyfile.rb -f json... 36 | ``` 37 | 38 | That is the language server running rubocop against `rubyfile.rb`. 39 | 40 | If you'd like to run that command yourself, you can do something similar to the following: 41 | 42 | ```shell 43 | $ cd /Users/wingrunr21/someproject/subdir 44 | $ cat rubyfile.rb | rubocop -s /Users/wingrunr21/someproject/subdir/rubyfile.rb -f json 45 | ``` 46 | 47 | The file must be piped into `rubocop` or other utilities. This methodology is the best representation of how the language server runs these commands. If the command succeeds here but not in the language server, additional steps will be necessary to troubleshoot. Please open an issue. 48 | 49 | ### Legacy 50 | 51 | The legacy lint and formatters do not attempt to detect your Ruby environment. This means that VSCode must be started with the appropriate environment set for it to be able to successfully run your lint and format commands. The easiest way to do this is via your terminal: 52 | 53 | ```shell 54 | $ cd /path/to/my/project 55 | $ rvm/chruby/rbenv/whatever if necessary 56 | $ code . 57 | ``` 58 | 59 | By doing this, your terminal has configured the environment correct and VSCode will inherit that environment when it starts. 60 | -------------------------------------------------------------------------------- /images/badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyide/vscode-ruby/79031948d032e97cf2a5116e27c167ade5125e15/images/badge.png -------------------------------------------------------------------------------- /images/debug-ruby-script.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyide/vscode-ruby/79031948d032e97cf2a5116e27c167ade5125e15/images/debug-ruby-script.gif -------------------------------------------------------------------------------- /images/syntax_after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyide/vscode-ruby/79031948d032e97cf2a5116e27c167ade5125e15/images/syntax_after.png -------------------------------------------------------------------------------- /images/syntax_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyide/vscode-ruby/79031948d032e97cf2a5116e27c167ade5125e15/images/syntax_before.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "npmClient": "yarn", 6 | "useWorkspaces": true, 7 | "version": "0.27.0" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "packages/*" 5 | ], 6 | "devDependencies": { 7 | "@commitlint/cli": "^11.0.0", 8 | "@commitlint/config-conventional": "^11.0.0", 9 | "@semantic-release/changelog": "^5.0.1", 10 | "commitizen": "^4.2.2", 11 | "cz-conventional-changelog": "^3.3.0", 12 | "husky": "^4.3.6", 13 | "lerna": "^3.16.4", 14 | "semantic-release": "^17.3.1", 15 | "semantic-release-vsce": "^3.0.1", 16 | "vsce": "^1.83.0" 17 | }, 18 | "engines": { 19 | "node": ">=16.13.1" 20 | }, 21 | "scripts": { 22 | "build": "lerna run build", 23 | "package": "mkdir -p build && rm -rf build/* && lerna run package", 24 | "test": "lerna run test", 25 | "watch": "lerna run watch" 26 | }, 27 | "config": { 28 | "commitizen": { 29 | "path": "cz-conventional-changelog" 30 | } 31 | }, 32 | "husky": { 33 | "hooks": { 34 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 35 | } 36 | }, 37 | "commitlint": { 38 | "extends": [ 39 | "@commitlint/config-conventional" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/.eslintrc.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["standard-with-typescript", "prettier/@typescript-eslint", "prettier"], 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "rules": { 7 | "lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }], 8 | "@typescript-eslint/explicit-member-accessibility": [ 9 | "error", 10 | { 11 | "overrides": { 12 | "constructors": "off", 13 | "accessors": "off" 14 | } 15 | } 16 | ], 17 | "@typescript-eslint/explicit-function-return-type": [ 18 | "error", 19 | { 20 | "allowTypedFunctionExpressions": true, 21 | "allowHigherOrderFunctions": true 22 | } 23 | ], 24 | "@typescript-eslint/array-type": ["error", { "default": "array" }] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/language-server-ruby/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../.eslintrc.base.json"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/language-server-ruby/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .cache 3 | -------------------------------------------------------------------------------- /packages/language-server-ruby/esbuild.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const outDir = 'dist'; 5 | 6 | const treeSitterWasmPlugin = { 7 | name: 'treeSitterWasm', 8 | setup(build) { 9 | const wasmPaths = [ 10 | require.resolve('web-tree-sitter/tree-sitter.wasm'), 11 | require.resolve('web-tree-sitter-ruby/tree-sitter-ruby.wasm') 12 | ]; 13 | // build.onEnd(result => { 14 | wasmPaths.forEach(wasmPath => { 15 | fs.copyFileSync(wasmPath, path.join(outDir, path.basename(wasmPath))) 16 | }); 17 | // }) 18 | } 19 | } 20 | 21 | require('esbuild').build({ 22 | entryPoints: ['src/index.ts'], 23 | bundle: true, 24 | sourcemap: true, 25 | minify: true, 26 | outfile: `${outDir}/index.js`, 27 | logLevel: 'info', 28 | external: ['vscode'], 29 | format: 'cjs', 30 | platform: 'node', 31 | plugins: [treeSitterWasmPlugin], 32 | watch: !!process.env.ESBUILD_WATCH 33 | }).catch(() => process.exit(1)) 34 | -------------------------------------------------------------------------------- /packages/language-server-ruby/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "language-server-ruby", 3 | "version": "0.28.1", 4 | "description": "Language server for Ruby", 5 | "repository": "https://github.com/rubyide/vscode-ruby", 6 | "author": "Stafford Brunk ", 7 | "license": "MIT", 8 | "private": false, 9 | "main": "dist/index.js", 10 | "dependencies": { 11 | "cross-spawn": "^6.0.5", 12 | "diff-match-patch": "^1.0.4", 13 | "loglevel": "^1.6.7", 14 | "rxjs": "^6.4.0", 15 | "vscode-languageserver": "^7.0.0", 16 | "vscode-languageserver-textdocument": "^1.0.1", 17 | "vscode-uri": "^3.0.2", 18 | "web-tree-sitter": "^0.20.2", 19 | "web-tree-sitter-ruby": "^0.19.0" 20 | }, 21 | "devDependencies": { 22 | "@types/chai": "^4.2.5", 23 | "@types/cross-spawn": "^6.0.0", 24 | "@types/diff-match-patch": "^1.0.32", 25 | "@types/mocha": "^5.2.7", 26 | "@typescript-eslint/eslint-plugin": "^2.8.0", 27 | "@typescript-eslint/parser": "^2.8.0", 28 | "chai": "^4.2.0", 29 | "esbuild": "^0.14.10", 30 | "eslint": ">=6.6.0", 31 | "eslint-config-prettier": "^6.7.0", 32 | "eslint-config-standard": "^14.1.0", 33 | "eslint-config-standard-with-typescript": "^11.0.1", 34 | "eslint-plugin-import": "^2.18.2", 35 | "eslint-plugin-node": "^10.0.0", 36 | "eslint-plugin-promise": "^4.2.1", 37 | "eslint-plugin-standard": "^4.0.1", 38 | "mocha": "^8.4.0", 39 | "nyc": "^15.1.0", 40 | "prettier": "^1.19.1", 41 | "source-map-support": "^0.5.16", 42 | "ts-node": "^10.4.0", 43 | "typescript": "^4.5.4" 44 | }, 45 | "scripts": { 46 | "lint": "eslint src/**/*.ts", 47 | "test": "nyc mocha -r ts-node/register -r source-map-support/register -r spec/hooks.js spec/**/*.ts", 48 | "build": "NODE_ENV=production node esbuild.js", 49 | "compile": "node esbuild.js", 50 | "watch": "ESBUILD_WATCH=true node esbuild.js" 51 | }, 52 | "nyc": { 53 | "cache": false, 54 | "extension": [ 55 | ".ts" 56 | ], 57 | "exclude": [ 58 | "**/*.d.ts", 59 | "coverage/**", 60 | "packages/*/test/**", 61 | "spec/**", 62 | "spec{,-*}.ts", 63 | "**/*{.,-}{test,spec}.ts", 64 | "**/node_modules/**", 65 | "esbuild.js" 66 | ], 67 | "reporter": [ 68 | "text", 69 | "lcov" 70 | ], 71 | "all": true 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/language-server-ruby/spec/fixtures/folds.rb: -------------------------------------------------------------------------------- 1 | require('open-uri') 2 | require('fileutils') 3 | require 'json' 4 | 5 | =begin 6 | foo 7 | bar 8 | baz 9 | =end 10 | 11 | # 12 | # foo 13 | # bar 14 | # baz 15 | # 16 | 17 | module Foo 18 | class Bam 19 | def foo 20 | puts "bar" 21 | end 22 | 23 | def baz 24 | puts "foo" 25 | end 26 | end 27 | class Ben 28 | end 29 | module Bamm 30 | end 31 | class Folds < Bar 32 | CONSTANT1 = { 33 | foo: 1, 34 | bar: 2, 35 | baz: 3 36 | } 37 | 38 | CONSTANT2 = [ 39 | :foo, 40 | :bar, 41 | :baz 42 | ] 43 | 44 | def self.klassmethod 45 | puts "foo" 46 | puts "bar" 47 | puts "baz" 48 | end 49 | 50 | def method1(a) 51 | case a 52 | when 1 53 | puts "foo" 54 | when 2 55 | puts "bar" 56 | when 3 57 | puts "baz" 58 | end 59 | 60 | case 61 | when a == 1 62 | puts "foo" 63 | when a == 2 64 | puts "bar" 65 | when a == 3 66 | puts "baz" 67 | end 68 | end 69 | 70 | def method2 71 | text = < void): void { 13 | const cursor = tree.walk(); 14 | const walk = (depth: number): void => { 15 | action(cursor.currentNode()); 16 | if (cursor.gotoFirstChild()) { 17 | do { 18 | walk(depth + 1); 19 | } while (cursor.gotoNextSibling()); 20 | cursor.gotoParent(); 21 | } 22 | }; 23 | walk(0); 24 | cursor.delete(); 25 | } 26 | 27 | export function getParser(): Parser { 28 | return (global as any).loader.parser; 29 | } 30 | -------------------------------------------------------------------------------- /packages/language-server-ruby/spec/hooks.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const Parser = require('web-tree-sitter'); 4 | const Ruby = require('web-tree-sitter-ruby').default; 5 | 6 | class FixtureLoader { 7 | async initialize() { 8 | await Parser.init(); 9 | const language = await Parser.Language.load(Ruby); 10 | this.parser = new Parser(); 11 | this.parser.setLanguage(language); 12 | } 13 | 14 | load(name) { 15 | const content = fs.readFileSync(path.join(__filename, '..', 'fixtures', name)).toString(); 16 | 17 | return { 18 | content, 19 | tree: this.parser.parse(content), 20 | }; 21 | } 22 | } 23 | exports.mochaHooks = { 24 | async beforeAll() { 25 | global.loader = new FixtureLoader(); 26 | await global.loader.initialize(); 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /packages/language-server-ruby/spec/util/Position.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import Position from '../../src/util/Position'; 3 | import { Point as TSPosition } from 'web-tree-sitter'; 4 | import { Position as VSPosition } from 'vscode-languageserver'; 5 | 6 | function generateRandomRowCol() { 7 | return { 8 | row: Math.round(Math.random() * 100), 9 | col: Math.round(Math.random() * 100), 10 | }; 11 | } 12 | 13 | describe('Position', () => { 14 | describe('fromVSPosition', () => { 15 | it('creates a valid Position', () => { 16 | const { row, col } = generateRandomRowCol(); 17 | const tsPosition: TSPosition = { 18 | row, 19 | column: col, 20 | }; 21 | const position = Position.fromTSPosition(tsPosition); 22 | expect(position.row).to.eql(row); 23 | expect(position.col).to.eql(col); 24 | }); 25 | }); 26 | 27 | describe('fromTSPosition', () => { 28 | it('creates a valid Position', () => { 29 | const { row, col } = generateRandomRowCol(); 30 | const vsPosition: VSPosition = { 31 | line: row, 32 | character: col, 33 | }; 34 | const position = Position.fromVSPosition(vsPosition); 35 | expect(position.row).to.eql(row); 36 | expect(position.col).to.eql(col); 37 | }); 38 | }); 39 | 40 | describe('tsPositionIsEqual', () => {}); 41 | 42 | describe('constructor', () => { 43 | it('creates a new Position with the given row and col', () => { 44 | const { row, col } = generateRandomRowCol(); 45 | const position = new Position(row, col); 46 | expect(position.row).to.eql(row); 47 | expect(position.col).to.eql(col); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/Analyzer.ts: -------------------------------------------------------------------------------- 1 | import { DocumentSymbol, FoldingRange } from 'vscode-languageserver'; 2 | import log from 'loglevel'; 3 | import { Observer } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | import { Tree, SyntaxNode } from 'web-tree-sitter'; 6 | import DocumentSymbolAnalyzer from './analyzers/DocumentSymbolAnalyzer'; 7 | import { forest, forestStream, ForestEventKind } from './Forest'; 8 | import FoldingRangeAnalyzer from './analyzers/FoldingRangeAnalyzer'; 9 | 10 | interface Analysis { 11 | uri: string; 12 | foldingRanges?: FoldingRange[]; 13 | documentSymbols?: DocumentSymbol[]; 14 | } 15 | 16 | class Analyzer { 17 | private foldingRangeAnalyzer: FoldingRangeAnalyzer; 18 | private documentSymbolAnalyzer: DocumentSymbolAnalyzer; 19 | 20 | constructor(public uri: string) { 21 | this.foldingRangeAnalyzer = new FoldingRangeAnalyzer(forest.parser.getLanguage()); 22 | this.documentSymbolAnalyzer = new DocumentSymbolAnalyzer(); 23 | } 24 | 25 | get analysis(): Analysis { 26 | return { 27 | uri: this.uri, 28 | foldingRanges: this.foldingRangeAnalyzer.foldingRanges, 29 | documentSymbols: this.documentSymbolAnalyzer.symbols, 30 | }; 31 | } 32 | 33 | public analyze(tree: Tree): Analysis { 34 | this.foldingRangeAnalyzer.analyze(tree.rootNode); 35 | 36 | const cursor = tree.walk(); 37 | const walk = (depth: number): void => { 38 | this.analyzeNode(cursor.currentNode()); 39 | if (cursor.gotoFirstChild()) { 40 | do { 41 | walk(depth + 1); 42 | } while (cursor.gotoNextSibling()); 43 | cursor.gotoParent(); 44 | } 45 | }; 46 | walk(0); 47 | cursor.delete(); 48 | 49 | return this.analysis; 50 | } 51 | 52 | private analyzeNode(node: SyntaxNode): void { 53 | this.documentSymbolAnalyzer.analyze(node); 54 | } 55 | } 56 | 57 | class Analyses implements Observer { 58 | public closed: boolean; 59 | private analyses: Map; 60 | 61 | constructor() { 62 | this.closed = false; 63 | this.analyses = new Map(); 64 | } 65 | 66 | public next(analysis: Analysis): void { 67 | this.analyses.set(analysis.uri, analysis); 68 | } 69 | 70 | public error(err: any): void { 71 | log.error(err); 72 | } 73 | 74 | public complete(): void { 75 | this.closed = true; 76 | } 77 | 78 | public getAnalysis(uri: string): Analysis { 79 | return this.analyses.get(uri); 80 | } 81 | } 82 | 83 | export const analyses = new Analyses(); 84 | forestStream 85 | .pipe( 86 | map( 87 | ({ kind, document, tree }): Analysis => { 88 | if (kind === ForestEventKind.DELETE) { 89 | return { uri: document.uri }; 90 | } else { 91 | const analyzer = new Analyzer(document.uri); 92 | return analyzer.analyze(tree); 93 | } 94 | } 95 | ) 96 | ) 97 | .subscribe(analyses); 98 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/CapabilityCalculator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * CapabilityCalculator 3 | */ 4 | 5 | import { 6 | ClientCapabilities, 7 | ServerCapabilities, 8 | TextDocumentSyncKind, 9 | } from 'vscode-languageserver'; 10 | 11 | export class CapabilityCalculator { 12 | public clientCapabilities: ClientCapabilities; 13 | public capabilities: ServerCapabilities; 14 | 15 | constructor(clientCapabilities: ClientCapabilities) { 16 | this.clientCapabilities = clientCapabilities; 17 | this.calculateCapabilities(); 18 | } 19 | 20 | get supportsWorkspaceFolders(): boolean { 21 | return ( 22 | this.clientCapabilities.workspace && !!this.clientCapabilities.workspace.workspaceFolders 23 | ); 24 | } 25 | 26 | get supportsWorkspaceConfiguration(): boolean { 27 | return this.clientCapabilities.workspace && !!this.clientCapabilities.workspace.configuration; 28 | } 29 | 30 | private calculateCapabilities(): void { 31 | this.capabilities = { 32 | // Perform incremental syncs 33 | // Incremental sync is disabled for now due to not being able to get the 34 | // old text 35 | // textDocumentSync: TextDocumentSyncKind.Incremental, 36 | textDocumentSync: TextDocumentSyncKind.Full, 37 | documentFormattingProvider: true, 38 | documentRangeFormattingProvider: true, 39 | documentHighlightProvider: true, 40 | documentSymbolProvider: true, 41 | foldingRangeProvider: true, 42 | }; 43 | 44 | if (this.clientCapabilities.workspace && this.clientCapabilities.workspace.workspaceFolders) { 45 | this.capabilities.workspace = { 46 | workspaceFolders: { 47 | supported: true, 48 | changeNotifications: true, 49 | }, 50 | }; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/DocumentManager.ts: -------------------------------------------------------------------------------- 1 | import { TextDocuments, Connection, TextDocumentIdentifier } from 'vscode-languageserver'; 2 | import { TextDocument } from 'vscode-languageserver-textdocument'; 3 | import { Subject } from 'rxjs'; 4 | 5 | export enum DocumentEventKind { 6 | OPEN, 7 | CHANGE_CONTENT, 8 | CLOSE, 9 | } 10 | 11 | export interface DocumentEvent { 12 | kind: DocumentEventKind; 13 | document: TextDocument; 14 | } 15 | 16 | export default class DocumentManager { 17 | private readonly documents: TextDocuments; 18 | public subject: Subject; 19 | 20 | constructor() { 21 | this.documents = new TextDocuments(TextDocument); 22 | this.subject = new Subject(); 23 | 24 | this.documents.onDidOpen(this.emitDocumentEvent(DocumentEventKind.OPEN)); 25 | this.documents.onDidChangeContent(this.emitDocumentEvent(DocumentEventKind.CHANGE_CONTENT)); 26 | this.documents.onDidClose(this.emitDocumentEvent(DocumentEventKind.CLOSE)); 27 | } 28 | 29 | public get(id: TextDocumentIdentifier | string): TextDocument { 30 | const docId = typeof id === 'string' ? id : id.uri; 31 | return this.documents.get(docId); 32 | } 33 | 34 | public listen(connection: Connection): void { 35 | this.documents.listen(connection); 36 | } 37 | 38 | private emitDocumentEvent(kind: DocumentEventKind): ({ document: TextDocument }) => void { 39 | return ({ document }): void => { 40 | this.subject.next({ 41 | kind, 42 | document, 43 | }); 44 | }; 45 | } 46 | } 47 | 48 | export const documents = new DocumentManager(); 49 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/Forest.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Forest 3 | */ 4 | 5 | import Parser, { Tree } from 'web-tree-sitter'; 6 | import { TextDocument } from 'vscode-languageserver-textdocument'; 7 | import { of } from 'rxjs'; 8 | import { switchMap } from 'rxjs/operators'; 9 | import { documents, DocumentEvent, DocumentEventKind } from './DocumentManager'; 10 | import TreeSitterFactory from './util/TreeSitterFactory'; 11 | 12 | export interface IForest { 13 | getTree(uri: string): Tree; 14 | createTree(uri: string, content: string): Tree; 15 | updateTree(uri: string, content: string): Tree; 16 | deleteTree(uri: string): boolean; 17 | } 18 | 19 | export enum ForestEventKind { 20 | OPEN, 21 | UPDATE, 22 | DELETE, 23 | } 24 | 25 | export interface ForestEvent { 26 | kind: ForestEventKind; 27 | document: TextDocument; 28 | tree?: Tree; 29 | } 30 | 31 | class Forest implements IForest { 32 | public parser: Parser; 33 | private readonly trees: Map; 34 | 35 | constructor() { 36 | this.trees = new Map(); 37 | this.parser = TreeSitterFactory.build(); 38 | } 39 | 40 | public getTree(uri: string): Tree | undefined { 41 | return this.trees.get(uri); 42 | } 43 | 44 | public createTree(uri: string, content: string): Tree { 45 | const tree: Tree = this.parser.parse(content); 46 | this.trees.set(uri, tree); 47 | 48 | return tree; 49 | } 50 | 51 | // For the time being this is a full reparse for every change 52 | // Once we can support incremental sync we can use tree-sitter's 53 | // edit functionality 54 | public updateTree(uri: string, content: string): Tree { 55 | let tree: Tree = this.getTree(uri); 56 | if (tree !== undefined) { 57 | tree = this.parser.parse(content); 58 | this.trees.set(uri, tree); 59 | } else { 60 | tree = this.createTree(uri, content); 61 | } 62 | 63 | return tree; 64 | } 65 | 66 | public deleteTree(uri: string): boolean { 67 | const tree = this.getTree(uri); 68 | if (tree !== undefined) { 69 | tree.delete(); 70 | } 71 | return this.trees.delete(uri); 72 | } 73 | 74 | public release(): void { 75 | this.trees.forEach(tree => tree.delete()); 76 | this.parser.delete(); 77 | } 78 | } 79 | 80 | export const forest = new Forest(); 81 | export const forestStream = documents.subject.pipe( 82 | switchMap((event: DocumentEvent) => { 83 | const { kind, document } = event; 84 | const uri = document.uri; 85 | const forestEvent: ForestEvent = { 86 | document, 87 | kind: undefined, 88 | }; 89 | 90 | switch (kind) { 91 | case DocumentEventKind.OPEN: 92 | forestEvent.tree = forest.createTree(uri, document.getText()); 93 | forestEvent.kind = ForestEventKind.OPEN; 94 | break; 95 | case DocumentEventKind.CHANGE_CONTENT: 96 | forestEvent.tree = forest.updateTree(uri, document.getText()); 97 | forestEvent.kind = ForestEventKind.UPDATE; 98 | break; 99 | case DocumentEventKind.CLOSE: 100 | forest.deleteTree(uri); 101 | forestEvent.kind = ForestEventKind.DELETE; 102 | break; 103 | } 104 | 105 | return of(forestEvent); 106 | }) 107 | ); 108 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/Formatter.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { TextDocument } from 'vscode-languageserver-textdocument'; 3 | import { Range, TextDocumentIdentifier, TextEdit } from 'vscode-languageserver'; 4 | import { RubyEnvironment } from 'vscode-ruby-common'; 5 | import { 6 | documentConfigurationCache, 7 | workspaceRubyEnvironmentCache, 8 | RubyConfiguration, 9 | } from './SettingsCache'; 10 | import { documents } from './DocumentManager'; 11 | import { URI } from 'vscode-uri'; 12 | import { from, Observable } from 'rxjs'; 13 | import { mergeMap } from 'rxjs/operators'; 14 | import { 15 | IFormatter, 16 | FormatterConfig, 17 | NullFormatter, 18 | RuboCop, 19 | Standard, 20 | Rufo, 21 | RubyFMT, 22 | Prettier, 23 | } from './formatters'; 24 | 25 | const FORMATTER_MAP = { 26 | rubocop: RuboCop, 27 | standard: Standard, 28 | rufo: Rufo, 29 | rubyfmt: RubyFMT, 30 | prettier: Prettier, 31 | }; 32 | 33 | function getFormatter( 34 | document: TextDocument, 35 | env: RubyEnvironment, 36 | config: RubyConfiguration, 37 | range?: Range 38 | ): IFormatter { 39 | // Only format if we have a formatter to use and an execution root 40 | if (typeof config.format === 'string' && config.workspaceFolderUri) { 41 | const executionRoot = 42 | config.executionRoot.toLowerCase() === 'workspace root' 43 | ? URI.parse(config.workspaceFolderUri).fsPath 44 | : path.dirname(URI.parse(document.uri).fsPath); 45 | const formatterConfig: FormatterConfig = { 46 | env, 47 | executionRoot, 48 | config: { 49 | command: config.format, 50 | useBundler: config.useBundler, 51 | }, 52 | }; 53 | 54 | if (range) { 55 | formatterConfig.range = range; 56 | } 57 | 58 | return new FORMATTER_MAP[config.format](document, formatterConfig); 59 | } else { 60 | return new NullFormatter(); 61 | } 62 | } 63 | 64 | const Formatter = { 65 | format(ident: TextDocumentIdentifier, range?: Range): Observable { 66 | const document = documents.get(ident.uri); 67 | 68 | return from(documentConfigurationCache.get(ident.uri)).pipe( 69 | mergeMap(config => 70 | from(workspaceRubyEnvironmentCache.get(config.workspaceFolderUri)).pipe( 71 | mergeMap(env => { 72 | return getFormatter(document, env, config, range).format(); 73 | }) 74 | ) 75 | ) 76 | ); 77 | }, 78 | }; 79 | export default Formatter; 80 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/Linter.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { URI } from 'vscode-uri'; 3 | import { iif, from, forkJoin, of, Observable } from 'rxjs'; 4 | import { map, mergeMap, switchMap } from 'rxjs/operators'; 5 | import { Diagnostic } from 'vscode-languageserver'; 6 | import { TextDocument } from 'vscode-languageserver-textdocument'; 7 | import { 8 | documentConfigurationCache, 9 | workspaceRubyEnvironmentCache, 10 | RubyConfiguration, 11 | RubyCommandConfiguration, 12 | } from './SettingsCache'; 13 | import { RubyEnvironment } from 'vscode-ruby-common'; 14 | import { ILinter, LinterConfig, RuboCop, Reek, Standard, NullLinter } from './linters'; 15 | import { documents, DocumentEvent, DocumentEventKind } from './DocumentManager'; 16 | 17 | const LINTER_MAP = { 18 | rubocop: RuboCop, 19 | reek: Reek, 20 | standard: Standard, 21 | }; 22 | 23 | export interface LintResult { 24 | document: TextDocument; 25 | diagnostics: Diagnostic[]; 26 | error?: string; 27 | } 28 | 29 | function getLinter( 30 | name: string, 31 | document: TextDocument, 32 | env: RubyEnvironment, 33 | config: RubyConfiguration 34 | ): ILinter { 35 | if (!config.workspaceFolderUri) { 36 | return new NullLinter( 37 | `unable to lint ${document.uri} with ${name} as its workspace root folder could not be determined` 38 | ); 39 | } 40 | const linter = LINTER_MAP[name]; 41 | if (!linter) return new NullLinter(`attempted to lint with unsupported linter: ${name}`); 42 | const lintConfig: RubyCommandConfiguration = 43 | typeof config.lint[name] === 'object' ? config.lint[name] : {}; 44 | const executionRoot = 45 | lintConfig.executionRoot.toLowerCase() === 'workspace root' 46 | ? URI.parse(config.workspaceFolderUri).fsPath 47 | : path.dirname(URI.parse(document.uri).fsPath); 48 | const linterConfig: LinterConfig = { 49 | env, 50 | executionRoot, 51 | config: lintConfig, 52 | }; 53 | return new linter(document, linterConfig); // eslint-disable-line new-cap 54 | } 55 | 56 | function lint(document: TextDocument): Observable { 57 | return from(documentConfigurationCache.get(document)).pipe( 58 | mergeMap(config => { 59 | return from(workspaceRubyEnvironmentCache.get(config.workspaceFolderUri)).pipe( 60 | map(env => { 61 | return { config, env }; 62 | }) 63 | ); 64 | }), 65 | mergeMap(({ config, env }) => { 66 | const linters = Object.keys(config.lint) 67 | .filter(l => config.lint[l] !== false) 68 | .map(l => getLinter(l, document, env, config).lint()); 69 | return forkJoin(linters).pipe( 70 | map(diagnostics => { 71 | return { 72 | document, 73 | diagnostics: [].concat(...diagnostics), 74 | }; 75 | }) 76 | ); 77 | }) 78 | ); 79 | } 80 | 81 | export const linter = documents.subject.pipe( 82 | switchMap((event: DocumentEvent) => 83 | iif( 84 | () => 85 | event.kind === DocumentEventKind.OPEN || event.kind === DocumentEventKind.CHANGE_CONTENT, 86 | lint(event.document), 87 | of({ 88 | document: event.document, 89 | diagnostics: [], 90 | }) 91 | ) 92 | ) 93 | ); 94 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/Server.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ConfigurationItem, 3 | Connection, 4 | InitializeParams, 5 | InitializeResult, 6 | } from 'vscode-languageserver'; 7 | import log from 'loglevel'; 8 | 9 | import { CapabilityCalculator } from './CapabilityCalculator'; 10 | import DocumentHighlightProvider from './providers/DocumentHighlightProvider'; 11 | import FoldingRangeProvider from './providers/FoldingRangeProvider'; 12 | import ConfigurationProvider from './providers/ConfigurationProvider'; 13 | import WorkspaceProvider from './providers/WorkspaceProvider'; 14 | import DocumentSymbolProvider from './providers/DocumentSymbolProvider'; 15 | 16 | import { documents } from './DocumentManager'; 17 | import { LintResult, linter } from './Linter'; 18 | 19 | import { documentConfigurationCache, RubyConfiguration } from './SettingsCache'; 20 | import DocumentFormattingProvider from './providers/DocumentFormattingProvider'; 21 | import { forest } from './Forest'; 22 | 23 | export interface ILanguageServer { 24 | readonly capabilities: InitializeResult; 25 | initialize(); 26 | setup(); 27 | shutdown(); 28 | } 29 | 30 | export class Server implements ILanguageServer { 31 | public connection: Connection; 32 | private calculator: CapabilityCalculator; 33 | 34 | constructor(connection: Connection, params: InitializeParams) { 35 | this.connection = connection; 36 | this.calculator = new CapabilityCalculator(params.capabilities); 37 | 38 | documents.listen(connection); 39 | 40 | linter.subscribe({ 41 | next: (result: LintResult): void => { 42 | connection.sendDiagnostics({ uri: result.document.uri, diagnostics: result.diagnostics }); 43 | }, 44 | }); 45 | 46 | documentConfigurationCache.fetcher = async ( 47 | targets: string[] 48 | ): Promise => { 49 | const items: ConfigurationItem[] = targets.map(t => { 50 | return { 51 | scopeUri: t, 52 | section: 'ruby', 53 | }; 54 | }); 55 | return this.connection.workspace.getConfiguration(items); 56 | }; 57 | } 58 | 59 | get capabilities(): InitializeResult { 60 | return { 61 | capabilities: this.calculator.capabilities, 62 | }; 63 | } 64 | 65 | /** 66 | * Initialize should be run during the initialization phase of the client connection 67 | */ 68 | public initialize(): void { 69 | this.registerInitializeProviders(); 70 | } 71 | 72 | /** 73 | * Setup should be run after the client connection has been initialized. We can do things here like 74 | * handle changes to the workspace and query configuration settings 75 | */ 76 | public setup(): void { 77 | this.registerInitializedProviders(); 78 | this.loadGlobalConfig(); 79 | } 80 | 81 | public shutdown(): void { 82 | forest.release(); 83 | } 84 | 85 | // registers providers on the initialize step 86 | private registerInitializeProviders(): void { 87 | // Handles highlight requests 88 | DocumentHighlightProvider.register(this.connection); 89 | 90 | // Handles folding requests 91 | FoldingRangeProvider.register(this.connection); 92 | 93 | // Handles document symbol requests 94 | DocumentSymbolProvider.register(this.connection); 95 | 96 | // Handles document formatting requests 97 | DocumentFormattingProvider.register(this.connection); 98 | } 99 | 100 | // registers providers on the initialized step 101 | private registerInitializedProviders(): void { 102 | // Handle workspace changes 103 | WorkspaceProvider.register(this.connection); 104 | 105 | // Handle configuration change notifications 106 | if (this.calculator.supportsWorkspaceConfiguration) { 107 | ConfigurationProvider.register(this.connection, () => { 108 | this.loadGlobalConfig(); 109 | }); 110 | } 111 | } 112 | 113 | /** 114 | * Loads configuration from the client and sets up global language server configuration 115 | * 116 | * Over time we'll use this method to allow users to enable/disable specific features 117 | * if they are running this language server alongside another one: eg Solargraph or Sorbet 118 | */ 119 | private async loadGlobalConfig(): Promise { 120 | const config: RubyConfiguration = await this.connection.workspace.getConfiguration('ruby'); 121 | try { 122 | const { logLevel = 'info' } = config.languageServer; 123 | log.setLevel(logLevel); 124 | } catch (e) { 125 | log.error(e); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/SettingsCache.ts: -------------------------------------------------------------------------------- 1 | import { WorkspaceFolder } from 'vscode-languageserver'; 2 | import { TextDocument } from 'vscode-languageserver-textdocument'; 3 | import { URI } from 'vscode-uri'; 4 | import { loadEnv, RubyEnvironment } from 'vscode-ruby-common'; 5 | import { LogLevelDesc } from 'loglevel'; 6 | 7 | export interface RubyCommandConfiguration { 8 | command?: string; 9 | useBundler?: boolean; 10 | executionRoot?: 'file path' | 'workspace root'; 11 | } 12 | 13 | export interface RuboCopLintConfiguration extends RubyCommandConfiguration { 14 | lint?: boolean; 15 | only?: string[]; 16 | except?: string[]; 17 | require?: string[]; 18 | rails?: boolean; 19 | forceExclusion?: boolean; 20 | } 21 | 22 | export interface RubyConfiguration extends RubyCommandConfiguration { 23 | workspaceFolderUri: string; 24 | interpreter?: { 25 | commandPath?: string; 26 | }; 27 | pathToBundler: string; 28 | lint: { 29 | fasterer?: boolean | RubyConfiguration; 30 | reek?: boolean | RubyConfiguration; 31 | rubocop?: boolean | RuboCopLintConfiguration; 32 | }; 33 | format: boolean | 'rubocop' | 'standard' | 'rufo' | 'prettier'; 34 | languageServer: { 35 | logLevel: LogLevelDesc; 36 | }; 37 | } 38 | 39 | class SettingsCache

{ 40 | private readonly cache: Map; 41 | public fetcher: (target: string[]) => Promise; 42 | 43 | constructor() { 44 | this.cache = new Map(); 45 | } 46 | 47 | public set(target: P | string, env: T): void { 48 | const key = typeof target === 'string' ? target : target.uri; 49 | this.cache.set(key, env); 50 | } 51 | 52 | public setAll(targets: { [key: string]: T }): void { 53 | for (const target of Object.keys(targets)) { 54 | this.set(target, targets[target]); 55 | } 56 | } 57 | 58 | public delete(target: P): boolean { 59 | return this.cache.delete(target.uri); 60 | } 61 | 62 | public deleteAll(targets: P[]): void { 63 | for (const target of targets) { 64 | this.delete(target); 65 | } 66 | } 67 | 68 | public async get(target: P | string): Promise { 69 | if (!target) return undefined; 70 | const key = typeof target === 'string' ? target : target.uri; 71 | let settings: T = this.cache.get(key); 72 | if (!settings) { 73 | const result = await this.fetcher([key]); 74 | settings = result.length > 0 ? result[0] : undefined; 75 | 76 | if (settings) { 77 | this.set(key, settings); 78 | } 79 | } 80 | 81 | return settings; 82 | } 83 | 84 | public async getAll(targets: P[]): Promise<{ [key: string]: T }> { 85 | const settings: { [key: string]: T } = {}; 86 | 87 | for (const target of targets) { 88 | settings[target.uri] = await this.get(target); 89 | } 90 | 91 | return settings; 92 | } 93 | 94 | public flush(): void { 95 | this.cache.clear(); 96 | } 97 | 98 | public toString(): string { 99 | return this.cache.toString(); 100 | } 101 | } 102 | 103 | export const documentConfigurationCache = new SettingsCache(); 104 | export const workspaceRubyEnvironmentCache = new SettingsCache(); 105 | workspaceRubyEnvironmentCache.fetcher = async (folders: string[]): Promise => { 106 | return Promise.all(folders.map(async f => loadEnv(URI.parse(f).fsPath) as RubyEnvironment)); 107 | }; 108 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/analyzers/BaseAnalyzer.ts: -------------------------------------------------------------------------------- 1 | import { SyntaxNode } from 'web-tree-sitter'; 2 | 3 | export default abstract class BaseAnalyzer { 4 | public diagnostics: T[]; 5 | 6 | constructor() { 7 | this.diagnostics = [] as T[]; 8 | } 9 | 10 | public analyze(_node: SyntaxNode): void {} 11 | 12 | public flush(): void { 13 | this.diagnostics = [] as T[]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/analyzers/DocumentHighlightAnalyzer.ts: -------------------------------------------------------------------------------- 1 | import { DocumentHighlight, DocumentHighlightKind, Range } from 'vscode-languageserver'; 2 | import { SyntaxNode, Tree } from 'web-tree-sitter'; 3 | import Position from '../util/Position'; 4 | import { forest } from '../Forest'; 5 | 6 | export default class DocumentHighlightAnalyzer { 7 | private static readonly BEGIN_TYPES: Set = new Set([ 8 | 'begin', 9 | 'def', 10 | 'if', 11 | 'case', 12 | 'unless', 13 | 'do', 14 | 'class', 15 | 'module', 16 | ]); 17 | 18 | public static async analyze(uri: string, position: Position): Promise { 19 | const tree: Tree = forest.getTree(uri); 20 | 21 | return this.computeHighlights(tree, position); 22 | } 23 | 24 | private static computeHighlights(tree: Tree, position: Position): DocumentHighlight[] { 25 | const rootNode: SyntaxNode = tree.rootNode; 26 | const node: SyntaxNode = rootNode.descendantForPosition(position.toTSPosition()); 27 | let highlights: DocumentHighlight[] = []; 28 | if (node.type === 'end') { 29 | highlights = highlights.concat(this.computeEndHighlight(node)); 30 | } 31 | if (!node.isNamed && this.BEGIN_TYPES.has(node.type)) { 32 | highlights = highlights.concat(this.computeBeginHighlight(node)); 33 | } 34 | return highlights; 35 | } 36 | 37 | private static computeBeginHighlight(node: SyntaxNode): DocumentHighlight[] { 38 | const endNode: SyntaxNode = node.parent.lastChild; 39 | return [ 40 | DocumentHighlight.create( 41 | Range.create( 42 | Position.fromTSPosition(node.startPosition).toVSPosition(), 43 | Position.fromTSPosition(node.endPosition).toVSPosition() 44 | ), 45 | DocumentHighlightKind.Text 46 | ), 47 | DocumentHighlight.create( 48 | Range.create( 49 | Position.fromTSPosition(endNode.startPosition).toVSPosition(), 50 | Position.fromTSPosition(endNode.endPosition).toVSPosition() 51 | ), 52 | DocumentHighlightKind.Text 53 | ), 54 | ]; 55 | } 56 | private static computeEndHighlight(node: SyntaxNode): DocumentHighlight[] { 57 | const startNode: SyntaxNode = node.parent.firstChild; 58 | return [ 59 | DocumentHighlight.create( 60 | Range.create( 61 | Position.fromTSPosition(startNode.startPosition).toVSPosition(), 62 | Position.fromTSPosition(startNode.endPosition).toVSPosition() 63 | ), 64 | DocumentHighlightKind.Text 65 | ), 66 | DocumentHighlight.create( 67 | Range.create( 68 | Position.fromTSPosition(node.startPosition).toVSPosition(), 69 | Position.fromTSPosition(node.endPosition).toVSPosition() 70 | ), 71 | DocumentHighlightKind.Text 72 | ), 73 | ]; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/analyzers/DocumentSymbolAnalyzer.ts: -------------------------------------------------------------------------------- 1 | import { DocumentSymbol } from 'vscode-languageserver'; 2 | import { SyntaxNode } from 'web-tree-sitter'; 3 | import BaseAnalyzer from './BaseAnalyzer'; 4 | import { Position, Stack } from '../util'; 5 | import RubyDocumentSymbol, { isWrapper } from '../util/RubyDocumentSymbol'; 6 | 7 | export default class DocumentSymbolAnalyzer extends BaseAnalyzer { 8 | private readonly symbolStack: Stack; 9 | private readonly nodeStack: Stack; 10 | 11 | constructor() { 12 | super(); 13 | this.symbolStack = new Stack(); 14 | this.nodeStack = new Stack(); 15 | } 16 | 17 | get symbols(): DocumentSymbol[] { 18 | return this.diagnostics; 19 | } 20 | 21 | public analyze(node: SyntaxNode): void { 22 | const symbol = RubyDocumentSymbol.build(node); 23 | 24 | if (symbol) { 25 | // empty nodeStack means we are at document root 26 | if (this.nodeStack.empty()) { 27 | this.diagnostics = this.diagnostics.concat(symbol); 28 | } else { 29 | const topSymbol = this.symbolStack.peek(); 30 | topSymbol.children = topSymbol.children.concat(symbol); 31 | } 32 | } 33 | 34 | // Stack management 35 | if (isWrapper(node) && symbol && !Array.isArray(symbol)) { 36 | this.symbolStack.push(symbol); 37 | this.nodeStack.push(node); 38 | } else if ( 39 | !this.nodeStack.empty() && 40 | Position.tsPositionIsEqual(this.nodeStack.peek().endPosition, node.endPosition) 41 | ) { 42 | this.nodeStack.pop(); 43 | this.symbolStack.pop(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/analyzers/queries/folds.ts: -------------------------------------------------------------------------------- 1 | const FOLDS_QUERY = `[ 2 | (method) 3 | (singleton_method) 4 | (class) 5 | (module) 6 | (if) 7 | (else) 8 | (unless) 9 | (case) 10 | (when) 11 | (do_block) 12 | (singleton_class) 13 | (lambda) 14 | (comment) 15 | (heredoc_body) 16 | (begin) 17 | (rescue) 18 | (hash) 19 | (array) 20 | (call method: (identifier) @require (#match? @require "require")) 21 | ] @fold`; 22 | export default FOLDS_QUERY; 23 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/formatters/BaseFormatter.ts: -------------------------------------------------------------------------------- 1 | import { Position, Range, TextEdit } from 'vscode-languageserver'; 2 | import { TextDocument } from 'vscode-languageserver-textdocument'; 3 | import log from 'loglevel'; 4 | import { of, Observable, throwError } from 'rxjs'; 5 | import { catchError, map, reduce } from 'rxjs/operators'; 6 | import { 7 | diff_match_patch as DiffMatchPatch, 8 | Diff, 9 | DIFF_DELETE, 10 | DIFF_INSERT, 11 | DIFF_EQUAL, 12 | } from 'diff-match-patch'; 13 | import { RubyEnvironment } from 'vscode-ruby-common'; 14 | import { spawn } from '../util/spawn'; 15 | import { RubyCommandConfiguration } from '../SettingsCache'; 16 | 17 | export interface IFormatter { 18 | format(): Observable; 19 | } 20 | 21 | export interface FormatterConfig { 22 | env: RubyEnvironment; 23 | executionRoot: string; 24 | config: RubyCommandConfiguration; 25 | range?: Range; 26 | } 27 | 28 | export default abstract class BaseFormatter implements IFormatter { 29 | protected document: TextDocument; 30 | private readonly originalText: string; 31 | protected config: FormatterConfig; 32 | private readonly differ: DiffMatchPatch; 33 | 34 | constructor(document: TextDocument, config: FormatterConfig) { 35 | this.document = document; 36 | this.originalText = document.getText(); 37 | this.config = config; 38 | this.differ = new DiffMatchPatch(); 39 | 40 | if (this.range) { 41 | this.modifyRange(); 42 | } 43 | } 44 | 45 | get cmd(): string { 46 | return 'echo'; 47 | } 48 | 49 | get args(): string[] { 50 | return [this.document.uri]; 51 | } 52 | 53 | get useBundler(): boolean { 54 | return this.config.config.useBundler; 55 | } 56 | 57 | get range(): Range { 58 | return this.config.range; 59 | } 60 | 61 | public format(): Observable { 62 | let { cmd, args } = this; 63 | 64 | if (this.useBundler) { 65 | args.unshift('exec', cmd); 66 | cmd = 'bundle'; 67 | } 68 | 69 | const formatStr = `${cmd} ${args.join(' ')}`; 70 | log.info(`Format: executing ${formatStr}...`); 71 | return spawn(cmd, args, { 72 | env: this.config.env, 73 | cwd: this.config.executionRoot, 74 | stdin: of(this.originalText), 75 | }).pipe( 76 | catchError(error => { 77 | const err: Error | null = this.processError(error, formatStr); 78 | return err ? throwError(err) : of(''); 79 | }), 80 | reduce((acc: string, value: any) => { 81 | if (value.source === 'stdout') { 82 | return `${acc}${value.text}`; 83 | } else { 84 | log.error(value.text); 85 | return acc; 86 | } 87 | }, ''), 88 | map((result: string) => this.processResults(result)) 89 | ); 90 | } 91 | 92 | protected processResults(output: string): TextEdit[] { 93 | const diffs: Diff[] = this.differ.diff_main(this.originalText, output); 94 | const edits: TextEdit[] = []; 95 | // VSCode wants TextEdits on the original document 96 | // this means position only gets moved for DIFF_EQUAL and DIFF_DELETE 97 | // as insert is new and doesn't have a position in the original 98 | let position = 0; 99 | for (const diff of diffs) { 100 | const [num, str] = diff; 101 | const startPos = this.document.positionAt(position); 102 | 103 | switch (num) { 104 | case DIFF_DELETE: 105 | edits.push({ 106 | range: { 107 | start: startPos, 108 | end: this.document.positionAt(position + str.length), 109 | }, 110 | newText: '', 111 | }); 112 | position += str.length; 113 | break; 114 | case DIFF_INSERT: 115 | edits.push({ 116 | range: { start: startPos, end: startPos }, 117 | newText: str, 118 | }); 119 | break; 120 | case DIFF_EQUAL: 121 | position += str.length; 122 | break; 123 | } 124 | 125 | // If we have a range we are doing a selection format. Thus, 126 | // only apply patches that start within the selected range 127 | if (this.range && num !== DIFF_EQUAL && !this.checkPositionInRange(startPos)) { 128 | edits.pop(); 129 | } 130 | } 131 | 132 | return edits; 133 | } 134 | 135 | protected isWindows(): boolean { 136 | return process.platform === 'win32'; 137 | } 138 | 139 | protected processError(error: any, formatStr: string): Error { 140 | const code = error.code || error.toString().match(/code: (\d+)/)[1] || null; 141 | const message = `\n unable to execute ${formatStr}:\n ${error.toString()} - ${this.messageForCode( 142 | code 143 | )}`; 144 | return new Error(message); 145 | } 146 | 147 | protected messageForCode(code: string): string { 148 | switch (code) { 149 | case '127': 150 | return 'missing gem executables'; 151 | case 'ENOENT': 152 | return 'command not found'; 153 | default: 154 | return 'unknown error'; 155 | } 156 | } 157 | 158 | // Modified from https://github.com/Microsoft/vscode/blob/main/src/vs/editor/common/core/range.ts#L90 159 | private checkPositionInRange(position: Position): boolean { 160 | const { start, end } = this.range; 161 | 162 | if (position.line < start.line || position.line > end.line) { 163 | return false; 164 | } 165 | 166 | if (position.line === start.line && position.character < start.character) { 167 | return false; 168 | } 169 | 170 | if (position.line === end.line && position.character > end.character) { 171 | return false; 172 | } 173 | 174 | return true; 175 | } 176 | 177 | // If the selection range just has whitespace before it in the line, 178 | // extend the range to account for that whitespace 179 | private modifyRange(): void { 180 | const { start } = this.range; 181 | const offset = this.document.offsetAt(start); 182 | const prefixLineText = this.originalText.substring(offset - start.character, offset); 183 | 184 | if (/^\s+$/.test(prefixLineText)) { 185 | this.range.start.character = 0; 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/formatters/NullFormatter.ts: -------------------------------------------------------------------------------- 1 | import { IFormatter } from './BaseFormatter'; 2 | import { Observable, of } from 'rxjs'; 3 | import { TextEdit } from 'vscode-languageserver'; 4 | 5 | export default class NullFormatter implements IFormatter { 6 | format(): Observable { 7 | return of([]); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/formatters/Prettier.ts: -------------------------------------------------------------------------------- 1 | import cp from 'child_process'; 2 | import { URI } from 'vscode-uri'; 3 | import BaseFormatter from './BaseFormatter'; 4 | 5 | enum PrettierKind { 6 | RbPrettier = 'rbprettier', 7 | Prettier = 'prettier', 8 | } 9 | 10 | export default class Prettier extends BaseFormatter { 11 | private _cmd: string = ''; 12 | 13 | get cmd(): string { 14 | if (this._cmd === '') return this.buildCmd(); 15 | return this._cmd; 16 | } 17 | 18 | get args(): string[] { 19 | const documentPath = URI.parse(this.document.uri); 20 | return [`${documentPath.fsPath}`]; 21 | } 22 | 23 | private buildCmd(): string { 24 | let prettierExecutable = this.getPrettierExecutable(); 25 | if (prettierExecutable === PrettierKind.RbPrettier) { 26 | this._cmd = prettierExecutable; 27 | return prettierExecutable; 28 | } 29 | 30 | prettierExecutable = this.isWindows() ? prettierExecutable + '.bat' : prettierExecutable; 31 | this._cmd = prettierExecutable; 32 | return prettierExecutable; 33 | } 34 | 35 | private getPrettierExecutable(): string { 36 | let args = []; 37 | if (this.useBundler) { 38 | args = [...args, 'bundle', 'exec']; 39 | } 40 | args = [...args, 'rbprettier', '--version']; 41 | const rbPrettierCommand = args.shift(); 42 | const rbPrettierProcess = cp.spawnSync(rbPrettierCommand, args); 43 | if (rbPrettierProcess.stderr.length !== 0) return PrettierKind.Prettier; 44 | 45 | return PrettierKind.RbPrettier; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/formatters/RuboCop.ts: -------------------------------------------------------------------------------- 1 | import { URI } from 'vscode-uri'; 2 | import { TextEdit } from 'vscode-languageserver'; 3 | import BaseFormatter from './BaseFormatter'; 4 | 5 | export default class RuboCop extends BaseFormatter { 6 | protected FORMATTED_OUTPUT_DELIMITER = '===================='; 7 | protected FORMATTED_OUTPUT_REGEX = new RegExp(`^${this.FORMATTED_OUTPUT_DELIMITER}$`, 'm'); 8 | 9 | get cmd(): string { 10 | const command = 'rubocop'; 11 | return this.isWindows() ? command + '.bat' : command; 12 | } 13 | 14 | get args(): string[] { 15 | const documentPath = URI.parse(this.document.uri); 16 | const args = ['-s', `'${documentPath.fsPath}'`, '-a']; 17 | return args; 18 | } 19 | 20 | protected processResults(output: string): TextEdit[] { 21 | let endOfDiagnostics = output.search(this.FORMATTED_OUTPUT_REGEX); 22 | if (endOfDiagnostics <= -1) { 23 | throw new Error(output); 24 | } 25 | endOfDiagnostics += this.FORMATTED_OUTPUT_DELIMITER.length; 26 | const cleanOutput = output.substring(endOfDiagnostics).trimLeft(); 27 | return super.processResults(cleanOutput); 28 | } 29 | 30 | protected processError(error: any, formatStr: string): Error { 31 | const code = error.code || error.toString().match(/code: (\d+)/)[1] || null; 32 | if (code === '1') return null; 33 | return super.processError(error, formatStr); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/formatters/RubyFMT.ts: -------------------------------------------------------------------------------- 1 | import BaseFormatter from './BaseFormatter'; 2 | 3 | export default class RubyFMT extends BaseFormatter { 4 | get cmd(): string { 5 | return 'rubyfmt'; 6 | } 7 | 8 | get args(): string[] { 9 | return []; 10 | } 11 | 12 | get useBundler(): boolean { 13 | return false; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/formatters/Rufo.ts: -------------------------------------------------------------------------------- 1 | import { URI } from 'vscode-uri'; 2 | import BaseFormatter from './BaseFormatter'; 3 | 4 | export default class Rubocop extends BaseFormatter { 5 | get cmd(): string { 6 | const command = 'rufo'; 7 | return this.isWindows() ? command + '.bat' : command; 8 | } 9 | 10 | get args(): string[] { 11 | const documentPath = URI.parse(this.document.uri); 12 | return [`--filename='${documentPath.fsPath}'`, '-x']; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/formatters/Standard.ts: -------------------------------------------------------------------------------- 1 | import { URI } from 'vscode-uri'; 2 | import RuboCop from './RuboCop'; 3 | 4 | export default class Standard extends RuboCop { 5 | get cmd(): string { 6 | const command = 'standardrb'; 7 | return this.isWindows() ? command + '.bat' : command; 8 | } 9 | 10 | get args(): string[] { 11 | const documentPath = URI.parse(this.document.uri); 12 | const args = ['-s', `'${documentPath.fsPath}'`, '--fix']; 13 | return args; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/formatters/index.ts: -------------------------------------------------------------------------------- 1 | export { IFormatter, FormatterConfig } from './BaseFormatter'; 2 | export { default as NullFormatter } from './NullFormatter'; 3 | export { default as RuboCop } from './RuboCop'; 4 | export { default as Rufo } from './Rufo'; 5 | export { default as Standard } from './Standard'; 6 | export { default as RubyFMT } from './RubyFMT'; 7 | export { default as Prettier } from './Prettier'; 8 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * LSP server for vscode-ruby 3 | */ 4 | 5 | import { 6 | createConnection, 7 | Connection, 8 | InitializeParams, 9 | ProposedFeatures, 10 | } from 'vscode-languageserver'; 11 | import log from 'loglevel'; 12 | 13 | import { ILanguageServer } from './Server'; 14 | import TreeSitterFactory from './util/TreeSitterFactory'; 15 | 16 | const connection: Connection = createConnection(ProposedFeatures.all); 17 | let server: ILanguageServer; 18 | 19 | connection.onInitialize(async (params: InitializeParams) => { 20 | log.setDefaultLevel('info'); 21 | log.info('Initializing Ruby language server...'); 22 | 23 | await TreeSitterFactory.initalize(); 24 | 25 | const { Server } = await import('./Server'); 26 | server = new Server(connection, params); 27 | server.initialize(); 28 | 29 | return server.capabilities; 30 | }); 31 | 32 | connection.onInitialized(() => { 33 | server.setup(); 34 | }); 35 | 36 | connection.onShutdown(() => server.shutdown()); 37 | connection.onExit(() => server.shutdown()); 38 | 39 | // Listen on the connection 40 | connection.listen(); 41 | 42 | // Don't die on unhandled Promise rejections 43 | process.on('unhandledRejection', (reason, p) => { 44 | log.error(`Unhandled Rejection at: Promise ${p} reason:, ${reason}`); 45 | }); 46 | 47 | // Don't die when attempting to pipe stdin to a bad spawn 48 | // https://github.com/electron/electron/issues/13254 49 | process.on('SIGPIPE', () => { 50 | log.error('SIGPIPE received'); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/linters/BaseLinter.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostic } from 'vscode-languageserver'; 2 | import { TextDocument } from 'vscode-languageserver-textdocument'; 3 | import log from 'loglevel'; 4 | import { spawn } from '../util/spawn'; 5 | import { EMPTY, of, Observable } from 'rxjs'; 6 | import { catchError, map, reduce } from 'rxjs/operators'; 7 | import { RubyCommandConfiguration } from '../SettingsCache'; 8 | import { IEnvironment } from 'vscode-ruby-common'; 9 | 10 | export interface ILinter { 11 | lint(): Observable; 12 | } 13 | 14 | export interface LinterConfig { 15 | env: IEnvironment; 16 | executionRoot: string; 17 | config: RubyCommandConfiguration; 18 | } 19 | 20 | export default abstract class BaseLinter implements ILinter { 21 | protected document: TextDocument; 22 | protected config: LinterConfig; 23 | protected code = 'BaseLinter'; 24 | 25 | constructor(document: TextDocument, config: LinterConfig) { 26 | this.document = document; 27 | this.config = config; 28 | } 29 | 30 | get cmd(): string { 31 | return this.lintConfig.command; 32 | } 33 | 34 | get args(): string[] { 35 | return [this.document.uri]; 36 | } 37 | 38 | get lintConfig(): RubyCommandConfiguration { 39 | return this.config.config; 40 | } 41 | 42 | public lint(): Observable { 43 | let { cmd, args } = this; 44 | 45 | if (!this.lintConfig.command && this.lintConfig.useBundler) { 46 | args.unshift('exec', cmd); 47 | cmd = 'bundle'; 48 | } 49 | 50 | log.info(`Lint: executing ${cmd} ${args.join(' ')}...`); 51 | log.debug(`Lint environment: ${JSON.stringify(this.config.env, null, 2)}`); 52 | return spawn(cmd, args, { 53 | env: this.config.env, 54 | cwd: this.config.executionRoot, 55 | stdin: of(this.document.getText()), 56 | }).pipe( 57 | catchError(error => { 58 | this.processError(error); 59 | return EMPTY; 60 | }), 61 | reduce((acc: string, value: any) => { 62 | if (value.source === 'stdout') { 63 | return `${acc}${value.text}`; 64 | } else { 65 | log.error(value.text); 66 | return acc; 67 | } 68 | }, ''), 69 | map((result: string) => { 70 | return result.length ? this.processResults(result) : []; 71 | }) 72 | ); 73 | } 74 | 75 | protected processResults(_output): Diagnostic[] { 76 | return []; 77 | } 78 | 79 | protected isWindows(): boolean { 80 | return process.platform === 'win32'; 81 | } 82 | 83 | protected processError(error: any): void { 84 | switch (error.code) { 85 | case 'ENOENT': 86 | log.warn( 87 | `Lint: unable to execute ${error.path} ${error.spawnargs.join( 88 | ' ' 89 | )} as the command could not be found` 90 | ); 91 | break; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/linters/NullLinter.ts: -------------------------------------------------------------------------------- 1 | import { ILinter } from './BaseLinter'; 2 | import { Observable, of } from 'rxjs'; 3 | import { Diagnostic } from 'vscode-languageserver'; 4 | import log from 'loglevel'; 5 | 6 | export default class NullLinter implements ILinter { 7 | private readonly msg: string; 8 | 9 | constructor(msg: string) { 10 | this.msg = msg; 11 | } 12 | 13 | lint(): Observable { 14 | log.warn(`Lint: ${this.msg}`); 15 | return of([]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/linters/Reek.ts: -------------------------------------------------------------------------------- 1 | import { URI } from 'vscode-uri'; 2 | import BaseLinter from './BaseLinter'; 3 | import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver'; 4 | import log from 'loglevel'; 5 | 6 | interface ReekOffense { 7 | context: string; 8 | lines: number[]; 9 | message: string; 10 | smell_type: string; 11 | source: string; 12 | depth: number; 13 | wiki_link: string; 14 | } 15 | 16 | export default class Reek extends BaseLinter { 17 | protected code = 'Reek'; 18 | 19 | get cmd(): string { 20 | if (this.lintConfig.command) { 21 | return this.lintConfig.command; 22 | } else { 23 | const command = 'reek'; 24 | return this.isWindows() ? command + '.bat' : command; 25 | } 26 | } 27 | 28 | get args(): string[] { 29 | const documentPath = URI.parse(this.document.uri); 30 | return ['-f', 'json', '--stdin-filename', documentPath.fsPath]; 31 | } 32 | 33 | protected processResults(data): Diagnostic[] { 34 | let results = [] as Diagnostic[]; 35 | try { 36 | const offenses: ReekOffense[] = JSON.parse(data); 37 | for (const offense of offenses) { 38 | const diagnostics = this.reekOffenseToDiagnostic(offense); 39 | results = results.concat(diagnostics); 40 | } 41 | } catch (e) { 42 | log.error(`Lint: Received invalid JSON from reek:\n\n${data}`); 43 | } 44 | 45 | return results; 46 | } 47 | 48 | private reekOffenseToDiagnostic(offense: ReekOffense): Diagnostic[] { 49 | const baseDiagnostic = { 50 | severity: DiagnosticSeverity.Warning, 51 | message: offense.message, 52 | source: offense.smell_type, 53 | code: this.code, 54 | }; 55 | return offense.lines.map(l => { 56 | return { 57 | ...baseDiagnostic, 58 | range: { 59 | start: { 60 | line: l, 61 | character: 1, 62 | }, 63 | end: { 64 | line: l, 65 | character: 1, 66 | }, 67 | }, 68 | }; 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/linters/RuboCop.ts: -------------------------------------------------------------------------------- 1 | import { URI } from 'vscode-uri'; 2 | import { Diagnostic, DiagnosticSeverity, Range } from 'vscode-languageserver'; 3 | import log from 'loglevel'; 4 | import BaseLinter from './BaseLinter'; 5 | import { RuboCopLintConfiguration } from '../SettingsCache'; 6 | 7 | interface RuboCopOffenseLocation { 8 | start_line: number; 9 | start_column: number; 10 | last_line: number; 11 | last_column: number; 12 | line: number; 13 | column: number; 14 | length: number; 15 | } 16 | 17 | interface RuboCopOffense { 18 | severity: string; // FIXME make this a fixed option set 19 | message: string; 20 | cop_name: string; 21 | corrected: boolean; 22 | location: RuboCopOffenseLocation; 23 | } 24 | 25 | export interface IRuboCopResults { 26 | summary: { 27 | offense_count: number; 28 | target_file_count: number; 29 | inspected_file_count: number; 30 | }; 31 | files: [ 32 | { 33 | path: string; 34 | offenses: RuboCopOffense[]; 35 | } 36 | ]; 37 | } 38 | 39 | export default class RuboCop extends BaseLinter { 40 | protected code = 'RuboCop'; 41 | 42 | private readonly DIAGNOSTIC_SEVERITIES = { 43 | refactor: DiagnosticSeverity.Hint, 44 | convention: DiagnosticSeverity.Information, 45 | warning: DiagnosticSeverity.Warning, 46 | error: DiagnosticSeverity.Error, 47 | fatal: DiagnosticSeverity.Error, 48 | }; 49 | 50 | get cmd(): string { 51 | if (this.lintConfig.command) { 52 | return this.lintConfig.command; 53 | } else { 54 | const command = 'rubocop'; 55 | return this.isWindows() ? command + '.bat' : command; 56 | } 57 | } 58 | 59 | get args(): string[] { 60 | const documentPath = URI.parse(this.document.uri); 61 | let args = ['-s', documentPath.fsPath, '-f', 'json']; 62 | if (this.lintConfig.rails) args.push('-R'); 63 | if (this.lintConfig.forceExclusion) args.push('--force-exclusion'); 64 | if (this.lintConfig.lint) args.push('-l'); 65 | if (this.lintConfig.only) args = args.concat('--only', this.lintConfig.only.join(',')); 66 | if (this.lintConfig.except) args = args.concat('--except', this.lintConfig.except.join(',')); 67 | if (this.lintConfig.require) args = args.concat('-r', this.lintConfig.require.join(',')); 68 | return args; 69 | } 70 | 71 | get lintConfig(): RuboCopLintConfiguration { 72 | return this.config.config; 73 | } 74 | 75 | protected processResults(data: string): Diagnostic[] { 76 | let results = [] as Diagnostic[]; 77 | try { 78 | const offenses: IRuboCopResults = JSON.parse(data); 79 | 80 | for (const file of offenses.files) { 81 | const diagnostics = file.offenses.map(o => this.rubocopOffenseToDiagnostic(o)); 82 | results = results.concat(diagnostics); 83 | } 84 | } catch (e) { 85 | log.error(`Lint: Received invalid JSON from rubocop:\n\n${data}`); 86 | } 87 | 88 | return results; 89 | } 90 | 91 | protected rubocopOffenseToDiagnostic(offense: RuboCopOffense): Diagnostic { 92 | const range = this.mapRubocopOffenseToRange(offense.location); 93 | return { 94 | range, 95 | severity: this.DIAGNOSTIC_SEVERITIES[offense.severity], 96 | message: offense.message, 97 | source: offense.cop_name, 98 | code: this.code, 99 | }; 100 | } 101 | 102 | private mapRubocopOffenseToRange(location: RuboCopOffenseLocation): Range { 103 | // RuboCop >= v0.52.0 104 | if (location.start_line) { 105 | return { 106 | start: { 107 | line: location.start_line - 1, 108 | character: location.start_column - 1, 109 | }, 110 | end: { 111 | line: location.last_line - 1, 112 | character: location.last_column, 113 | }, 114 | }; 115 | } else { 116 | const line = location.line - 1; 117 | const offenseCharacter = location.column - 1; 118 | 119 | return { 120 | start: { 121 | line, 122 | character: offenseCharacter, 123 | }, 124 | end: { 125 | line, 126 | character: offenseCharacter + location.length, 127 | }, 128 | }; 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/linters/Standard.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostic } from 'vscode-languageserver'; 2 | import log from 'loglevel'; 3 | import RuboCop, { IRuboCopResults } from './RuboCop'; 4 | 5 | export default class Standard extends RuboCop { 6 | protected code = 'Standard'; 7 | 8 | get cmd(): string { 9 | if (this.lintConfig.command) { 10 | return this.lintConfig.command; 11 | } else { 12 | const command = 'standardrb'; 13 | return this.isWindows() ? command + '.bat' : command; 14 | } 15 | } 16 | 17 | get args(): string[] { 18 | const args = super.args; 19 | args.push('--no-fix'); 20 | return args; 21 | } 22 | 23 | // This method is overridden to deal with the "notice" that is 24 | // currently output 25 | protected processResults(data): Diagnostic[] { 26 | const lastCurly = data.lastIndexOf('}') + 1; 27 | let results = [] as Diagnostic[]; 28 | try { 29 | const offenses: IRuboCopResults = JSON.parse(data.substring(0, lastCurly)); 30 | 31 | for (const file of offenses.files) { 32 | const diagnostics = file.offenses.map(o => this.rubocopOffenseToDiagnostic(o)); 33 | results = results.concat(diagnostics); 34 | } 35 | } catch (e) { 36 | log.error(`Lint: Received invalid JSON from standardrb:\n\n${data}`); 37 | } 38 | 39 | return results; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/linters/index.ts: -------------------------------------------------------------------------------- 1 | export { ILinter, LinterConfig } from './BaseLinter'; 2 | export { default as NullLinter } from './NullLinter'; 3 | export { default as RuboCop } from './RuboCop'; 4 | export { default as Reek } from './Reek'; 5 | export { default as Standard } from './Standard'; 6 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/providers/ConfigurationProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | DidChangeConfigurationParams, 4 | DidChangeConfigurationNotification, 5 | } from 'vscode-languageserver'; 6 | import Provider from './Provider'; 7 | import { documentConfigurationCache } from '../SettingsCache'; 8 | 9 | export default class ConfigurationProvider extends Provider { 10 | private configChangeCallback: () => void; 11 | 12 | static register(connection: Connection, callback?: () => void): ConfigurationProvider { 13 | return new ConfigurationProvider(connection, callback); 14 | } 15 | 16 | constructor(connection: Connection, callback?: () => void) { 17 | super(connection); 18 | 19 | this.configChangeCallback = callback || (() => {}); 20 | 21 | this.connection.client.register(DidChangeConfigurationNotification.type, undefined); 22 | this.connection.onDidChangeConfiguration(this.handleDidChangeConfiguration); 23 | } 24 | 25 | private handleDidChangeConfiguration = async ( 26 | _params: DidChangeConfigurationParams // params is empty in the pull config model 27 | ): Promise => { 28 | documentConfigurationCache.flush(); 29 | this.configChangeCallback(); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/providers/DocumentFormattingProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | DocumentFormattingParams, 4 | DocumentRangeFormattingParams, 5 | TextEdit, 6 | } from 'vscode-languageserver'; 7 | import Provider from './Provider'; 8 | import Formatter from '../Formatter'; 9 | 10 | export default class DocumentFormattingProvider extends Provider { 11 | static register(connection: Connection): DocumentFormattingProvider { 12 | return new DocumentFormattingProvider(connection); 13 | } 14 | 15 | constructor(connection: Connection) { 16 | super(connection); 17 | this.connection.onDocumentFormatting(this.handleDocumentFormattingRequest); 18 | this.connection.onDocumentRangeFormatting(this.handleDocumentRangeFormattingRequest); 19 | } 20 | 21 | private handleDocumentFormattingRequest = async ( 22 | params: DocumentFormattingParams 23 | ): Promise => { 24 | const { textDocument } = params; 25 | 26 | return Formatter.format(textDocument).toPromise(); 27 | }; 28 | 29 | private handleDocumentRangeFormattingRequest = async ( 30 | params: DocumentRangeFormattingParams 31 | ): Promise => { 32 | const { textDocument, range } = params; 33 | 34 | return Formatter.format(textDocument, range).toPromise(); 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/providers/DocumentHighlightProvider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DocumentHighlightProvider 3 | * 4 | * Super basic highlight provider 5 | */ 6 | 7 | import { DocumentHighlight, Connection, TextDocumentPositionParams } from 'vscode-languageserver'; 8 | import Position from '../util/Position'; 9 | import Provider from './Provider'; 10 | import DocumentHighlightAnalyzer from '../analyzers/DocumentHighlightAnalyzer'; 11 | 12 | // TODO support more highlight use cases than just balanced pairs 13 | 14 | export default class DocumentHighlightProvider extends Provider { 15 | static register(connection: Connection): DocumentHighlightProvider { 16 | return new DocumentHighlightProvider(connection); 17 | } 18 | 19 | constructor(connection: Connection) { 20 | super(connection); 21 | this.connection.onDocumentHighlight(this.handleDocumentHighlight); 22 | } 23 | 24 | protected handleDocumentHighlight = async ( 25 | params: TextDocumentPositionParams 26 | ): Promise => { 27 | const position: Position = Position.fromVSPosition(params.position); 28 | const { 29 | textDocument: { uri }, 30 | } = params; 31 | return DocumentHighlightAnalyzer.analyze(uri, position); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/providers/DocumentSymbolProvider.ts: -------------------------------------------------------------------------------- 1 | import { DocumentSymbol, DocumentSymbolParams, Connection } from 'vscode-languageserver'; 2 | import Provider from './Provider'; 3 | import { analyses } from '../Analyzer'; 4 | 5 | export default class DocumentSymbolProvider extends Provider { 6 | static register(connection: Connection): DocumentSymbolProvider { 7 | return new DocumentSymbolProvider(connection); 8 | } 9 | 10 | constructor(connection: Connection) { 11 | super(connection); 12 | this.connection.onDocumentSymbol(this.handleDocumentSymbol); 13 | } 14 | 15 | private handleDocumentSymbol = async ( 16 | params: DocumentSymbolParams 17 | ): Promise => { 18 | const { 19 | textDocument: { uri }, 20 | } = params; 21 | const analysis = analyses.getAnalysis(uri); 22 | return analysis.documentSymbols; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/providers/FoldingRangeProvider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * FoldingRangeProvider 3 | */ 4 | 5 | import { 6 | FoldingRange, 7 | FoldingRangeRequest, 8 | FoldingRangeParams, 9 | Connection, 10 | } from 'vscode-languageserver'; 11 | import Provider from './Provider'; 12 | import { analyses } from '../Analyzer'; 13 | 14 | export default class FoldingRangeProvider extends Provider { 15 | static register(connection: Connection): FoldingRangeProvider { 16 | return new FoldingRangeProvider(connection); 17 | } 18 | 19 | constructor(connection: Connection) { 20 | super(connection); 21 | 22 | this.connection.onRequest(FoldingRangeRequest.type, this.handleFoldingRange); 23 | } 24 | 25 | protected handleFoldingRange = async (params: FoldingRangeParams): Promise => { 26 | const { 27 | textDocument: { uri }, 28 | } = params; 29 | const analysis = analyses.getAnalysis(uri); 30 | return analysis.foldingRanges; 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/providers/Provider.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from 'vscode-languageserver'; 2 | 3 | export default abstract class Provider { 4 | protected connection: Connection; 5 | 6 | constructor(connection: Connection) { 7 | this.connection = connection; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/providers/WorkspaceProvider.ts: -------------------------------------------------------------------------------- 1 | import Provider from './Provider'; 2 | import { 3 | DidChangeWatchedFilesParams, 4 | Connection, 5 | WorkspaceFoldersChangeEvent, 6 | } from 'vscode-languageserver'; 7 | import log from 'loglevel'; 8 | 9 | import { workspaceRubyEnvironmentCache } from '../SettingsCache'; 10 | 11 | export default class WorkspaceProvider extends Provider { 12 | public static register(connection: Connection): WorkspaceProvider { 13 | return new WorkspaceProvider(connection); 14 | } 15 | 16 | constructor(connection: Connection) { 17 | super(connection); 18 | 19 | this.connection.workspace.onDidChangeWorkspaceFolders(this.handleWorkspaceFoldersChange); 20 | this.connection.onDidChangeWatchedFiles(this.handleDidChangeWatchedFiles); 21 | } 22 | 23 | private handleWorkspaceFoldersChange = async ( 24 | event: WorkspaceFoldersChangeEvent 25 | ): Promise => { 26 | const loader = workspaceRubyEnvironmentCache.getAll(event.added); 27 | const remover = workspaceRubyEnvironmentCache.deleteAll(event.removed); 28 | await Promise.all([loader, remover]); 29 | }; 30 | 31 | private handleDidChangeWatchedFiles = async ( 32 | params: DidChangeWatchedFilesParams 33 | ): Promise => { 34 | log.info('Watched file change!'); 35 | log.info(params); 36 | // TODO load workspace environment again based on workspace where the file changed 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/util/Position.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Position class 3 | * 4 | * This class supports converting to/from VSCode and TreeSitter positions 5 | */ 6 | 7 | import { Point as TSPosition } from 'web-tree-sitter'; 8 | import { Position as VSPosition } from 'vscode-languageserver'; 9 | 10 | export default class Position { 11 | public row: number; 12 | public col: number; 13 | 14 | constructor(row: number, col: number) { 15 | this.row = row; 16 | this.col = col; 17 | } 18 | 19 | public static fromVSPosition(position: VSPosition): Position { 20 | return new Position(position.line, position.character); 21 | } 22 | 23 | public static fromTSPosition(position: TSPosition): Position { 24 | return new Position(position.row, position.column); 25 | } 26 | 27 | public static tsPositionIsEqual(p1: TSPosition, p2: TSPosition): boolean { 28 | return p1.row === p2.row && p1.column === p2.column; 29 | } 30 | 31 | public toVSPosition(): VSPosition { 32 | return VSPosition.create(this.row, this.col); 33 | } 34 | 35 | public toTSPosition(): TSPosition { 36 | return { 37 | row: this.row, 38 | column: this.col, 39 | }; 40 | } 41 | 42 | public isEqual(position: Position) { 43 | return this.row === position.row && this.col === position.col; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/util/RubyDocumentSymbol.ts: -------------------------------------------------------------------------------- 1 | import { DocumentSymbol, Range, SymbolKind } from 'vscode-languageserver'; 2 | import { SyntaxNode } from 'web-tree-sitter'; 3 | import Position from './Position'; 4 | 5 | const SYMBOLKINDS = { 6 | singleton_method: SymbolKind.Method, 7 | method: SymbolKind.Method, 8 | class: SymbolKind.Class, 9 | module: SymbolKind.Module, 10 | assignment: SymbolKind.Constant, 11 | method_call: SymbolKind.Property, 12 | }; 13 | 14 | const IDENTIFIER_NODES = { 15 | module: 'constant', 16 | class: 'constant', 17 | method: 'identifier', 18 | singleton_method: 'identifier', 19 | assignment: 'constant', 20 | method_call: 'identifier', 21 | }; 22 | 23 | export function isWrapper(node: SyntaxNode): boolean { 24 | return IDENTIFIER_NODES.hasOwnProperty(node.type); 25 | } 26 | 27 | const RubyDocumentSymbol = { 28 | build(node: SyntaxNode): DocumentSymbol | DocumentSymbol[] | void { 29 | const symbolKind = SYMBOLKINDS[node.type]; 30 | if (!symbolKind) return; 31 | 32 | const symbol = DocumentSymbol.create(null, null, null, null, null); 33 | symbol.range = Range.create( 34 | Position.fromTSPosition(node.startPosition).toVSPosition(), 35 | Position.fromTSPosition(node.endPosition).toVSPosition() 36 | ); 37 | symbol.kind = symbolKind; 38 | 39 | if (isWrapper(node)) { 40 | if (!node.childCount) return; 41 | // Handle foo = Foo::Bar::Baz.bam showing Foo in the outline 42 | if (node.type === 'assignment' && node.firstChild.type === 'identifier') return; 43 | const identifierNode = node.descendantsOfType(IDENTIFIER_NODES[node.type])[0]; 44 | if (identifierNode) { 45 | symbol.children = []; 46 | symbol.name = identifierNode.text; 47 | 48 | // Prepend self. to class methods 49 | if (node.type === 'singleton_method') { 50 | symbol.name = `self.${identifierNode.text}`; 51 | } 52 | 53 | // Override constructor type 54 | if (symbol.name === 'initialize') { 55 | symbol.kind = SymbolKind.Constructor; 56 | } 57 | 58 | // detect attr_ method calls 59 | if (symbol.name.indexOf('attr_') === 0) { 60 | const argumentList = node.descendantsOfType('argument_list')[0]; 61 | const symbols = []; 62 | for (const child of argumentList.namedChildren) { 63 | const newSymbol = { 64 | ...symbol, 65 | }; 66 | newSymbol.name = child.text[0] === ':' ? child.text.substring(1) : child.text; 67 | newSymbol.selectionRange = Range.create( 68 | Position.fromTSPosition(child.startPosition).toVSPosition(), 69 | Position.fromTSPosition(child.endPosition).toVSPosition() 70 | ); 71 | 72 | symbols.push(newSymbol); 73 | } 74 | 75 | return symbols; 76 | } else if (node.type !== 'method_call') { 77 | symbol.selectionRange = Range.create( 78 | Position.fromTSPosition(identifierNode.startPosition).toVSPosition(), 79 | Position.fromTSPosition(identifierNode.endPosition).toVSPosition() 80 | ); 81 | } else { 82 | return; 83 | } 84 | } else { 85 | return; 86 | } 87 | } else { 88 | symbol.selectionRange = symbol.range; 89 | symbol.name = node.text; 90 | } 91 | 92 | return symbol; 93 | }, 94 | }; 95 | 96 | export default RubyDocumentSymbol; 97 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/util/Stack.ts: -------------------------------------------------------------------------------- 1 | // Super simple stack 2 | export default class Stack { 3 | private stack: T[]; 4 | 5 | constructor() { 6 | this.stack = []; 7 | } 8 | 9 | get size() { 10 | return this.stack.length; 11 | } 12 | 13 | public push(value: T): void { 14 | this.stack.push(value); 15 | } 16 | 17 | public pop(): T { 18 | return this.stack.pop(); 19 | } 20 | 21 | public peek(): T { 22 | return this.stack[this.size - 1]; 23 | } 24 | 25 | public empty(): boolean { 26 | return this.size === 0; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/util/TreeSitterFactory.ts: -------------------------------------------------------------------------------- 1 | import log from 'loglevel'; 2 | import Parser from 'web-tree-sitter'; 3 | import Ruby from 'web-tree-sitter-ruby'; 4 | 5 | const TreeSitterFactory = { 6 | language: null, 7 | 8 | async initalize(): Promise { 9 | await Parser.init(); 10 | log.debug(`Loading Ruby tree-sitter syntax from ${Ruby}`); 11 | this.language = await Parser.Language.load(Ruby); 12 | }, 13 | 14 | build(): Parser { 15 | const parser = new Parser(); 16 | parser.setLanguage(this.language); 17 | return parser; 18 | }, 19 | }; 20 | export default TreeSitterFactory; 21 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/util/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Position } from './Position'; 2 | export { default as RubyDocumentSymbol } from './RubyDocumentSymbol'; 3 | export { default as Stack } from './Stack'; 4 | -------------------------------------------------------------------------------- /packages/language-server-ruby/src/util/spawn.ts: -------------------------------------------------------------------------------- 1 | import { of, merge, Observer, Observable, Subscription, Subject, AsyncSubject } from 'rxjs'; 2 | import { reduce } from 'rxjs/operators'; 3 | import { default as crossSpawn } from 'cross-spawn'; // eslint-disable-line import/no-named-default 4 | import { SpawnOptions } from 'child_process'; 5 | import log from 'loglevel'; 6 | 7 | export type SpawnOpts = { 8 | stdin?: Observable; 9 | encoding?: BufferEncoding; 10 | } & SpawnOptions; 11 | 12 | export interface SpawnReturn { 13 | source: string; 14 | text: string; 15 | } 16 | 17 | /** 18 | * Spawns a process attached as a child of the current process. 19 | * Modified from the spawn-rx version 20 | * 21 | * @param {string} exe The executable to run 22 | * @param {Array} params The parameters to pass to the child 23 | * @param {Object} opts Options to pass to spawn. 24 | * 25 | * @return {Observable} Returns an Observable that when subscribed 26 | * to, will create a child process. The 27 | * process output will be streamed to this 28 | * Observable, and if unsubscribed from, the 29 | * process will be terminated early. If the 30 | * process terminates with a non-zero value, 31 | * the Observable will terminate with onError. 32 | */ 33 | export function spawn( 34 | cmd: string, 35 | args: string[] = [], 36 | opts: SpawnOpts = {} 37 | ): Observable { 38 | return Observable.create((subj: Observer) => { 39 | const { stdin, ...optsWithoutStdIn } = opts; 40 | log.trace(`spawning process: ${cmd} ${args.join()}, ${JSON.stringify(optsWithoutStdIn)}`); 41 | 42 | const proc = crossSpawn(cmd, args, optsWithoutStdIn); 43 | 44 | const bufHandler = (source: string) => (b: string | Buffer): void => { 45 | if (b.length < 1) { 46 | return; 47 | } 48 | let chunk = '<< String sent back was too long >>'; 49 | try { 50 | if (typeof b === 'string') { 51 | chunk = b.toString(); 52 | } else { 53 | chunk = b.toString( optsWithoutStdIn.encoding || 'utf8'); 54 | } 55 | } catch (e) { 56 | chunk = `<< Lost chunk of process output for ${cmd} - length was ${b.length}>>`; 57 | } 58 | 59 | subj.next({ source: source, text: chunk }); 60 | }; 61 | 62 | const ret = new Subscription(); 63 | 64 | if (stdin) { 65 | if (proc.stdin) { 66 | ret.add( 67 | stdin.subscribe( 68 | (x: string): void => { 69 | proc.stdin.write(x); 70 | }, 71 | subj.error.bind(subj), 72 | (): void => { 73 | proc.stdin.end(); 74 | } 75 | ) 76 | ); 77 | } else { 78 | subj.error( 79 | new Error( 80 | `opts.stdio conflicts with provided spawn opts.stdin observable, 'pipe' is required` 81 | ) 82 | ); 83 | } 84 | } 85 | 86 | let stderrCompleted: Subject | Observable | null = null; 87 | let stdoutCompleted: Subject | Observable | null = null; 88 | let noClose = false; 89 | 90 | if (proc.stdout) { 91 | stdoutCompleted = new AsyncSubject(); 92 | proc.stdout.on('data', bufHandler('stdout')); 93 | proc.stdout.on('close', () => { 94 | (stdoutCompleted as Subject).next(true); 95 | (stdoutCompleted as Subject).complete(); 96 | }); 97 | } else { 98 | stdoutCompleted = of(true); 99 | } 100 | 101 | if (proc.stderr) { 102 | stderrCompleted = new AsyncSubject(); 103 | proc.stderr.on('data', bufHandler('stderr')); 104 | proc.stderr.on('close', () => { 105 | (stderrCompleted as Subject).next(true); 106 | (stderrCompleted as Subject).complete(); 107 | }); 108 | } else { 109 | stderrCompleted = of(true); 110 | } 111 | 112 | if (proc.stdin) { 113 | proc.stdin.on('error', e => { 114 | subj.error(e); 115 | }); 116 | } 117 | 118 | proc.on('error', (e: Error) => { 119 | noClose = true; 120 | subj.error(e); 121 | }); 122 | 123 | proc.on('close', (code: number) => { 124 | noClose = true; 125 | const pipesClosed = merge(stdoutCompleted, stderrCompleted).pipe(reduce(acc => acc, true)); 126 | 127 | if (code === 0) { 128 | pipesClosed.subscribe(() => subj.complete()); 129 | } else { 130 | pipesClosed.subscribe(() => subj.error(new Error(`Failed with exit code: ${code}`))); 131 | } 132 | }); 133 | 134 | ret.add( 135 | new Subscription((): void => { 136 | if (noClose) { 137 | return; 138 | } 139 | 140 | log.trace(`Killing process: ${cmd} ${args.join()}`); 141 | proc.kill(); 142 | }) 143 | ); 144 | 145 | return ret; 146 | }); 147 | } 148 | -------------------------------------------------------------------------------- /packages/language-server-ruby/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "outDir": "dist", 8 | "rootDir": "src" 9 | }, 10 | "include": ["src", "spec"], 11 | "exclude": ["node_modules"], 12 | "references": [{ "path": "../vscode-ruby-common" }] 13 | } 14 | -------------------------------------------------------------------------------- /packages/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "noUnusedParameters": true, 7 | "sourceMap": true, 8 | "esModuleInterop": true, 9 | "lib": ["dom", "es2017", "es2019.string"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../.eslintrc.base.json"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/README.md: -------------------------------------------------------------------------------- 1 | # Ruby for Visual Studio Code 2 | 3 | [![CircleCI](https://img.shields.io/circleci/build/github/rubyide/vscode-ruby?label=circleci&token=c9eaf03305b3fe24e8bc819f3f48060431e8e78f)](https://circleci.com/gh/rubyide/vscode-ruby) 4 | [![Build status](https://ci.appveyor.com/api/projects/status/vlgs2y7tsc4xpj4c?svg=true)](https://ci.appveyor.com/project/rebornix/vscode-ruby) 5 | [![codecov](https://codecov.io/gh/rubyide/vscode-ruby/branch/main/graph/badge.svg)](https://codecov.io/gh/rubyide/vscode-ruby) 6 | 7 | This extension provides enhanced Ruby language and debugging support for Visual Studio Code. 8 | 9 | ## Features 10 | 11 | - Automatic Ruby environment detection with support for rvm, rbenv, chruby, and asdf 12 | - Lint support via RuboCop, Standard, and Reek 13 | - Format support via RuboCop, Standard, Rufo, Prettier and RubyFMT 14 | - Semantic code folding support 15 | - Semantic highlighting support 16 | - Basic Intellisense support 17 | 18 | ## Installation 19 | 20 | Search for `ruby` in the [VS Code Extension Gallery](https://code.visualstudio.com/docs/editor/extension-gallery) and install it! 21 | 22 | ## Initial Configuration 23 | 24 | By default, the extension provides sensible defaults for developers to get a better experience using Ruby in Visual Studio Code. However, these defaults do not include settings to enable features like formatting or linting. Given how dynamic Ruby projects can be (are you using rvm, rbenv, chruby, or asdf? Are your gems globally installed or via bundler? etc), the extension requires additional configuration for additional features to be available. 25 | 26 | ### Using the Language Server 27 | 28 | It is **highly recommended** that you enable the Ruby language server (via the Use Language Server setting or `ruby.useLanguageServer` config option). The server does not default to enabled while it is under development but it provides a significantly better experience than the legacy extension functionality. See [docs/language-server.md](https://github.com/rubyide/vscode-ruby/blob/main/docs/language-server.md) for more information on the language server. 29 | 30 | Legacy functionality will most likely not receive additional improvements and will be fully removed when the extension hits v1.0 31 | 32 | ### Example Initial Configuration: 33 | 34 | ```json 35 | "ruby.useBundler": true, //run non-lint commands with bundle exec 36 | "ruby.useLanguageServer": true, // use the internal language server (see below) 37 | "ruby.lint": { 38 | "rubocop": { 39 | "useBundler": true // enable rubocop via bundler 40 | }, 41 | "reek": { 42 | "useBundler": true // enable reek via bundler 43 | } 44 | }, 45 | "ruby.format": "rubocop" // use rubocop for formatting 46 | ``` 47 | 48 | Reviewing the [linting](https://github.com/rubyide/vscode-ruby/blob/main/docs/linting.md), [formatting](https://github.com/rubyide/vscode-ruby/blob/main/docs/formatting.md), and [environment detection](https://github.com/rubyide/vscode-ruby/blob/main/docs/language-server.md) docs is recommended 49 | 50 | For full details on configuration options, please take a look at the `Ruby` section in the VS Code settings UI. Each option is associated with a name and description. 51 | 52 | ### Debug Configuration 53 | 54 | See [docs/debugger.md](https://github.com/rubyide/vscode-ruby/blob/main/docs/debugger.md). 55 | 56 | ### Legacy Configuration 57 | 58 | [docs/legacy.md](https://github.com/rubyide/vscode-ruby/blob/main/docs/legacy.md) contains the documentation around the legacy functionality 59 | 60 | ## Troubleshooting 61 | 62 | See [docs/troubleshooting.md](https://github.com/rubyide/vscode-ruby/blob/main/docs/troubleshooting.md) 63 | 64 | ## Other Notable Extensions 65 | 66 | - [Ruby Solargraph](https://marketplace.visualstudio.com/items?itemName=castwide.solargraph) - Solargraph is a language server that provides intellisense, code completion, and inline documentation for Ruby. 67 | - [VSCode Endwise](https://github.com/kaiwood/vscode-endwise) - Wisely add "end" in Ruby 68 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/images/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyide/vscode-ruby/79031948d032e97cf2a5116e27c167ade5125e15/packages/vscode-ruby-client/images/ruby.png -------------------------------------------------------------------------------- /packages/vscode-ruby-client/scripts/build-dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -r dist/server dist/debugger 4 | cp -R ../language-server-ruby/dist dist/server 5 | cp -R ../vscode-ruby-debugger/dist dist/debugger 6 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/scripts/link-dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | parent=$(cd ../ && pwd) 4 | mkdir -p dist 5 | rm -r dist/server dist/debugger 6 | ln -sf $parent/language-server-ruby/dist dist/server 7 | ln -sf $parent/vscode-ruby-debugger/dist dist/debugger 8 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * LSP client for vscode-ruby 3 | */ 4 | import path from 'path'; 5 | 6 | import { commands, ExtensionContext, window, workspace, WorkspaceFolder } from 'vscode'; 7 | import { 8 | ConfigurationParams, 9 | CancellationToken, 10 | LanguageClient, 11 | LanguageClientOptions, 12 | ServerOptions, 13 | TransportKind, 14 | WorkspaceMiddleware, 15 | } from 'vscode-languageclient/node'; 16 | 17 | let client: LanguageClient; 18 | 19 | export function activate(context: ExtensionContext): void { 20 | const serverModule: string = context.asAbsolutePath(path.join('dist', 'server', 'index.js')); 21 | const debugOptions: { execArgv: string[] } = { execArgv: ['--nolazy', '--inspect=6009'] }; 22 | 23 | // If the extension is launched in debug mode then the debug server options are used 24 | // Otherwise the run options are used 25 | const serverOptions: ServerOptions = { 26 | run: { module: serverModule, transport: TransportKind.ipc }, 27 | debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }, 28 | }; 29 | 30 | const rubyDocumentSelector: { scheme: string; language: string }[] = [ 31 | { scheme: 'file', language: 'ruby' }, 32 | { scheme: 'untitled', language: 'ruby' }, 33 | ]; 34 | 35 | // Options to control the language client 36 | const clientOptions: LanguageClientOptions = { 37 | documentSelector: rubyDocumentSelector, 38 | synchronize: { 39 | // Notify server of changes to version manager files 40 | fileEvents: workspace.createFileSystemWatcher('**/{.ruby-version,.rvmrc,.tool-versions}'), 41 | }, 42 | outputChannel: window.createOutputChannel('Ruby Language Server'), 43 | middleware: { 44 | workspace: { 45 | configuration: ( 46 | params: ConfigurationParams, 47 | token: CancellationToken, 48 | next: Function 49 | ): any[] => { 50 | if (!params.items) { 51 | return []; 52 | } 53 | let result = next(params, token, next); 54 | let settings = result[0]; 55 | let scopeUri = ''; 56 | 57 | for (let item of params.items) { 58 | if (!item.scopeUri) { 59 | continue; 60 | } else { 61 | scopeUri = item.scopeUri; 62 | } 63 | } 64 | let resource = client.protocol2CodeConverter.asUri(scopeUri); 65 | let workspaceFolder = workspace.getWorkspaceFolder(resource); 66 | 67 | // If the resource doesn't have a workspace folder, fall back to the root if available 68 | if (!workspaceFolder && workspace.workspaceFolders) { 69 | workspaceFolder = workspace.workspaceFolders[0]; 70 | } 71 | 72 | if (workspaceFolder) { 73 | // Save the file's workspace folder 74 | const protocolUri = client.code2ProtocolConverter.asUri(workspaceFolder.uri); 75 | settings.workspaceFolderUri = protocolUri; 76 | } 77 | return result; 78 | }, 79 | } as WorkspaceMiddleware, 80 | }, 81 | }; 82 | 83 | // Create the language client and start the client. 84 | client = new LanguageClient('ruby', 'Ruby', serverOptions, clientOptions); 85 | client.registerProposedFeatures(); 86 | 87 | // Push the disposable to the context's subscriptions so that the 88 | // client can be deactivated on extension deactivation 89 | context.subscriptions.push( 90 | client.start(), 91 | commands.registerCommand('ruby.showLanguageServerOutputChannel', () => { 92 | client.outputChannel.show(); 93 | }) 94 | ); 95 | } 96 | 97 | export function deactivate(): Thenable { 98 | return client ? client.stop() : undefined; 99 | } 100 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/format/rubyFormat.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import { AutoCorrect } from './RuboCop'; 4 | 5 | 6 | export class RubyDocumentFormattingEditProvider implements vscode.DocumentFormattingEditProvider { 7 | private autoCorrect: AutoCorrect; 8 | 9 | public register(ctx: vscode.ExtensionContext, documentSelector: vscode.DocumentSelector) { 10 | // only attempt to format if ruby.format is set to rubocop 11 | if (vscode.workspace.getConfiguration("ruby").get("format") !== "rubocop") { 12 | return; 13 | } 14 | 15 | this.autoCorrect = new AutoCorrect(); 16 | this.autoCorrect.test().then( 17 | () => ctx.subscriptions.push( 18 | vscode.languages.registerDocumentFormattingEditProvider(documentSelector, this) 19 | ) 20 | // silent failure - AutoCorrect will handle error messages 21 | ); 22 | } 23 | 24 | public provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): Thenable { 25 | const root = document.fileName ? path.dirname(document.fileName) : vscode.workspace.rootPath; 26 | const input = document.getText(); 27 | return this.autoCorrect.correct(input, root) 28 | .then( 29 | result => { 30 | return [new vscode.TextEdit(document.validateRange(new vscode.Range(0, 0, Infinity, Infinity)), result)]; 31 | }, 32 | err => { 33 | // silent failure - AutoCorrect will handle error messages 34 | return []; 35 | } 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/languageConfiguration.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import languageConfiguration from './languageConfiguration'; 3 | 4 | describe('wordPattern', function() { 5 | const wordPattern = languageConfiguration.wordPattern; 6 | 7 | it('should not match leading colon in symbols (#257)', function() { 8 | const text = ':fnord'; 9 | const matches = text.match(wordPattern); 10 | 11 | assert.strictEqual(matches[0], 'fnord'); 12 | }); 13 | 14 | it('should not match leading colons in constants (#257)', function() { 15 | const text = '::Bar'; 16 | const matches = text.match(wordPattern); 17 | 18 | assert.strictEqual(matches[0], 'Bar'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/languageConfiguration.ts: -------------------------------------------------------------------------------- 1 | const languageConfiguration = { 2 | indentationRules: { 3 | increaseIndentPattern: /^(\s*(module|class|((private|protected)\s+)?def|unless|if|else|elsif|case|when|begin|rescue|ensure|for|while|until|(?=.*?\b(do|begin|case|if|unless)\b)("(\\.|[^\\"])*"|'(\\.|[^\\'])*'|[^#"'])*(\s(do|begin|case)|[-+=&|*/~%^<>~]\s*(if|unless)))\b(?![^;]*;.*?\bend\b)|("(\\.|[^\\"])*"|'(\\.|[^\\'])*'|[^#"'])*(\((?![^\)]*\))|\{(?![^\}]*\})|\[(?![^\]]*\]))).*$/, 4 | decreaseIndentPattern: /^\s*([}\])](,?\s*(#|$)|\.[a-zA-Z_]\w*\b)|(end|rescue|ensure|else|elsif|when)\b)/, 5 | }, 6 | wordPattern: /(-?\d+(?:\.\d+))|([A-Za-z][^-`~@#%^&()=+[{}|;:'",<>/.*\]\s\\!?]*[!?]?)/, 7 | }; 8 | 9 | export default languageConfiguration; 10 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/lint/lib/fsPromise.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let fsCB = require('fs'); 4 | let fs = {}; 5 | 6 | function wrap(name) { 7 | return function () { 8 | let args = Array.prototype.slice.call(arguments); 9 | return new Promise((resolve, reject) => { 10 | const cb = function (e) { 11 | if (e) return reject(e); 12 | if (arguments.length === 1) return resolve(); 13 | if (arguments.length === 2) return resolve(arguments[1]); 14 | let response = Array.prototype.slice.call(arguments); 15 | response.shift(); //drop err 16 | resolve(response); 17 | }; 18 | args.push(cb); 19 | fsCB[name].apply(fsCB, args); 20 | }); 21 | }; 22 | } 23 | let keys = Object.keys(fsCB); 24 | //set everything that isn't an async function - all of the async's have a Sync equiv 25 | keys.filter(n => !(n + "Sync" in fsCB)) 26 | .forEach(n => fs[n] = fsCB[n]); 27 | 28 | //wrap all functions that have a sync equiv 29 | keys.filter(n => typeof fsCB[n] === 'function') 30 | .filter(n => (n + "Sync" in fsCB)) 31 | .forEach(n => fs[n] = wrap(n)); 32 | 33 | module.exports = fs; -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/lint/lib/lintResults.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const vscode = require('vscode'); 3 | 4 | const severities = { 5 | refactor: vscode.DiagnosticSeverity.Hint, 6 | convention: vscode.DiagnosticSeverity.Information, 7 | info: vscode.DiagnosticSeverity.Information, 8 | warning: vscode.DiagnosticSeverity.Warning, 9 | error: vscode.DiagnosticSeverity.Error, 10 | fatal: vscode.DiagnosticSeverity.Error 11 | }; 12 | 13 | export default class LintResults { 14 | constructor(linter) { 15 | this._fileDiagnostics = vscode.languages.createDiagnosticCollection(linter); 16 | } 17 | updateForFile(uri, lint) { 18 | this._fileDiagnostics.delete(uri); 19 | if (lint.error) { 20 | console.log("Linter error:", lint.source, lint.error); 21 | return; 22 | } 23 | if ((!lint.result || !lint.result.length) && (!lint.lintError || !lint.lintError.length)) return; 24 | 25 | let allOf = lint.result.concat(lint.lintError).map(offense => { 26 | let tail = offense.location.column + offense.location.length; 27 | let d = new vscode.Diagnostic(new vscode.Range( 28 | offense.location.line - 1, offense.location.column - 1, 29 | offense.location.line - 1, tail - 1), 30 | offense.message, severities[offense.severity] || vscode.DiagnosticSeverity.Error); 31 | d.source = offense.cop_name || lint.linter; 32 | return d; 33 | }); 34 | this._fileDiagnostics.set(uri, allOf); 35 | } 36 | dispose() { 37 | this._fileDiagnostics.dispose(); 38 | } 39 | } -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/lint/lib/linters/RuboCop.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function RuboCop(opts) { 4 | this.exe = "rubocop"; 5 | this.ext = process.platform === 'win32' ? ".bat" : ""; 6 | this.path = opts.path; 7 | this.responsePath = "stdout"; 8 | 9 | this.args = ["-s", "{path}", "-f", "json"] ; 10 | 11 | if (opts.forceExclusion) this.args.push("--force-exclusion"); 12 | if (opts.lint) this.args.push("-l"); 13 | if (opts.only) this.args = this.args.concat("--only", opts.only.join(',')); 14 | if (opts.except) this.args = this.args.concat("--except", opts.except.join(',')); 15 | if (opts.rails) this.args.push('-R'); 16 | if (opts.require) this.args = this.args.concat("-r", opts.require.join(',')); 17 | } 18 | 19 | RuboCop.prototype.processResult = function (data) { 20 | if (data == '') { 21 | return []; 22 | } 23 | let offenses = JSON.parse(data); 24 | if (offenses.summary.offense_count == 0) { 25 | return []; 26 | } 27 | return (offenses.files || [{ 28 | offenses: [] 29 | }])[0].offenses; 30 | }; 31 | 32 | module.exports = RuboCop; 33 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/lint/lib/linters/Ruby.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const EOL = require("os") 3 | .EOL; 4 | 5 | function RubyWC(opts) { 6 | this.responsePath = "stderr"; 7 | this.title = "ruby"; 8 | this.exe = "ruby"; 9 | this.ext = ""; 10 | this.path = opts.path; 11 | this.args = ["-wc"]; 12 | 13 | if (opts.rubyInterpreterPath) { 14 | this.exe = opts.rubyInterpreterPath; 15 | } 16 | 17 | if(opts.unicode) { 18 | this.args.push("-Ku"); 19 | } 20 | } 21 | 22 | RubyWC.prototype.processResult = function(data) { 23 | let messageLines = []; 24 | let multiLine = []; 25 | data.split(EOL) 26 | .forEach(line => { 27 | if (!line.length) return; 28 | let m = /^\-:(\d+): (?:(\w+): )?(.*)$/.exec(line); 29 | if (!m) { 30 | let marker = /^\s*\^$/.test(line); 31 | if (marker) { 32 | let p = 0, 33 | start = 0; 34 | while (multiLine.length && p < line.length) { 35 | start = p; 36 | p = multiLine.shift(); 37 | } 38 | if (start < line.length) { 39 | let l = messageLines[messageLines.length - 1]; 40 | l.location.column = start + 1; 41 | l.location.length = line.length - start; 42 | } 43 | return; 44 | } 45 | let re = /.*?[^\\]\;/g; 46 | let p = re.exec(line); 47 | multiLine = []; 48 | while (p) { 49 | multiLine.push(p.index + p[0].length); 50 | p = re.exec(line); 51 | } 52 | return; 53 | } 54 | messageLines.push({ 55 | location: { 56 | line: parseInt(m[1]), 57 | column: 1, 58 | length: 10000 59 | }, 60 | severity: m[2], 61 | message: m[3] 62 | }); 63 | }); 64 | return messageLines; 65 | }; 66 | 67 | module.exports = RubyWC; 68 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/lint/lib/linters/debride.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const EOL = require("os") 3 | .EOL; 4 | 5 | function Debride(opts) { 6 | this.exe = "debride"; 7 | this.ext = process.platform === 'win32' ? ".bat" : ""; 8 | this.path = opts.path; 9 | this.responsePath = "stdout"; 10 | this.args = ['{path}']; 11 | this.temp = true; 12 | if (opts.rails) this.args.unshift('-r'); 13 | } 14 | 15 | Debride.prototype.processResult = function(data) { 16 | let head = ""; 17 | let messageLines = []; 18 | data.split(EOL) 19 | .forEach(line => { 20 | if (line === "These methods MIGHT not be called:") return; 21 | if (!line.length) return (head = ""); 22 | let m = /\s*(.*?)\s+.*:(\d+)$/.exec(line); 23 | if (!m) return (head = line); 24 | let method = (head !== "main" ? head + "::" : "") + m[1]; 25 | messageLines.push({ 26 | location: { 27 | line: parseInt(m[2]), 28 | column: 1, 29 | length: 10000 30 | }, 31 | severity: 'info', 32 | message: `Method ${method} may not be called` 33 | }); 34 | }); 35 | return messageLines; 36 | }; 37 | 38 | module.exports = Debride; 39 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/lint/lib/linters/fasterer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const EOL = require("os") 3 | .EOL; 4 | 5 | function Fasterer(opts) { 6 | this.exe = "fasterer"; 7 | this.ext = require('os') 8 | .platform() === 'win32' ? ".bat" : ""; 9 | this.path = opts.path; 10 | this.responsePath = "stdout"; 11 | this.args = ['{path}']; 12 | this.temp = true; 13 | this.settings = ".fasterer.yml"; 14 | 15 | if (opts.rails) this.args.unshift('-r'); 16 | } 17 | 18 | Fasterer.prototype.processResult = function(data) { 19 | let messageLines = []; 20 | data.split(EOL) 21 | .forEach(line => { 22 | let m = /^(.*) Occurred at lines: (.*)/.exec(line); 23 | if (m) { 24 | let re = /(\d+)/g; 25 | let m2 = re.exec(m[2]); 26 | while (m2) { 27 | messageLines.push({ 28 | location: { 29 | line: parseInt(m2[1]), 30 | column: 1, 31 | length: 10000 32 | }, 33 | severity: 'info', 34 | message: m[1] 35 | }); 36 | m2 = re.exec(m[2]); 37 | } 38 | } 39 | }); 40 | return messageLines; 41 | }; 42 | 43 | module.exports = Fasterer; 44 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/lint/lib/linters/reek.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function Reek(opts) { 4 | this.exe = "reek"; 5 | this.ext = process.platform === 'win32' ? ".bat" : ""; 6 | this.path = opts.path; 7 | this.responsePath = "stdout"; 8 | this.errorPath = "stderr"; 9 | this.args = ["-f", "json", "--force-exclusion"]; 10 | } 11 | 12 | Reek.prototype.processResult = function(data) { 13 | if (!data) return []; 14 | let offenses = JSON.parse(data); 15 | let messageLines = []; 16 | offenses.forEach(offense => { 17 | offense.lines.forEach(line => { 18 | messageLines.push({ 19 | location: { 20 | line: line, 21 | column: 1, 22 | length: 10000 23 | }, 24 | message: offense.context + ": " + offense.message, 25 | cop_name: this.exe + ":" + offense.smell_type, 26 | severity: "info" 27 | }); 28 | }); 29 | }); 30 | return messageLines; 31 | }; 32 | const EOL = require("os") 33 | .EOL; 34 | 35 | Reek.prototype.processError = function(data) { 36 | //similar to the ruby output, but we get a length 37 | let messageLines = []; 38 | data.split(EOL) 39 | .forEach(line => { 40 | if (!line.length) return; 41 | let m = /^STDIN:(\d+):(\d+): (?:(\w+): )?(.*)$/.exec(line); 42 | if (!m) { 43 | let marker = /^STDIN:\d+:\s*\^(\~*)\s*$/.exec(line); 44 | if (marker) { 45 | messageLines[messageLines.length - 1].location.length = (marker[1] || "") 46 | .length + 1; 47 | } 48 | return; 49 | } 50 | messageLines.push({ 51 | location: { 52 | line: parseInt(m[1]), 53 | column: parseInt(m[2]), 54 | length: 10000 55 | }, 56 | severity: m[3], 57 | message: m[4] 58 | }); 59 | }); 60 | return messageLines; 61 | }; 62 | 63 | module.exports = Reek; 64 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/lint/lib/linters/ruby-lint.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function RubyLint(opts) { 4 | this.exe = "ruby-lint"; 5 | this.ext = process.platform === 'win32' ? ".bat" : ""; 6 | this.path = opts.path; 7 | this.responsePath = "stdout"; 8 | this.args = ['{path}', '-p', 'json']; 9 | if (opts.levels) this.args = this.args.concat('-l', opts.levels.join(',')); //info,warning,error 10 | if (opts.classes) this.args = this.args.concat('-a', opts.classes.join(',')); //argument_amount, pedantics, shadowing_variables, undefined_methods, undefined_variables, unused_variables, useless_equality_checks 11 | this.settings = "ruby-lint.yml"; 12 | this.temp = true; 13 | } 14 | 15 | /* 16 | [{ 17 | "level": "warning", 18 | "message": "unused local variable x", 19 | "line": 9, 20 | "column": 1, 21 | "file": "C:/Users/Bryan/AppData/Local/Temp/116127-4472-1g3n731", 22 | "filename": "116127-4472-1g3n731", 23 | "node": "(lvasgn :x\n (int 5))" 24 | }] 25 | */ 26 | 27 | RubyLint.prototype.processResult = function (data) { 28 | if (!data) return []; 29 | let offenses = JSON.parse(data); 30 | return offenses.map(offense => ({ 31 | message: offense.message, 32 | severity: offense.level, 33 | location: { 34 | line: offense.line, 35 | column: offense.column, 36 | length: 0 37 | } 38 | })); 39 | }; 40 | 41 | module.exports = RubyLint; -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/lint/lintCollection.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Config } from './lintConfig'; 4 | import Linter from './lib/linter'; 5 | import LintResults from './lib/lintResults'; 6 | 7 | export class LintCollection { 8 | private _results: any; 9 | private _docLinters: any; 10 | private _cfg: { [key: string]: Config }; 11 | private _rootPath: string; 12 | private _globalConfig: Config; 13 | 14 | constructor(globalConfig : Config, lintConfig : { [key: string]: Config }, rootPath) { 15 | this._results = {}; 16 | this._docLinters = {}; 17 | this._globalConfig = globalConfig; 18 | this._cfg = {}; 19 | this.cfg(lintConfig, globalConfig); 20 | this._rootPath = rootPath; 21 | } 22 | 23 | public run(doc) { 24 | if (!doc) return; 25 | if (doc.languageId !== 'ruby') return; 26 | if (!this._docLinters[doc.fileName] || this._docLinters[doc.fileName].doc != doc) 27 | this._docLinters[doc.fileName] = new Linter( 28 | doc, 29 | this._rootPath, 30 | this._update.bind(this, doc) 31 | ); 32 | this._docLinters[doc.fileName].run(this._cfg); 33 | } 34 | 35 | public _update(doc, result) { 36 | const linter = result.linter; 37 | if (!this._results[linter]) this._results[linter] = new LintResults(linter); 38 | this._results[linter].updateForFile(doc.uri, result); 39 | return Promise.resolve(); 40 | } 41 | 42 | public cfg(newConfig, globalConfig) { 43 | let activeLinters = Object.keys(this._cfg); 44 | let toRemove = activeLinters.filter(l => !(l in newConfig) || !newConfig[l]); 45 | toRemove.forEach(l => { 46 | if (this._results[l]) { 47 | this._results[l].dispose(); 48 | delete this._results[l]; 49 | } 50 | delete this._cfg[l]; 51 | }); 52 | // we change the config internally, so that the config of any (awaiting) linters will be updated by reference 53 | for (let l in newConfig) { 54 | if (newConfig[l]) this._cfg[l] = Object.assign({}, newConfig[l], globalConfig); 55 | } 56 | } 57 | 58 | public dispose() { 59 | for (let l in this._results) this._results[l].dispose(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/lint/lintConfig.ts: -------------------------------------------------------------------------------- 1 | export class Config 2 | { 3 | pathToRuby: string = 'ruby'; 4 | pathToBundler: string = 'bundle'; 5 | useBundler: boolean | undefined = undefined; 6 | } 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/providers/completion.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DocumentSelector, ExtensionContext } from 'vscode'; 3 | import * as cp from 'child_process'; 4 | 5 | export function registerCompletionProvider(ctx: ExtensionContext, documentSelector: DocumentSelector) { 6 | if (vscode.workspace.getConfiguration('ruby').codeCompletion == 'rcodetools') { 7 | const completeCommand = function (args) { 8 | let rctCompletePath = vscode.workspace.getConfiguration('ruby.rctComplete').get('commandPath', 'rct-complete'); 9 | args.push('--interpreter'); 10 | args.push(vscode.workspace.getConfiguration('ruby.interpreter').get('commandPath', 'ruby')); 11 | if (process.platform === 'win32') 12 | return cp.spawn('cmd', ['/c', rctCompletePath].concat(args)); 13 | return cp.spawn(rctCompletePath, args); 14 | } 15 | 16 | const completeTest = completeCommand(['--help']); 17 | completeTest.on('exit', () => { 18 | ctx.subscriptions.push( 19 | vscode.languages.registerCompletionItemProvider( 20 | /** selector */documentSelector, 21 | /** provider */{ 22 | provideCompletionItems: (document, position, token, context): Thenable => { 23 | return new Promise((resolve, reject) => { 24 | const line = position.line + 1; 25 | const column = position.character; 26 | let child = completeCommand([ 27 | '--completion-class-info', 28 | '--dev', 29 | '--fork', 30 | '--line=' + line, 31 | '--column=' + column 32 | ]); 33 | let outbuf = [], 34 | errbuf = []; 35 | child.stderr.on('data', (data) => errbuf.push(data)); 36 | child.stdout.on('data', (data) => outbuf.push(data)); 37 | child.stdout.on('end', () => { 38 | if (errbuf.length > 0) return reject(Buffer.concat(errbuf).toString()); 39 | let completionItems = []; 40 | Buffer.concat(outbuf).toString().split('\n').forEach(function (elem) { 41 | let items = elem.split('\t'); 42 | if (/^[^\w]/.test(items[0])) return; 43 | if (items[0].trim().length === 0) return; 44 | let completionItem = new vscode.CompletionItem(items[0]); 45 | completionItem.detail = items[1]; 46 | completionItem.documentation = items[1]; 47 | completionItem.filterText = items[0]; 48 | completionItem.insertText = items[0]; 49 | completionItem.label = items[0]; 50 | completionItem.kind = vscode.CompletionItemKind.Method; 51 | completionItems.push(completionItem); 52 | }, this); 53 | if (completionItems.length === 0) 54 | return reject({items: []}); 55 | return resolve({items: completionItems}); 56 | }); 57 | child.stdin.end(document.getText()); 58 | }); 59 | } 60 | }, 61 | /** triggerCharacters */ ...['.'] 62 | ) 63 | ) 64 | }); 65 | completeTest.on('error', () => 0); 66 | } 67 | } -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/providers/configuration.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export function registerConfigurationProvider(): void { 4 | vscode.debug.registerDebugConfigurationProvider('Ruby', new RubyConfigurationProvider()); 5 | } 6 | 7 | class RubyConfigurationProvider implements vscode.DebugConfigurationProvider { 8 | public provideDebugConfigurations( 9 | folder: vscode.WorkspaceFolder, 10 | token: vscode.CancellationToken 11 | ): Thenable { 12 | const names: string[] = rubyConfigurations.map( 13 | (config: vscode.DebugConfiguration) => config.name 14 | ); 15 | 16 | return vscode.window.showQuickPick(names).then((selected: string) => { 17 | return [ 18 | rubyConfigurations.find((config: vscode.DebugConfiguration) => config.name === selected), 19 | ]; 20 | }); 21 | } 22 | 23 | public resolveDebugConfiguration?( 24 | folder: vscode.WorkspaceFolder | undefined, 25 | debugConfiguration: vscode.DebugConfiguration 26 | ): vscode.ProviderResult { 27 | const cwd: string = debugConfiguration.cwd || '${workspaceRoot}'; 28 | 29 | return { ...debugConfiguration, cwd }; 30 | } 31 | } 32 | 33 | const rubyConfigurations: vscode.DebugConfiguration[] = [ 34 | { 35 | name: 'Debug Local File', 36 | type: 'Ruby', 37 | request: 'launch', 38 | program: '${workspaceRoot}/main.rb', 39 | }, 40 | { 41 | name: 'Listen for rdebug-ide', 42 | type: 'Ruby', 43 | request: 'attach', 44 | remoteHost: '127.0.0.1', 45 | remotePort: '1234', 46 | remoteWorkspaceRoot: '${workspaceRoot}', 47 | }, 48 | { 49 | name: 'Rails server', 50 | type: 'Ruby', 51 | request: 'launch', 52 | program: '${workspaceRoot}/bin/rails', 53 | args: ['server'], 54 | }, 55 | { 56 | name: 'Minitest - current line', 57 | type: 'Ruby', 58 | cwd: '${workspaceRoot}', 59 | request: 'launch', 60 | program: '${workspaceRoot}/bin/rails', 61 | args: [ 62 | 'test', 63 | '${file}:${lineNumber}' 64 | ] 65 | }, 66 | { 67 | name: 'RSpec - all', 68 | type: 'Ruby', 69 | request: 'launch', 70 | program: '${workspaceRoot}/bin/rspec', 71 | args: ['-I', '${workspaceRoot}'], 72 | }, 73 | { 74 | name: 'RSpec - active spec file only', 75 | type: 'Ruby', 76 | request: 'launch', 77 | program: '${workspaceRoot}/bin/rspec', 78 | args: ['-I', '${workspaceRoot}', '${file}'], 79 | }, 80 | { 81 | name: 'Cucumber', 82 | type: 'Ruby', 83 | request: 'launch', 84 | program: '${workspaceRoot}/bin/cucumber', 85 | }, 86 | ]; 87 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/providers/formatter.ts: -------------------------------------------------------------------------------- 1 | import { DocumentSelector, ExtensionContext } from 'vscode'; 2 | import { RubyDocumentFormattingEditProvider } from '../format/rubyFormat'; 3 | 4 | export function registerFormatter(ctx: ExtensionContext, documentSelector: DocumentSelector) { 5 | new RubyDocumentFormattingEditProvider().register(ctx, documentSelector); 6 | } 7 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/providers/highlight.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DocumentSelector, ExtensionContext } from 'vscode'; 3 | 4 | export function registerHighlightProvider(ctx: ExtensionContext, documentSelector: DocumentSelector) { 5 | // highlight provider 6 | let pairedEnds = []; 7 | 8 | const getEnd = function (line) { 9 | //end must be on a line by itself, or followed directly by a dot 10 | let match = line.text.match(/^(\s*)end\b[\.\s#]?\s*$/); 11 | if (match) { 12 | return new vscode.Range(line.lineNumber, match[1].length, line.lineNumber, match[1].length + 3); 13 | } 14 | } 15 | 16 | const getEntry = function(line) { 17 | let match = line.text.match(/^(.*\b)(begin|class|def|for|if|module|unless|until|case|while)\b[^;]*$/); 18 | if (match) { 19 | return new vscode.Range(line.lineNumber, match[1].length, line.lineNumber, match[1].length + match[2].length); 20 | } else { 21 | //check for do 22 | match = line.text.match(/\b(do)\b\s*(\|.*\|[^;]*)?$/); 23 | if (match) { 24 | return new vscode.Range(line.lineNumber, match.index, line.lineNumber, match.index + 2); 25 | } 26 | } 27 | } 28 | 29 | const balancePairs = function (doc) { 30 | pairedEnds = []; 31 | if (doc.languageId !== 'ruby') return; 32 | 33 | let waitingEntries = []; 34 | let entry, end; 35 | for (let i = 0; i < doc.lineCount; i++) { 36 | if ((entry = getEntry(doc.lineAt(i)))) { 37 | waitingEntries.push(entry); 38 | } else if (waitingEntries.length && (end = getEnd(doc.lineAt(i)))) { 39 | pairedEnds.push({ 40 | entry: waitingEntries.pop(), 41 | end: end 42 | }); 43 | } 44 | } 45 | } 46 | 47 | const balanceEvent = function (event) { 48 | if (event && event.document) balancePairs(event.document); 49 | } 50 | 51 | ctx.subscriptions.push(vscode.languages.registerDocumentHighlightProvider(documentSelector, { 52 | provideDocumentHighlights: (doc, pos) => { 53 | let result = pairedEnds.find(pair => ( 54 | pair.entry.start.line === pos.line || 55 | pair.end.start.line === pos.line)); 56 | if (result) { 57 | return [new vscode.DocumentHighlight(result.entry, 2), new vscode.DocumentHighlight(result.end, 2)]; 58 | } 59 | } 60 | })); 61 | 62 | ctx.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(balanceEvent)); 63 | ctx.subscriptions.push(vscode.workspace.onDidChangeTextDocument(balanceEvent)); 64 | ctx.subscriptions.push(vscode.workspace.onDidOpenTextDocument(balancePairs)); 65 | if (vscode.window && vscode.window.activeTextEditor) { 66 | balancePairs(vscode.window.activeTextEditor.document); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/providers/intellisense.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ExtensionContext, SymbolKind, SymbolInformation } from 'vscode'; 3 | import { Locate } from '../locate/locate'; 4 | 5 | export function registerIntellisenseProvider(ctx: ExtensionContext) { 6 | // for locate: if it's a project, use the root, othewise, don't bother 7 | if (vscode.workspace.getConfiguration('ruby').intellisense == 'rubyLocate') { 8 | const refreshLocate = () => { 9 | let progressOptions = { location: vscode.ProgressLocation.Window, title: 'Indexing Ruby source files' }; 10 | vscode.window.withProgress(progressOptions, () => locate.walk()); 11 | }; 12 | const settings: any = vscode.workspace.getConfiguration("ruby.locate") || {}; 13 | let locate = new Locate(vscode.workspace.rootPath, settings); 14 | refreshLocate(); 15 | ctx.subscriptions.push(vscode.commands.registerCommand('ruby.reloadProject', refreshLocate)); 16 | 17 | const watch = vscode.workspace.createFileSystemWatcher(settings.include); 18 | watch.onDidChange(uri => locate.parse(uri.fsPath)); 19 | watch.onDidCreate(uri => locate.parse(uri.fsPath)); 20 | watch.onDidDelete(uri => locate.rm(uri.fsPath)); 21 | const locationConverter = match => new vscode.Location(vscode.Uri.file(match.file), new vscode.Position(match.line, match.char)); 22 | const defProvider = { 23 | provideDefinition: (doc, pos) => { 24 | const txt = doc.getText(doc.getWordRangeAtPosition(pos)); 25 | return locate.find(txt).then(matches => matches.map(locationConverter)); 26 | } 27 | }; 28 | ctx.subscriptions.push(vscode.languages.registerDefinitionProvider(['ruby', 'erb'], defProvider)); 29 | const symbolKindTable = { 30 | class: () => SymbolKind.Class, 31 | module: () => SymbolKind.Module, 32 | method: symbolInfo => symbolInfo.name === 'initialize' ? SymbolKind.Constructor : SymbolKind.Method, 33 | classMethod: () => SymbolKind.Method, 34 | }; 35 | const defaultSymbolKind = symbolInfo => { 36 | console.warn(`Unknown symbol type: ${symbolInfo.type}`); 37 | return SymbolKind.Variable; 38 | }; 39 | // NOTE: Workaround for high CPU usage on IPC (channel.onread) when too many symbols returned. 40 | // For channel.onread see issue like this: https://github.com/Microsoft/vscode/issues/6026 41 | const numOfSymbolLimit = 3000; 42 | const symbolsConverter = matches => matches.slice(0, numOfSymbolLimit).map(match => { 43 | const symbolKind = (symbolKindTable[match.type] || defaultSymbolKind)(match); 44 | return new SymbolInformation(match.name, symbolKind, match.containerName, locationConverter(match)); 45 | }); 46 | // Gradually migrate intellisense features here as the langauge server supports more 47 | if (!vscode.workspace.getConfiguration('ruby').useLanguageServer) { 48 | const docSymbolProvider = { 49 | provideDocumentSymbols: (document, token) => { 50 | return locate.listInFile(document.fileName).then(symbolsConverter); 51 | } 52 | }; 53 | ctx.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(['ruby', 'erb'], docSymbolProvider)); 54 | } 55 | const workspaceSymbolProvider = { 56 | provideWorkspaceSymbols: (query, token) => { 57 | return locate.query(query).then(symbolsConverter); 58 | } 59 | }; 60 | ctx.subscriptions.push(vscode.languages.registerWorkspaceSymbolProvider(workspaceSymbolProvider)); 61 | } else { 62 | var rubyLocateDisabled = () => { 63 | vscode.window.showInformationMessage('The `ruby.intellisense` configuration is not set to use rubyLocate.') 64 | }; 65 | ctx.subscriptions.push(vscode.commands.registerCommand('ruby.reloadProject', rubyLocateDisabled)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/providers/linters.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ExtensionContext } from 'vscode'; 3 | import debounce from 'lodash/debounce'; 4 | 5 | import { LintCollection } from '../lint/lintCollection'; 6 | import { Config as LintConfig } from '../lint/lintConfig'; 7 | 8 | function getGlobalLintConfig() : LintConfig { 9 | let globalConfig = new LintConfig(); 10 | 11 | let pathToRuby = vscode.workspace.getConfiguration("ruby.interpreter").commandPath; 12 | if (pathToRuby) { 13 | globalConfig.pathToRuby = pathToRuby; 14 | } 15 | 16 | let useBundler = vscode.workspace.getConfiguration("ruby").get("useBundler"); 17 | if (useBundler !== null) { 18 | globalConfig.useBundler = useBundler; 19 | } 20 | 21 | let pathToBundler = vscode.workspace.getConfiguration("ruby").pathToBundler; 22 | if (pathToBundler) { 23 | globalConfig.pathToBundler = pathToBundler; 24 | } 25 | return globalConfig; 26 | } 27 | 28 | export function registerLinters(ctx: ExtensionContext) { 29 | const globalConfig = getGlobalLintConfig(); 30 | const linters = new LintCollection(globalConfig, vscode.workspace.getConfiguration("ruby").lint, vscode.workspace.rootPath); 31 | ctx.subscriptions.push(linters); 32 | 33 | function executeLinting(e: vscode.TextEditor | vscode.TextDocumentChangeEvent) { 34 | if (!e) return; 35 | linters.run(e.document); 36 | } 37 | 38 | // Debounce linting to prevent running on every keypress, only run when typing has stopped 39 | const lintDebounceTime = vscode.workspace.getConfiguration('ruby').lintDebounceTime; 40 | const executeDebouncedLinting = debounce(executeLinting, lintDebounceTime); 41 | 42 | ctx.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(executeLinting)); 43 | ctx.subscriptions.push(vscode.workspace.onDidChangeTextDocument(executeDebouncedLinting)); 44 | ctx.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => { 45 | const docs = vscode.window.visibleTextEditors.map(editor => editor.document); 46 | console.log("Config changed. Should lint:", docs.length); 47 | const globalConfig = getGlobalLintConfig(); 48 | linters.cfg(vscode.workspace.getConfiguration("ruby").lint, globalConfig); 49 | docs.forEach(doc => linters.run(doc)); 50 | })); 51 | 52 | // run against all of the current open files 53 | vscode.window.visibleTextEditors.forEach(executeLinting); 54 | } -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/ruby.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * vscode-ruby main 3 | */ 4 | import { ExtensionContext, languages, workspace } from 'vscode'; 5 | import * as utils from './utils'; 6 | 7 | import languageConfiguration from './languageConfiguration'; 8 | import { registerCompletionProvider } from './providers/completion'; 9 | import { registerConfigurationProvider } from './providers/configuration'; 10 | import { registerFormatter } from './providers/formatter'; 11 | import { registerHighlightProvider } from './providers/highlight'; 12 | import { registerIntellisenseProvider } from './providers/intellisense'; 13 | import { registerLinters } from './providers/linters'; 14 | import { registerTaskProvider } from './task/rake'; 15 | import * as client from './client'; 16 | 17 | const DOCUMENT_SELECTOR: { language: string; scheme: string }[] = [ 18 | { language: 'ruby', scheme: 'file' }, 19 | { language: 'ruby', scheme: 'untitled' }, 20 | ]; 21 | 22 | export async function activate(context: ExtensionContext): Promise { 23 | // register language config 24 | languages.setLanguageConfiguration('ruby', languageConfiguration); 25 | utils.getOutputChannel(); 26 | 27 | if (workspace.getConfiguration('ruby').useLanguageServer) { 28 | client.activate(context); 29 | } else { 30 | // Register legacy providers 31 | registerHighlightProvider(context, DOCUMENT_SELECTOR); 32 | registerFormatter(context, DOCUMENT_SELECTOR); 33 | 34 | if (workspace.rootPath) { 35 | registerLinters(context); 36 | } 37 | } 38 | 39 | // Register providers 40 | registerIntellisenseProvider(context); 41 | registerCompletionProvider(context, DOCUMENT_SELECTOR); 42 | registerConfigurationProvider(); 43 | 44 | if (workspace.rootPath) { 45 | registerTaskProvider(context); 46 | } 47 | 48 | utils.loadEnv(); 49 | } 50 | 51 | export function deactivate(): void { 52 | if (workspace.getConfiguration('ruby').useLanguageServer) { 53 | client.deactivate(); 54 | } 55 | 56 | return undefined; 57 | } 58 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/task/rake.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as path from 'path'; 4 | import * as fs from 'fs'; 5 | import * as vscode from 'vscode'; 6 | import { getOutputChannel, exec } from '../utils'; 7 | 8 | let rakeFiles: Set = new Set(); 9 | 10 | export async function registerTaskProvider(ctx: vscode.ExtensionContext) { 11 | let rakePromise: Thenable | undefined = undefined; 12 | let files = await vscode.workspace.findFiles("**/[rR]akefile{*,.rb}"); 13 | for (let i = 0; i < files.length; i++) { 14 | rakeFiles.add(files[i]); 15 | } 16 | 17 | let fileWatcher = vscode.workspace.createFileSystemWatcher("**/[rR]akefile{*,.rb}"); 18 | fileWatcher.onDidChange(() => rakePromise = undefined); 19 | fileWatcher.onDidCreate((uri) => { 20 | rakeFiles.add(uri); 21 | rakePromise = undefined 22 | }); 23 | fileWatcher.onDidDelete((uri) => { 24 | rakeFiles.delete(uri); 25 | rakePromise = undefined 26 | }); 27 | 28 | let taskProvider = vscode.workspace.registerTaskProvider('rake', { 29 | provideTasks: () => { 30 | if (!rakePromise) { 31 | rakePromise = getRakeTasks(); 32 | } 33 | return rakePromise; 34 | }, 35 | resolveTask(_task: vscode.Task): vscode.Task | undefined { 36 | return undefined; 37 | } 38 | }); 39 | } 40 | 41 | function exists(file: string): Promise { 42 | return new Promise((resolve, _reject) => { 43 | fs.exists(file, (value) => { 44 | resolve(value); 45 | }); 46 | }); 47 | } 48 | 49 | interface RakeTaskDefinition extends vscode.TaskDefinition { 50 | task: string; 51 | file?: string; 52 | } 53 | 54 | const buildNames: string[] = ['build', 'compile', 'watch']; 55 | function isBuildTask(name: string): boolean { 56 | for (let buildName of buildNames) { 57 | if (name.indexOf(buildName) !== -1) { 58 | return true; 59 | } 60 | } 61 | return false; 62 | } 63 | 64 | const testNames: string[] = ['test']; 65 | function isTestTask(name: string): boolean { 66 | for (let testName of testNames) { 67 | if (name.indexOf(testName) !== -1) { 68 | return true; 69 | } 70 | } 71 | return false; 72 | } 73 | 74 | async function getRakeTasks(): Promise { 75 | let workspaceRoot = vscode.workspace.rootPath; 76 | let emptyTasks: vscode.Task[] = []; 77 | if (!workspaceRoot) { 78 | return emptyTasks; 79 | } 80 | 81 | if (rakeFiles.size < 1) { 82 | return emptyTasks; 83 | } 84 | 85 | for (let key in rakeFiles.keys) { 86 | if (!await exists(rakeFiles[key])) { 87 | return emptyTasks; 88 | } 89 | } 90 | 91 | let commandLine = 'rake -AT'; 92 | try { 93 | let { stdout, stderr } = await exec(commandLine, { cwd: workspaceRoot }); 94 | if (stderr && stderr.length > 0) { 95 | getOutputChannel().appendLine(stderr); 96 | } 97 | let result: vscode.Task[] = []; 98 | if (stdout) { 99 | let lines = stdout.split(/\r{0,1}\n/); 100 | for (let line of lines) { 101 | if (line.length === 0) { 102 | continue; 103 | } 104 | let regExp = /rake\s(.*)#/; 105 | let matches = regExp.exec(line); 106 | if (matches && matches.length === 2) { 107 | let taskName = matches[1].trim(); 108 | let kind: RakeTaskDefinition = { 109 | type: 'rake', 110 | task: taskName 111 | }; 112 | let task = new vscode.Task(kind, taskName, 'rake', new vscode.ShellExecution(`rake ${taskName}`)); 113 | result.push(task); 114 | let lowerCaseLine = line.toLowerCase(); 115 | if (isBuildTask(lowerCaseLine)) { 116 | task.group = vscode.TaskGroup.Build; 117 | } else if (isTestTask(lowerCaseLine)) { 118 | task.group = vscode.TaskGroup.Test; 119 | } 120 | } 121 | } 122 | } 123 | return result; 124 | } catch (err) { 125 | let channel = getOutputChannel(); 126 | if (err.stderr) { 127 | channel.appendLine(err.stderr); 128 | } 129 | if (err.stdout) { 130 | channel.appendLine(err.stdout); 131 | } 132 | channel.appendLine('Auto detecting rake tasks failed.'); 133 | channel.show(true); 134 | return emptyTasks; 135 | } 136 | } -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/util/env.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa'; 2 | import defaultShell from 'default-shell'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | const SHIM_DIR = path.resolve(__dirname, 'shims'); 7 | const SHIM_EXTENSION = isWindows() ? 'cmd' : 'sh'; 8 | 9 | if (!fs.existsSync(SHIM_DIR)) { 10 | fs.mkdirSync(SHIM_DIR); 11 | } 12 | 13 | function isWindows(): boolean { 14 | return process.platform === 'win32'; 15 | } 16 | 17 | function getTemplate(shell: string): string { 18 | let template; 19 | if (isWindows()) { 20 | template = 'SET'; 21 | } else { 22 | template = `#!${shell} -i\nexport`; 23 | } 24 | 25 | return template; 26 | } 27 | 28 | function mkShim(shell: string, shimPath: string): boolean { 29 | const template = getTemplate(shell); 30 | let result = false; 31 | 32 | try { 33 | fs.writeFileSync(shimPath, template); 34 | fs.chmodSync(shimPath, 0o744); 35 | result = true; 36 | } catch (e) { 37 | console.log(e); 38 | } 39 | 40 | return result; 41 | } 42 | 43 | function getShim(): string { 44 | const shellName: string = path.basename(defaultShell); 45 | const shimPath = path.join(SHIM_DIR, `env.${shellName}.${SHIM_EXTENSION}`); 46 | if (!fs.existsSync(shimPath)) { 47 | mkShim(defaultShell, shimPath); 48 | } 49 | 50 | return shimPath; 51 | } 52 | 53 | // Based on the dotenv parse function: 54 | // https://github.com/motdotla/dotenv/blob/main/lib/main.js#L32 55 | // modified to not have to deal with Buffers and to handle stuff 56 | // like export and declare -x at the start of the line 57 | function processExportLine(line: string): string[] { 58 | const result = []; 59 | // matching "KEY' and 'VAL' in 'KEY=VAL' with support for arbitrary prefixes 60 | const keyValueArr = line.match(/^(?:[\w-]*\s+)*([\w.-]+)\s*=\s*(.*)?\s*$/); 61 | if (keyValueArr != null) { 62 | const key = keyValueArr[1]; 63 | 64 | // default undefined or missing values to empty string 65 | let value = keyValueArr[2] || ''; 66 | 67 | // expand newlines in quoted values 68 | const len = value ? value.length : 0; 69 | if (len > 0 && value.charAt(0) === '"' && value.charAt(len - 1) === '"') { 70 | value = value.replace(/\\n/gm, '\n'); 71 | } 72 | 73 | // remove any surrounding quotes and extra spaces 74 | value = value.replace(/(^['"]|['"]$)/g, '').trim(); 75 | 76 | result.push(key, value); 77 | } 78 | 79 | return result; 80 | } 81 | 82 | const RUBY_ENVIRONMENT_VARIABLES = [ 83 | 'PATH', 84 | 'Path', // Windows 85 | 'PATHEXT', // Windows 86 | 'RUBY_VERSION', 87 | 'RUBY_ROOT', 88 | 'RUBY_ENGINE', 89 | 'RUBYOPT', 90 | 'GEM_HOME', 91 | 'GEM_PATH', 92 | 'GEM_ROOT', 93 | 'HOME', 94 | 'RUBOCOP_OPTS', 95 | 'LANG', 96 | 'BUNDLE_PATH', 97 | 'RBENV_ROOT', 98 | 'ASDF_DATA_DIR', 99 | 'ASDF_CONFIG_FILE', 100 | 'ASDF_DEFAULT_TOOL_VERSIONS_FILENAME', 101 | ]; 102 | 103 | export interface IEnvironment { 104 | [key: string]: string; 105 | } 106 | 107 | export function loadEnv(cwd: string): IEnvironment { 108 | const shim: string = getShim(); 109 | const env: IEnvironment = {}; 110 | const { stdout, stderr } = execa.sync(shim, [], { 111 | cwd, 112 | }); 113 | 114 | console.error(stderr); 115 | 116 | for (const line of stdout.split('\n')) { 117 | const result: string[] = processExportLine(line); 118 | const name = result[0]; 119 | if (RUBY_ENVIRONMENT_VARIABLES.indexOf(name) >= 0) { 120 | env[name] = result[1]; 121 | } 122 | } 123 | 124 | return env; 125 | } 126 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as cp from 'child_process'; 3 | 4 | export function exec( 5 | command: string, 6 | options: cp.ExecOptions 7 | ): Promise<{ stdout: string; stderr: string }> { 8 | return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { 9 | cp.exec(command, options, (error, stdout, stderr) => { 10 | if (error) { 11 | reject({ error, stdout, stderr }); 12 | } 13 | resolve({ stdout, stderr }); 14 | }); 15 | }); 16 | } 17 | 18 | let _channel: vscode.OutputChannel; 19 | export function getOutputChannel(): vscode.OutputChannel { 20 | if (!_channel) { 21 | _channel = vscode.window.createOutputChannel('Ruby'); 22 | vscode.commands.registerCommand('ruby.showOutputChannel', () => { 23 | _channel.show(); 24 | }); 25 | } 26 | return _channel; 27 | } 28 | 29 | export async function loadEnv() { 30 | let { stdout, stderr } = await exec(process.env.SHELL + ' -lc export', { 31 | cwd: vscode.workspace.rootPath, 32 | }); 33 | let envs = stdout.trim().split('\n'); 34 | for (let i = 0; i < envs.length; i++) { 35 | let definition = envs[i]; 36 | let result = definition.split('=', 2); 37 | let envKey = result[0]; 38 | let envValue = result[1]; 39 | if (['PATH', 'GEM_HOME', 'GEM_PATH', 'RUBY_VERSION'].indexOf(envKey) > -1) { 40 | if (!process.env[envKey]) { 41 | process.env[envKey] = envValue; 42 | } 43 | } 44 | } 45 | 46 | getOutputChannel().appendLine(stderr); 47 | } 48 | export async function checkVersion() { 49 | getOutputChannel().appendLine(process.env.SHELL); 50 | let { stdout, stderr } = await exec('ruby -v', { cwd: vscode.workspace.rootPath }); 51 | 52 | getOutputChannel().appendLine(stdout); 53 | getOutputChannel().appendLine(stderr); 54 | } 55 | -------------------------------------------------------------------------------- /packages/vscode-ruby-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "allowJs": true, 9 | "noImplicitReturns": false, 10 | "noUnusedLocals": false, 11 | "noUnusedParameters": false, 12 | "rootDir": "src", 13 | "outDir": "dist/client" 14 | }, 15 | "include": ["src"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /packages/vscode-ruby-common/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../.eslintrc.base.json"], 3 | "plugins": ["chai-expect", "chai-friendly"], 4 | "rules": { 5 | "chai-expect/missing-assertion": 2, 6 | "chai-expect/terminating-properties": 1, 7 | "chai-expect/no-inner-compare": 1, 8 | "no-unused-expressions": 0, 9 | "chai-friendly/no-unused-expressions": 2 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/vscode-ruby-common/.gitignore: -------------------------------------------------------------------------------- 1 | test/shims 2 | *.tsbuildinfo 3 | lib 4 | -------------------------------------------------------------------------------- /packages/vscode-ruby-common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-ruby-common", 3 | "version": "0.28.1", 4 | "description": "Common utilities for VSCode Ruby", 5 | "main": "src/index.ts", 6 | "repository": "https://github.com/rubyide/vscode-ruby", 7 | "author": "Stafford Brunk ", 8 | "license": "MIT", 9 | "private": true, 10 | "scripts": { 11 | "test": "nyc mocha -r ts-node/register -r source-map-support/register -r test/setup.ts test/**/*.ts" 12 | }, 13 | "dependencies": { 14 | "cross-spawn": "^7.0.1", 15 | "default-shell": "^1.0.1" 16 | }, 17 | "devDependencies": { 18 | "@types/chai": "^4.2.5", 19 | "@types/chai-fs": "^2.0.2", 20 | "@types/cross-spawn": "^6.0.1", 21 | "@types/fs-extra": "^8.0.1", 22 | "@types/mocha": "^5.2.7", 23 | "@types/node": "^12.12.11", 24 | "@types/sinon": "^7.5.0", 25 | "@typescript-eslint/eslint-plugin": "^2.8.0", 26 | "@typescript-eslint/parser": "^2.8.0", 27 | "chai": "^4.2.0", 28 | "chai-fs": "^2.0.0", 29 | "eslint": "^6.6.0", 30 | "eslint-config-prettier": "^6.7.0", 31 | "eslint-config-standard": "^14.1.0", 32 | "eslint-config-standard-with-typescript": "^11.0.1", 33 | "eslint-plugin-chai-expect": "^2.0.1", 34 | "eslint-plugin-chai-friendly": "^0.5.0", 35 | "eslint-plugin-import": "^2.18.2", 36 | "eslint-plugin-node": "^10.0.0", 37 | "eslint-plugin-promise": "^4.2.1", 38 | "eslint-plugin-standard": "^4.0.1", 39 | "fs-extra": "^8.1.0", 40 | "mocha": "^6.2.2", 41 | "nyc": "^14.1.1", 42 | "prettier": "^1.19.1", 43 | "sinon": "^7.5.0", 44 | "source-map-support": "^0.5.16", 45 | "ts-node": "^10.4.0", 46 | "typescript": "^4.5.4" 47 | }, 48 | "nyc": { 49 | "cache": false, 50 | "extension": [ 51 | ".ts" 52 | ], 53 | "exclude": [ 54 | "**/*.d.ts", 55 | "coverage/**", 56 | "packages/*/test/**", 57 | "test/**", 58 | "test{,-*}.ts", 59 | "**/*{.,-}{test,spec}.ts", 60 | "**/node_modules/**" 61 | ], 62 | "reporter": [ 63 | "text", 64 | "lcov" 65 | ], 66 | "all": true 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/vscode-ruby-common/src/environment.ts: -------------------------------------------------------------------------------- 1 | import spawn from 'cross-spawn'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | const SHIM_DIR = path.resolve(__dirname, 'shims'); 6 | 7 | function isWindows(): boolean { 8 | return process.platform === 'win32'; 9 | } 10 | 11 | function defaultShell(): string { 12 | if (isWindows()) { 13 | return process.env.COMSPEC || 'cmd.exe'; 14 | } 15 | 16 | if (!process.env.SHELL) { 17 | return process.platform === 'darwin' ? '/bin/bash' : '/bin/sh'; 18 | } 19 | 20 | return process.env.SHELL; 21 | } 22 | 23 | function shimExtension(shell: string): string { 24 | let extension = 'sh'; 25 | if (shell.indexOf('fish') >= 0) extension = 'fish'; 26 | else if (isWindows()) extension = 'cmd'; 27 | return extension; 28 | } 29 | 30 | function getTemplate(shell: string): string { 31 | let template; 32 | if (isWindows()) { 33 | template = 'SET'; 34 | } else if (shell.indexOf('fish') >= 0) { 35 | template = `#!${shell} 36 | for name in (set -nx) 37 | if string match --quiet '*PATH' $name 38 | echo $name=(string join : -- $$name) 39 | else 40 | echo $name="$$name" 41 | end 42 | end`; 43 | } else { 44 | template = `#!${shell} -i\nexport`; 45 | } 46 | 47 | return template; 48 | } 49 | 50 | function mkShim(shell: string, shimPath: string): boolean { 51 | const template = getTemplate(shell); 52 | let result = false; 53 | 54 | try { 55 | fs.writeFileSync(shimPath, template); 56 | fs.chmodSync(shimPath, 0o744); 57 | result = true; 58 | } catch (e) { 59 | console.error(e); 60 | } 61 | 62 | return result; 63 | } 64 | 65 | function getShellName(shell: string): string { 66 | const cleanShell = isWindows() ? path.basename(shell) : shell; 67 | return cleanShell.replace(/[\/\\]/g, '.'); 68 | } 69 | 70 | function getShim(shell: string, shimDir: string): string { 71 | let shellName: string = getShellName(shell); 72 | if (shellName[0] === '.') shellName = shellName.slice(1); 73 | const shimPath = path.join(shimDir, `env.${shellName}.${shimExtension(shellName)}`); 74 | if (!fs.existsSync(shimDir)) { 75 | fs.mkdirSync(shimDir); 76 | } 77 | if (!fs.existsSync(shimPath)) { 78 | mkShim(shell, shimPath); 79 | } 80 | 81 | return shimPath; 82 | } 83 | 84 | // Based on the dotenv parse function: 85 | // https://github.com/motdotla/dotenv/blob/main/lib/main.js#L32 86 | // modified to not have to deal with Buffers and to handle stuff 87 | // like export and declare -x at the start of the line 88 | function processExportLine(line: string): string[] { 89 | const result = []; 90 | // matching "KEY' and 'VAL' in 'KEY=VAL' with support for arbitrary prefixes 91 | const keyValueArr = line.match(/^(?:[\w-]*\s+)*([\w.-]+)\s*=\s*(.*)?\s*$/); 92 | if (keyValueArr != null) { 93 | const key = keyValueArr[1]; 94 | 95 | // default undefined or missing values to empty string 96 | let value = keyValueArr[2] || ''; 97 | 98 | // expand newlines in quoted values 99 | const len = value ? value.length : 0; 100 | if (len > 0 && value.charAt(0) === '"' && value.charAt(len - 1) === '"') { 101 | value = value.replace(/\\n/gm, '\n'); 102 | } 103 | 104 | // remove any surrounding quotes and extra spaces 105 | value = value.replace(/(^['"]|['"]$)/g, '').trim(); 106 | 107 | result.push(key, value); 108 | } 109 | 110 | return result; 111 | } 112 | 113 | function processEnvironment(output: string): IEnvironment { 114 | const env: IEnvironment = {}; 115 | for (const line of output.split('\n')) { 116 | const result: string[] = processExportLine(line); 117 | const name = result[0]; 118 | if (RUBY_ENVIRONMENT_VARIABLES.indexOf(name) >= 0) { 119 | env[name] = result[1]; 120 | } 121 | } 122 | 123 | return env; 124 | } 125 | 126 | // Whitelist environment variables to pass on 127 | // Don't want to pull in potentially secret variables 128 | // If updating this make sure the RubyEnvironment interface 129 | // also gets updated. 130 | // 131 | // It'd be really nice if there was a way 132 | // of generating the correct constant and/or TypeScript interface 133 | // from a single declaration 134 | const RUBY_ENVIRONMENT_VARIABLES = [ 135 | 'PATH', 136 | 'Path', // Windows 137 | 'PATHEXT', // Windows 138 | 'RUBY_VERSION', 139 | 'RUBY_ROOT', 140 | 'RUBY_ENGINE', 141 | 'RUBYOPT', 142 | 'GEM_HOME', 143 | 'GEM_PATH', 144 | 'GEM_ROOT', 145 | 'HOME', 146 | 'RUBOCOP_OPTS', 147 | 'LANG', 148 | 'BUNDLE_PATH', 149 | 'RBENV_ROOT', 150 | 'ASDF_DATA_DIR', 151 | 'ASDF_CONFIG_FILE', 152 | 'ASDF_DEFAULT_TOOL_VERSIONS_FILENAME', 153 | ]; 154 | 155 | export interface IEnvironment { 156 | [key: string]: string; 157 | } 158 | 159 | export interface RubyEnvironment extends IEnvironment { 160 | PATH: string; 161 | Path?: string; // Windows 162 | PATHEXT?: string; // Windows 163 | RUBY_VERSION: string; 164 | RUBY_ROOT: string; 165 | RUBY_ENGINE: string; 166 | RUBYOPT: string; 167 | GEM_HOME: string; 168 | GEM_PATH: string; 169 | GEM_ROOT: string; 170 | HOME: string; 171 | RUBOCOP_OPTS: string; 172 | LANG: string; 173 | BUNDLE_PATH?: string; 174 | RBENV_ROOT?: string; 175 | ASDF_DATA_DIR?: string; 176 | ASDF_CONFIG_FILE?: string; 177 | ASDF_DEFAULT_TOOL_VERSIONS_FILENAME?: string; 178 | } 179 | 180 | export interface LoadEnvOptions { 181 | shell?: string; 182 | shimDir?: string; 183 | } 184 | 185 | export function loadEnv(cwd: string, options = {} as LoadEnvOptions): IEnvironment { 186 | const { shell = defaultShell(), shimDir = SHIM_DIR } = options; 187 | const shim: string = getShim(shell, shimDir); 188 | 189 | const { stdout, stderr, status } = spawn.sync(shim, [], { 190 | cwd, 191 | }); 192 | 193 | if (status !== 0) { 194 | console.error(stderr.toString()); 195 | } 196 | 197 | return processEnvironment(stdout.toString()); 198 | } 199 | -------------------------------------------------------------------------------- /packages/vscode-ruby-common/src/index.ts: -------------------------------------------------------------------------------- 1 | export { loadEnv, IEnvironment, RubyEnvironment } from './environment'; 2 | -------------------------------------------------------------------------------- /packages/vscode-ruby-common/test/fixtures/environment/environment.fish.txt: -------------------------------------------------------------------------------- 1 | GEM_HOME=/home/someuser/.gem/ruby/2.5.1 2 | GEM_PATH=/home/someuser/.gem/ruby/2.5.1:/home/someuser/.rubies/ruby-2.5.1/lib/ruby/gems/2.5.0 3 | GEM_ROOT=/home/someuser/.rubies/ruby-2.5.1/lib/ruby/gems/2.5.0 4 | HOME=/home/someuser 5 | LANG=en_US.UTF-8 6 | OLDPWD=/foo/bar/baz 7 | PAGER=less 8 | PWD=/bar/baz/foo 9 | RUBYOPT='' 10 | RUBY_ENGINE=ruby 11 | RUBY_ROOT=/home/someuser/.rubies/ruby-2.5.1 12 | RUBY_VERSION=2.5.1 13 | SHELL=/usr/local/bin/fish 14 | TERM=xterm-256color 15 | USER=someuser 16 | -------------------------------------------------------------------------------- /packages/vscode-ruby-common/test/fixtures/environment/environment.posix.txt: -------------------------------------------------------------------------------- 1 | export GEM_HOME=/home/someuser/.gem/ruby/2.5.1 2 | export GEM_PATH=/home/someuser/.gem/ruby/2.5.1:/home/someuser/.rubies/ruby-2.5.1/lib/ruby/gems/2.5.0 3 | export GEM_ROOT=/home/someuser/.rubies/ruby-2.5.1/lib/ruby/gems/2.5.0 4 | export HOME=/home/someuser 5 | export LANG=en_US.UTF-8 6 | export OLDPWD=/foo/bar/baz 7 | export PAGER=less 8 | export PWD=/bar/baz/foo 9 | export RUBYOPT='' 10 | export RUBY_ENGINE=ruby 11 | export RUBY_ROOT=/home/someuser/.rubies/ruby-2.5.1 12 | export RUBY_VERSION=2.5.1 13 | export SHELL=/bin/zsh 14 | export TERM=xterm-256color 15 | export USER=someuser 16 | -------------------------------------------------------------------------------- /packages/vscode-ruby-common/test/fixtures/environment/environment.win32.txt: -------------------------------------------------------------------------------- 1 | ALLUSERSPROFILE=C:\ProgramData 2 | APPDATA=C:\Users\someuser\AppData\Roaming 3 | CommonProgramFiles=C:\Program Files\Common Files 4 | CommonProgramFiles(x86)=C:\Program Files (x86)\Common Files 5 | CommonProgramW6432=C:\Program Files\Common Files 6 | ComSpec=C:\WINDOWS\system32\cmd.exe 7 | DriverData=C:\Windows\System32\Drivers\DriverData 8 | HOMEDRIVE=C: 9 | HOMEPATH=\Users\someuser 10 | LOCALAPPDATA=C:\Users\someuser\AppData\Local 11 | OS=Windows_NT 12 | Path=C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Ruby26-x64\bin;C:\Users\someuser\AppData\Local\Microsoft\WindowsApps;;C:\Users\someuser\AppData\Local\Programs\Microsoft VS Code\bin 13 | PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.RB;.RBW 14 | ProgramData=C:\ProgramData 15 | ProgramFiles=C:\Program Files 16 | ProgramFiles(x86)=C:\Program Files (x86) 17 | ProgramW6432=C:\Program Files 18 | PROMPT=$P$G 19 | PSModulePath=C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\ 20 | PUBLIC=C:\Users\Public 21 | SESSIONNAME=Console 22 | SystemDrive=C: 23 | SystemRoot=C:\WINDOWS 24 | TEMP=C:\Users\someuser\AppData\Local\Temp 25 | TMP=C:\Users\someuser\AppData\Local\Temp 26 | USERNAME=someuser 27 | USERPROFILE=C:\Users\someuser 28 | windir=C:\WINDOWS 29 | -------------------------------------------------------------------------------- /packages/vscode-ruby-common/test/setup.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiFs from 'chai-fs'; 3 | chai.use(chaiFs); 4 | -------------------------------------------------------------------------------- /packages/vscode-ruby-common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "target": "esnext", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "rootDir": "src", 9 | "outDir": "lib" 10 | }, 11 | "include": ["src"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/vscode-ruby-debugger/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../.eslintrc.base.json"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/vscode-ruby-debugger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-ruby-debugger", 3 | "version": "0.28.1", 4 | "description": "Debugger implementation for vscode-ruby", 5 | "main": "src/index.ts", 6 | "repository": "https://github.com/rubyide/vscode-ruby", 7 | "author": "Stafford Brunk ", 8 | "license": "MIT", 9 | "private": true, 10 | "capabilities": { 11 | "virtualWorkspaces": false, 12 | "untrustedWorkspaces": { 13 | "supported": false, 14 | "description": "Trust is required to debug code in this workspace." 15 | } 16 | }, 17 | "scripts": { 18 | "lint": "eslint src/**/*.ts", 19 | "test": "nyc mocha --timeout 5000 -r ts-node/register -r source-map-support/register spec/**/*.ts", 20 | "build": "yarn compile --minify", 21 | "compile": "esbuild ./src/main.ts --bundle --outfile=dist/main.js --external:vscode --format=cjs --platform=node", 22 | "watch": "yarn compile --sourcemap --watch" 23 | }, 24 | "dependencies": { 25 | "vscode-debugadapter": "^1.35.0", 26 | "vscode-debugprotocol": "^1.35.0", 27 | "xmldom": "^0.1.27" 28 | }, 29 | "devDependencies": { 30 | "@types/mocha": "^5.2.7", 31 | "@types/xmldom": "^0.1.29", 32 | "@typescript-eslint/eslint-plugin": "^2.8.0", 33 | "@typescript-eslint/parser": "^2.8.0", 34 | "chai": "^4.2.0", 35 | "eslint": ">=6.6.0", 36 | "eslint-config-standard": "^14.1.0", 37 | "eslint-config-standard-with-typescript": "^11.0.1", 38 | "eslint-plugin-import": "^2.18.2", 39 | "eslint-plugin-node": "^10.0.0", 40 | "eslint-plugin-promise": "^4.2.1", 41 | "eslint-plugin-standard": "^4.0.1", 42 | "mocha": "^6.2.2", 43 | "nyc": "^14.1.1", 44 | "prettier": "^1.19.1", 45 | "source-map-support": "^0.5.16", 46 | "ts-node": "^10.4.0", 47 | "typescript": "^4.5.4", 48 | "vscode-debugadapter-testsupport": "^1.35.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/vscode-ruby-debugger/spec/adapter.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | 'use strict'; 7 | 8 | import assert, { doesNotReject } from 'assert'; 9 | import path from 'path'; 10 | import { DebugProtocol } from 'vscode-debugprotocol'; 11 | import { DebugClient } from 'vscode-debugadapter-testsupport'; 12 | 13 | const DATA_ROOT = path.join(__dirname, 'data'); 14 | const DEBUG_ADAPTER = path.join(__dirname, '..', 'dist', 'main.js'); 15 | const PROGRAM = path.join(DATA_ROOT, 'basic.rb'); 16 | 17 | describe('vscode-ruby-debugger', () => { 18 | let dc: DebugClient; 19 | 20 | before(() => { 21 | dc = new DebugClient('node', DEBUG_ADAPTER, 'node'); 22 | dc.start(); 23 | }); 24 | 25 | after(() => { 26 | dc.stop(); 27 | }); 28 | 29 | describe('basic', () => { 30 | it('unknown request should produce error', done => { 31 | dc.send('illegal_request') 32 | .then(() => { 33 | done(new Error('does not report error on unknown request')); 34 | }) 35 | .catch(() => { 36 | done(); 37 | }); 38 | }); 39 | }); 40 | 41 | describe('initialize', () => { 42 | it('should return supported features', () => { 43 | return dc.initializeRequest().then(response => { 44 | assert.equal(response.body.supportsConfigurationDoneRequest, true); 45 | }); 46 | }); 47 | 48 | it("should produce error for invalid 'pathFormat'", done => { 49 | dc.initializeRequest({ 50 | adapterID: 'mock', 51 | linesStartAt1: true, 52 | columnsStartAt1: true, 53 | pathFormat: 'url', 54 | }) 55 | .then(response => { 56 | done(new Error("does not report error on invalid 'pathFormat' attribute")); 57 | }) 58 | .catch(err => { 59 | // error expected 60 | done(); 61 | }); 62 | }); 63 | }); 64 | 65 | describe('launch', () => { 66 | it('should run program to the end', () => { 67 | return Promise.all([ 68 | dc.configurationSequence(), 69 | dc.launch({ program: PROGRAM }), 70 | dc.waitForEvent('terminated'), 71 | ]); 72 | }); 73 | 74 | /* 75 | test('should stop on entry', () => { 76 | 77 | const PROGRAM = Path.join(DATA_ROOT, 'basic.rb'); 78 | const ENTRY_LINE = 1; 79 | 80 | return Promise.all([ 81 | dc.configurationSequence(), 82 | dc.launch({ program: PROGRAM, stopOnEntry: true }), 83 | dc.assertStoppedLocation('entry', { line: ENTRY_LINE } ) 84 | ]); 85 | }); 86 | */ 87 | }); 88 | 89 | describe('setBreakpoints', () => { 90 | // test('should stop on a breakpoint', () => { 91 | 92 | // const PROGRAM = Path.join(DATA_ROOT, 'basic.rb'); 93 | // const BREAKPOINT_LINE = 2; 94 | 95 | // return Promise.all>([ 96 | // dc.hitBreakpoint( 97 | // { program: PROGRAM }, 98 | // { path: PROGRAM, line: BREAKPOINT_LINE } ), 99 | // dc.assertStoppedLocation('breakpoint', { line: BREAKPOINT_LINE }), 100 | // dc.waitForEvent('stopped').then(event => { 101 | // return dc.continueRequest({ 102 | // threadId: event.body.threadId 103 | // }); 104 | // }), 105 | // dc.waitForEvent('terminated')]); 106 | // }); 107 | 108 | it('should get correct variables on a breakpoint', async () => { 109 | const BREAKPOINT_LINE = 3; 110 | let response; 111 | 112 | dc.hitBreakpoint({ program: PROGRAM }, { path: PROGRAM, line: BREAKPOINT_LINE }); 113 | response = await dc.assertStoppedLocation('breakpoint', { line: BREAKPOINT_LINE }); 114 | response = await dc.scopesRequest({ 115 | frameId: response.body.stackFrames[0].id, 116 | }); 117 | response = await dc.variablesRequest({ 118 | variablesReference: response.body.scopes[0].variablesReference, 119 | }); 120 | assert.strictEqual(response.body.variables.length, 3); 121 | assert.strictEqual(response.body.variables[0].name, 'a'); 122 | assert.strictEqual(response.body.variables[0].value, '10'); 123 | assert.strictEqual(response.body.variables[1].name, 'b'); 124 | assert.strictEqual(response.body.variables[1].value, 'undefined'); 125 | assert.strictEqual(response.body.variables[2].name, 'c'); 126 | assert.strictEqual(response.body.variables[2].value, 'undefined'); 127 | dc.disconnectRequest({}); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /packages/vscode-ruby-debugger/spec/data/basic.rb: -------------------------------------------------------------------------------- 1 | # Variables and expressions. 2 | a = 10 3 | b = 3 * a + 2 4 | printf("%d %d\n", a, b); 5 | 6 | # Type is dynamic. 7 | b = "A string" 8 | c = 'Another String' 9 | print b + " and " + c + "\n" -------------------------------------------------------------------------------- /packages/vscode-ruby-debugger/src/common.ts: -------------------------------------------------------------------------------- 1 | export enum SocketClientState { 2 | ready, 3 | connected, 4 | closed 5 | } 6 | 7 | export enum Mode { 8 | launch, 9 | attach 10 | } 11 | -------------------------------------------------------------------------------- /packages/vscode-ruby-debugger/src/helper.ts: -------------------------------------------------------------------------------- 1 | export function startsWith(str, searchString, position){ 2 | position = position || 0; 3 | return str.substr(position, searchString.length) === searchString; 4 | }; 5 | 6 | export function endsWith (str, searchString, position) { 7 | var subjectString = str.toString(); 8 | if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) { 9 | position = subjectString.length; 10 | } 11 | position -= searchString.length; 12 | var lastIndex = subjectString.indexOf(searchString, position); 13 | return lastIndex !== -1 && lastIndex === position; 14 | }; 15 | 16 | export function includes(str, search, start) { 17 | 'use strict'; 18 | if (typeof start !== 'number') { 19 | start = 0; 20 | } 21 | 22 | if (start + search.length > str.length) { 23 | return false; 24 | } else { 25 | return str.indexOf(search, start) !== -1; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /packages/vscode-ruby-debugger/src/interface.ts: -------------------------------------------------------------------------------- 1 | import { DebugProtocol } from 'vscode-debugprotocol'; 2 | 3 | /** 4 | * This interface should always match the schema found in the vscode-ruby extension manifest. 5 | */ 6 | export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { 7 | /** An absolute path to the program to debug. */ 8 | program: string; 9 | /** Optional arguments passed to the program being debugged. */ 10 | args: string[]; 11 | /** Automatically stop target after launch. If not specified, target does not stop. */ 12 | stopOnEntry?: boolean; 13 | /** Show debugger process output. If not specified, there will only be executable output */ 14 | showDebuggerOutput?: boolean; 15 | /** Show commands issued by the IDE to the debugger. If not specified, no commands will be shown. */ 16 | showDebuggerCommands?: boolean; 17 | /** Omit files matching any of these regexes from stack traces, and automatically step through them when debugging. */ 18 | skipFiles: string[]; 19 | /** Automatically step out of files matching these regexes during debugging. */ 20 | finishFiles: string[]; 21 | /** Executable working directory. */ 22 | cwd?: string; 23 | } 24 | 25 | export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { 26 | /** Executable working directory. */ 27 | cwd?: string; 28 | /** Optional host address for remote debugging. */ 29 | remoteHost?: string; 30 | /** Optional port for remote debugging. */ 31 | remotePort?: string; 32 | /** Path to UNIX domain socket for remote debugging. */ 33 | localSocketPath?: string; 34 | /** Optional remote workspace root, this parameter is required for remote debugging */ 35 | remoteWorkspaceRoot?: string; 36 | /** Show debugger process output. If not specified, there will only be executable output */ 37 | showDebuggerOutput?: boolean; 38 | /** Show commands issued by the IDE to the debugger. If not specified, no commands will be shown. */ 39 | showDebuggerCommands?: boolean; 40 | /** Omit files matching any of these regexes from stack traces, and automatically step through them when debugging. */ 41 | skipFiles: string[]; 42 | /** Automatically step out of files matching these regexes during debugging. */ 43 | finishFiles: string[]; 44 | } 45 | 46 | export interface IRubyEvaluationResult { 47 | name: string; 48 | kind: string; 49 | type: string; 50 | value: string; 51 | id: string; 52 | variablesReference: number; 53 | } 54 | 55 | export interface IDebugVariable { 56 | objectId?: number; 57 | variables?: IRubyEvaluationResult[]; 58 | variableName?: string; 59 | variableType?: string; 60 | } 61 | 62 | export interface ICommand { 63 | command: string; 64 | resolve: (value?: any) => void 65 | reject: (error?: any) => void 66 | } 67 | -------------------------------------------------------------------------------- /packages/vscode-ruby-debugger/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "allowJs": true, 8 | "noImplicitReturns": false, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "outDir": "dist", 12 | "rootDir": "src" 13 | }, 14 | "include": ["src"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/vscode-ruby/README.md: -------------------------------------------------------------------------------- 1 | # VSCode Ruby 2 | 3 | This extension provides improved syntax highlighting, language configuration, and snippets to Ruby and ERB files within Visual Studio Code. It is meant to be used alongside the [Ruby extension](https://marketplace.visualstudio.com/items?itemName=rebornix.Ruby). 4 | 5 | ## Syntax Highlighting 6 | 7 | Before: 8 | ![syntax_before](https://raw.githubusercontent.com/rubyide/vscode-ruby/main/images/syntax_before.png) 9 | 10 | After: 11 | ![syntax_after](https://raw.githubusercontent.com/rubyide/vscode-ruby/main/images/syntax_after.png) 12 | 13 | ## Using with the Ruby extension 14 | 15 | This extension is listed as a dependency for the Ruby extension and does not need to be installed independently 16 | -------------------------------------------------------------------------------- /packages/vscode-ruby/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyide/vscode-ruby/79031948d032e97cf2a5116e27c167ade5125e15/packages/vscode-ruby/images/logo.png -------------------------------------------------------------------------------- /packages/vscode-ruby/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | { 20 | 21 | 22 | } 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/vscode-ruby/language-configuration-erb.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "blockComment": [ "<%#", "%>" ] 4 | }, 5 | "brackets": [ 6 | ["<%", "%>"], 7 | ["<%=", "%>"], 8 | ["<%#", "%>"] 9 | ], 10 | "autoClosingPairs": [ 11 | ["<%", "%>"], 12 | ["<%=", "%>"], 13 | ["<%#", "%>"], 14 | ["{", "}"], 15 | ["[", "]"], 16 | ["(", ")"], 17 | ["`", "`"], 18 | { "open": "'", "close": "'", "notIn": ["string", "comment"] }, 19 | { "open": "\"", "close": "\"", "notIn": ["string"] }, 20 | ["<", ">"], 21 | { "open": "%", "close": "%", "notIn": ["string", "comment"] } 22 | ], 23 | "surroundingPairs": [ 24 | ["<%", "%>"], 25 | ["<%=", "%>"], 26 | ["<%#", "%>"], 27 | ["{", "}"], 28 | ["[", "]"], 29 | ["(", ")"], 30 | ["\"", "\""], 31 | ["'", "'"], 32 | ["`", "`"], 33 | ["<", ">"], 34 | ["%", "%"] 35 | ], 36 | "onEnterRules": [ 37 | { 38 | "beforeText": { 39 | "pattern": "<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\\w][_:\\w-.\\d]*)(?:(?:[^'\"/>]|\"[^\"]*\"|'[^']*')*?(?!\\/)>)[^<]*$", 40 | "flags": "i" 41 | }, 42 | "afterText": { 43 | "pattern": "^<\\/([_:\\w][_:\\w-.\\d]*)\\s*>", 44 | "flags": "i" 45 | }, 46 | "action": { 47 | "indent": "indentOutdent" 48 | } 49 | }, 50 | { 51 | "beforeText": { 52 | "pattern": "<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\\w][_:\\w-.\\d]*)(?:(?:[^'\"/>]|\"[^\"]*\"|'[^']*')*?(?!\\/)>)[^<]*$", 53 | "flags": "i" 54 | }, 55 | "action": { 56 | "indent": "indent" 57 | } 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /packages/vscode-ruby/language-configuration-ruby.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "#", 4 | "blockComment": ["=begin", "=end"] 5 | }, 6 | "brackets": [ 7 | ["{", "}"], 8 | ["[", "]"], 9 | ["(", ")"], 10 | ["begin","end"], 11 | ["def","end"], 12 | ["if","end"], 13 | ["case","end"], 14 | ["unless","end"], 15 | ["do","end"], 16 | ["class","end"], 17 | ["module","end"] 18 | ], 19 | "autoClosingPairs": [ 20 | ["{", "}"], 21 | ["[", "]"], 22 | ["(", ")"], 23 | ["`", "`"], 24 | { "open": "'", "close": "'", "notIn": ["string", "comment"] }, 25 | { "open": "\"", "close": "\"", "notIn": ["string"] } 26 | ], 27 | "surroundingPairs": [ 28 | ["{", "}"], 29 | ["[", "]"], 30 | ["(", ")"], 31 | ["\"", "\""], 32 | ["'", "'"], 33 | ["`", "`"] 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /packages/vscode-ruby/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-ruby", 3 | "displayName": "VSCode Ruby", 4 | "publisher": "wingrunr21", 5 | "version": "0.28.0", 6 | "description": "Syntax highlighing, snippet, and language configuration support for Ruby", 7 | "repository": "https://github.com/rubyide/vscode-ruby", 8 | "author": "Stafford Brunk ", 9 | "license": "MIT", 10 | "private": true, 11 | "extensionKind": [ 12 | "ui", 13 | "workspace" 14 | ], 15 | "icon": "images/logo.png", 16 | "galleryBanner": { 17 | "color": "#f05336", 18 | "theme": "light" 19 | }, 20 | "engines": { 21 | "vscode": "^1.58.0" 22 | }, 23 | "scripts": { 24 | "package": "vsce package --yarn && mv ./*.vsix ../../build/", 25 | "test": "yarn test:ruby && yarn test:erb && yarn test:gemfile", 26 | "test:ruby": "vscode-tmgrammar-test -s source.ruby -g syntaxes/ruby.cson.json -t \"test/**/*.rb\"", 27 | "test:erb": "vscode-tmgrammar-test -s text.html.erb -g syntaxes/erb.cson.json -t \"test/**/*.erb\"", 28 | "test:gemfile": "vscode-tmgrammar-test -s source.ruby.gemfile -g syntaxes/gemfile.cson.json -t \"test/Gemfile\"" 29 | }, 30 | "contributes": { 31 | "snippets": [ 32 | { 33 | "language": "ruby", 34 | "path": "./snippets/ruby.json" 35 | }, 36 | { 37 | "language": "erb", 38 | "path": "./snippets/erb.json" 39 | } 40 | ], 41 | "grammars": [ 42 | { 43 | "language": "ruby", 44 | "scopeName": "source.ruby", 45 | "path": "./syntaxes/ruby.cson.json" 46 | }, 47 | { 48 | "language": "erb", 49 | "scopeName": "text.html.erb", 50 | "path": "./syntaxes/erb.cson.json", 51 | "embeddedLanguages": { 52 | "source.css": "css", 53 | "source.js": "javascript", 54 | "source.ruby": "ruby" 55 | } 56 | }, 57 | { 58 | "language": "gemfile", 59 | "scopeName": "source.ruby.gemfile", 60 | "path": "./syntaxes/gemfile.cson.json" 61 | } 62 | ], 63 | "languages": [ 64 | { 65 | "id": "ruby", 66 | "aliases": [ 67 | "Ruby", 68 | "ruby" 69 | ], 70 | "firstLine": "^#!\\s*/.*(?:ruby|rbx|rake)\\b", 71 | "extensions": [ 72 | ".arb", 73 | ".builder", 74 | ".cgi", 75 | ".fcgi", 76 | ".gemspec", 77 | ".god", 78 | ".irbrc", 79 | ".jbuilder", 80 | ".mspec", 81 | ".pluginspec", 82 | ".podspec", 83 | ".prawn", 84 | ".pryrc", 85 | ".rabl", 86 | ".rake", 87 | ".rb", 88 | ".rbuild", 89 | ".rbw", 90 | ".rbx", 91 | ".rjs", 92 | ".ru", 93 | ".ruby", 94 | ".spec", 95 | ".thor", 96 | ".watchr" 97 | ], 98 | "filenames": [ 99 | "appfile", 100 | "appraisals", 101 | "berksfile", 102 | "brewfile", 103 | "capfile", 104 | "deliverfile", 105 | "fastfile", 106 | "guardfile", 107 | "podfile", 108 | "puppetfile", 109 | "rakefile", 110 | "snapfile", 111 | "thorfile", 112 | "vagrantfile", 113 | "dangerfile" 114 | ], 115 | "configuration": "./language-configuration-ruby.json" 116 | }, 117 | { 118 | "id": "erb", 119 | "aliases": [ 120 | "erb", 121 | "Encapsulated Ruby" 122 | ], 123 | "extensions": [ 124 | ".erb", 125 | ".rhtml", 126 | ".rhtm" 127 | ], 128 | "configuration": "./language-configuration-erb.json" 129 | }, 130 | { 131 | "id": "gemfile", 132 | "aliases": [ 133 | "Gemfile", 134 | "Bundler", 135 | "bundler" 136 | ], 137 | "filenames": [ 138 | "Gemfile" 139 | ], 140 | "configuration": "./language-configuration-ruby.json" 141 | }, 142 | { 143 | "id": "ignore", 144 | "filenames": [ 145 | ".chefignore" 146 | ] 147 | } 148 | ] 149 | }, 150 | "devDependencies": { 151 | "vscode-tmgrammar-test": "^0.0.11" 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /packages/vscode-ruby/snippets/erb.json: -------------------------------------------------------------------------------- 1 | { 2 | "if": { 3 | "prefix": "if", 4 | "body": [ 5 | "<% if ${1:truevalue} %>", 6 | " $2", 7 | "<% end %>" 8 | ], 9 | "description": "if .. end" 10 | }, 11 | "else": { 12 | "prefix": "else", 13 | "body": [ 14 | "<% else %>" 15 | ], 16 | "description": "else" 17 | }, 18 | "elsif": { 19 | "prefix": "elsif", 20 | "body": [ 21 | "<% elsif ${1:truevalue} %>" 22 | ], 23 | "description": "elsif" 24 | }, 25 | "end": { 26 | "prefix": "end", 27 | "body": [ 28 | "<% end %>" 29 | ], 30 | "description": "end" 31 | }, 32 | "ife": { 33 | "prefix": "ife", 34 | "body": [ 35 | "<% if ${1:truevalue} %>", 36 | " $2", 37 | "<% else %>", 38 | " $3", 39 | "<% end %>" 40 | ], 41 | "description": "if .. else .. end" 42 | }, 43 | "unless": { 44 | "prefix": "unless", 45 | "body": [ 46 | "<% unless ${1:falsevalue} %>", 47 | " $2", 48 | "<% end %>" 49 | ], 50 | "description": "unless .. end" 51 | }, 52 | "unlesse": { 53 | "prefix": "unlesse", 54 | "body": [ 55 | "<% unless ${1:falsevalue} %>", 56 | " $2", 57 | "<% else %>", 58 | " $3", 59 | "<% end %>" 60 | ], 61 | "description": "unless .. end" 62 | }, 63 | "each": { 64 | "prefix": "each", 65 | "body": [ 66 | "<% ${1:items}.each do |${2:item}| %>", 67 | " $2", 68 | "<% end %>" 69 | ], 70 | "description": "each do" 71 | }, 72 | "render": { 73 | "prefix": "pe", 74 | "body": [ 75 | "<%= $1 %>" 76 | ], 77 | "description": "render block pe" 78 | }, 79 | "exec": { 80 | "prefix": "er", 81 | "body": [ 82 | "<% $1 %>" 83 | ], 84 | "description": "erb exec block" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/vscode-ruby/snippets/ruby.json: -------------------------------------------------------------------------------- 1 | { 2 | "Exception block": { 3 | "prefix": "begin", 4 | "body": [ 5 | "begin", 6 | "\t$1", 7 | "rescue => exception", 8 | "\t", 9 | "end" 10 | ] 11 | }, 12 | "Exception block with ensure": { 13 | "prefix": "begin ensure", 14 | "body": [ 15 | "begin", 16 | "\t$1", 17 | "rescue => exception", 18 | "\t", 19 | "ensure", 20 | "\t", 21 | "end" 22 | ] 23 | }, 24 | "Exception block with else": { 25 | "prefix": "begin else", 26 | "body": [ 27 | "begin", 28 | "\t$1", 29 | "rescue => exception", 30 | "\t", 31 | "else", 32 | "\t", 33 | "end" 34 | ] 35 | }, 36 | "Exception block with else and ensure": { 37 | "prefix": "begin else ensure", 38 | "body": [ 39 | "begin", 40 | "\t$1", 41 | "rescue => exception", 42 | "\t", 43 | "else", 44 | "\t", 45 | "ensure", 46 | "\t", 47 | "end" 48 | ] 49 | }, 50 | "Class definition with initialize": { 51 | "prefix": "class init", 52 | "body": [ 53 | "class ${1:ClassName}", 54 | "\tdef initialize", 55 | "\t\t$0", 56 | "\tend", 57 | "end" 58 | ] 59 | }, 60 | "Class definition": { 61 | "prefix": "class", 62 | "body": [ 63 | "class ${1:ClassName}", 64 | "\t$0", 65 | "end" 66 | ] 67 | }, 68 | "for loop": { 69 | "prefix": "for", 70 | "body": [ 71 | "for ${1:value} in ${2:enumerable} do", 72 | "\t$0", 73 | "end" 74 | ] 75 | }, 76 | "if": { 77 | "prefix": "if", 78 | "body": [ 79 | "if ${1:test}", 80 | "\t$0", 81 | "end" 82 | ] 83 | }, 84 | "if else": { 85 | "prefix": "if else", 86 | "body": [ 87 | "if ${1:test}", 88 | "\t$0", 89 | "else", 90 | "\t", 91 | "end" 92 | ] 93 | }, 94 | "if elsif": { 95 | "prefix": "if elsif", 96 | "body": [ 97 | "if ${1:test}", 98 | "\t$0", 99 | "elsif ", 100 | "\t", 101 | "end" 102 | ] 103 | }, 104 | "if elsif else": { 105 | "prefix": "if elsif else", 106 | "body": [ 107 | "if ${1:test}", 108 | "\t$0", 109 | "elsif ", 110 | "\t", 111 | "else", 112 | "\t", 113 | "end" 114 | ] 115 | }, 116 | "case": { 117 | "prefix": "case", 118 | "body": [ 119 | "case ${1:test}", 120 | "when $2", 121 | "\t$3", 122 | "when $4", 123 | "\t$5", 124 | "else", 125 | "\t$6", 126 | "end" 127 | ] 128 | }, 129 | "forever loop": { 130 | "prefix": "loop", 131 | "body": [ 132 | "loop do", 133 | "\t$0", 134 | "end" 135 | ] 136 | }, 137 | "Module definition": { 138 | "prefix": "module", 139 | "body": [ 140 | "module ${1:ModuleName}", 141 | "\t$0", 142 | "end" 143 | ] 144 | }, 145 | "unless": { 146 | "prefix": "unless", 147 | "body": [ 148 | "unless ${1:test}", 149 | "\t$0", 150 | "end" 151 | ] 152 | }, 153 | "until loop": { 154 | "prefix": "until", 155 | "body": [ 156 | "until ${1:test}", 157 | "\t$0", 158 | "end" 159 | ] 160 | }, 161 | "while loop": { 162 | "prefix": "while", 163 | "body": [ 164 | "while ${1:test}", 165 | "\t$0", 166 | "end" 167 | ] 168 | }, 169 | "method definition": { 170 | "prefix": "def", 171 | "body": [ 172 | "def ${1:method_name}", 173 | "\t$0", 174 | "end" 175 | ] 176 | }, 177 | "Rake Task": { 178 | "prefix": "rake", 179 | "description": "Create a Rake Task", 180 | "body": [ 181 | "namespace :${1} do", 182 | "\tdesc \"${2}\"", 183 | "\ttask ${3}: :environment do", 184 | "\t\t${4}", 185 | "\tend", 186 | "end" 187 | ] 188 | }, 189 | "Insert do … end block": { 190 | "prefix": "do", 191 | "body": [ 192 | "do", 193 | "\t$0", 194 | "end" 195 | ] 196 | }, 197 | "Insert do |variable| … end block": { 198 | "prefix": "dop", 199 | "body": [ 200 | "do |${1:variable}|", 201 | "\t$0", 202 | "end" 203 | ] 204 | }, 205 | "Insert curly braces block": { 206 | "prefix": [ 207 | "{p", 208 | "{P" 209 | ], 210 | "body": "{ ${1:|${2:variable}| }$0 " 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /packages/vscode-ruby/syntaxes/erb.cson.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HTML (Ruby - ERB)", 3 | "scopeName": "text.html.erb", 4 | "fileTypes": ["rhtml", "html.erb"], 5 | "injections": { 6 | "text.html.erb - (meta.embedded.block.erb | meta.embedded.line.erb | meta.tag | comment), L:meta.tag, L:source.js.embedded.html": { 7 | "patterns": [ 8 | { 9 | "begin": "(^\\s*)(?=<%+#(?![^%]*%>))", 10 | "beginCaptures": { 11 | "0": { 12 | "name": "punctuation.whitespace.comment.leading.erb" 13 | } 14 | }, 15 | "end": "(?!\\G)(\\s*$\\n)?", 16 | "endCaptures": { 17 | "0": { 18 | "name": "punctuation.whitespace.comment.trailing.erb" 19 | } 20 | }, 21 | "patterns": [ 22 | { 23 | "include": "#comment" 24 | } 25 | ] 26 | }, 27 | { 28 | "begin": "(^\\s*)(?=<%(?![^%]*%>))", 29 | "beginCaptures": { 30 | "0": { 31 | "name": "punctuation.whitespace.embedded.leading.erb" 32 | } 33 | }, 34 | "end": "(?!\\G)(\\s*$\\n)?", 35 | "endCaptures": { 36 | "0": { 37 | "name": "punctuation.whitespace.embedded.trailing.erb" 38 | } 39 | }, 40 | "patterns": [ 41 | { 42 | "include": "#tags" 43 | } 44 | ] 45 | }, 46 | { 47 | "include": "#comment" 48 | }, 49 | { 50 | "include": "#tags" 51 | } 52 | ] 53 | } 54 | }, 55 | "patterns": [ 56 | { 57 | "include": "text.html.basic" 58 | } 59 | ], 60 | "repository": { 61 | "comment": { 62 | "patterns": [ 63 | { 64 | "begin": "<%+#", 65 | "beginCaptures": { 66 | "0": { 67 | "name": "punctuation.definition.comment.begin.erb" 68 | } 69 | }, 70 | "end": "%>", 71 | "endCaptures": { 72 | "0": { 73 | "name": "punctuation.definition.comment.end.erb" 74 | } 75 | }, 76 | "name": "comment.block.erb" 77 | } 78 | ] 79 | }, 80 | "tags": { 81 | "patterns": [ 82 | { 83 | "begin": "<%+(?!>)[-=]?(?![^%]*%>)", 84 | "beginCaptures": { 85 | "0": { 86 | "name": "punctuation.section.embedded.begin.erb" 87 | } 88 | }, 89 | "contentName": "source.ruby.embedded.erb", 90 | "end": "-?%>", 91 | "endCaptures": { 92 | "0": { 93 | "name": "punctuation.section.embedded.end.erb" 94 | }, 95 | "1": { 96 | "name": "source.ruby" 97 | } 98 | }, 99 | "name": "meta.embedded.block.erb", 100 | "patterns": [ 101 | { 102 | "captures": { 103 | "1": { 104 | "name": "punctuation.definition.comment.erb" 105 | } 106 | }, 107 | "match": "(#).*?(?=-?%>)", 108 | "name": "comment.line.number-sign.erb" 109 | }, 110 | { 111 | "include": "source.ruby" 112 | } 113 | ] 114 | }, 115 | { 116 | "begin": "<%+(?!>)[-=]?", 117 | "beginCaptures": { 118 | "0": { 119 | "name": "punctuation.section.embedded.begin.erb" 120 | } 121 | }, 122 | "contentName": "source.ruby.embedded.erb", 123 | "end": "-?%>", 124 | "endCaptures": { 125 | "0": { 126 | "name": "punctuation.section.embedded.end.erb" 127 | }, 128 | "1": { 129 | "name": "source.ruby" 130 | } 131 | }, 132 | "name": "meta.embedded.line.erb", 133 | "patterns": [ 134 | { 135 | "captures": { 136 | "1": { 137 | "name": "punctuation.definition.comment.erb" 138 | } 139 | }, 140 | "match": "(#).*?(?=-?%>)", 141 | "name": "comment.line.number-sign.erb" 142 | }, 143 | { 144 | "include": "source.ruby" 145 | } 146 | ] 147 | } 148 | ] 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /packages/vscode-ruby/syntaxes/gemfile.cson.json: -------------------------------------------------------------------------------- 1 | { 2 | "information_for_contributors": [ 3 | "This file has been converted from https://github.com/atom/language-ruby/blob/main/grammars/gemfile.cson", 4 | "If you want to provide a fix or improvement, please create a pull request against the original repository.", 5 | "Once accepted there, we are happy to receive an update request." 6 | ], 7 | "version": "https://github.com/atom/language-ruby/commit/3cb25d88e1173a29027eb52259415a3f851b8662", 8 | "name": "Gemfile", 9 | "scopeName": "source.ruby.gemfile", 10 | "fileTypes": ["Gemfile"], 11 | "patterns": [ 12 | { 13 | "include": "source.ruby" 14 | }, 15 | { 16 | "begin": "\\b(? 4.2.8' 17 | # <--- keyword.other.special-method.ruby.gemfile 18 | gem 'foo', github: 'https://github.com/foo/bar' 19 | # ^^^^^^ meta.declaration.ruby.gemfile 20 | 21 | git 'https://github.com/rails/rails.git' do 22 | # <--- keyword.other.special-method.ruby.gemfile 23 | gem 'railties' 24 | gem 'actionpack' 25 | gem 'activemodel' 26 | end 27 | 28 | group :development do 29 | # <----- keyword.other.special-method.ruby.gemfile 30 | gem 'pry' 31 | end 32 | 33 | platforms :ruby do 34 | # <--------- keyword.other.special-method.ruby.gemfile 35 | gem "ruby-debug" 36 | gem "sqlite3" 37 | end 38 | -------------------------------------------------------------------------------- /packages/vscode-ruby/test/heredoc.rb: -------------------------------------------------------------------------------- 1 | # SYNTAX TEST "source.ruby" 2 | p(<<~'SOMETEXT' , <<~OTHERTEXT, Time.now) 3 | # ^^^^^^^^^^^^^ string.unquoted.heredoc.ruby punctuation.definition.string.begin.ruby 4 | # ^^^^^^^^^^^^ string.unquoted.heredoc.ruby string.unquoted.heredoc.ruby punctuation.definition.string.begin.ruby 5 | test1 if unless else "foorbar" 6 | # <------------------------------ string.unquoted.heredoc.ruby 7 | SOMETEXT 8 | # <-------- string.unquoted.heredoc.ruby punctuation.definition.string.end.ruby 9 | test2 if unless else "foobar" 10 | OTHERTEXT 11 | 12 | function_call <<-EOS.strip, false, nil, :foo => :bar 13 | # ^^^^^^ string.unquoted.heredoc.ruby punctuation.definition.string.begin.ruby 14 | # ^^^^^ string.unquoted.heredoc.ruby meta.function-call.ruby entity.name.function.ruby 15 | # ^^^^^ string.unquoted.heredoc.ruby constant.language.boolean.ruby 16 | # ^^^ string.unquoted.heredoc.ruby constant.language.nil.ruby 17 | # ^^^^ string.unquoted.heredoc.ruby constant.language.symbol.hashkey.ruby 18 | # ^^ string.unquoted.heredoc.ruby punctuation.separator.key-value 19 | # ^^^^ string.unquoted.heredoc.ruby constant.language.symbol.ruby 20 | the quick 21 | EOS 22 | 23 | function_call <<-EOS.strip, false, nil, :foo => :bar 24 | #{foo} 25 | # ^^^^^^ meta.embedded.line.ruby 26 | EOS 27 | 28 | send :method, <<-EOF 29 | hello 30 | EOF 31 | 32 | exec_sql(<<-SQL, 1, 'book') 33 | # ^^^^^^ meta.embedded.block.sql 34 | SELECT * FROM products WHERE id = ? OR name = ?; 35 | # <----------------------------------------------- meta.embedded.block.sql source.sql 36 | SQL 37 | 38 | <<-SLIM 39 | # <------- meta.embedded.block.slim 40 | .foo 41 | # ^^^^ meta.embedded.block.slim text.slim 42 | #{bar} 43 | SLIM 44 | -------------------------------------------------------------------------------- /packages/vscode-ruby/test/simple_syntax.rb: -------------------------------------------------------------------------------- 1 | # SYNTAX TEST "source.ruby" 2 | 3 | if foo && bar && baz 4 | # ^^^ variable.other.ruby 5 | # ^^^ variable.other.ruby 6 | # ^^^ variable.other.ruby 7 | end 8 | 9 | foo && bar && baz 10 | # <--- variable.other.ruby 11 | # ^^^ variable.other.ruby 12 | # ^^^ variable.other.ruby 13 | 14 | Klass::Klass2::Klass3.instance.meth 15 | # ^^^^^^^^ meta.function-call.ruby entity.name.function.ruby 16 | # ^^^^ meta.function-call.ruby entity.name.function.ruby 17 | 18 | Klass.meth 19 | # ^^^^ meta.function-call.ruby entity.name.function.ruby 20 | Klass.meth() 21 | # ^^^^ meta.function-call.ruby entity.name.function.ruby 22 | Klass.meth 'arg', 'arg', 'arg', :arg, methcall 23 | # ^^^^ meta.function-call.ruby entity.name.function.ruby 24 | Klass.meth('arg', 'arg', 'arg', :arg, methcall) 25 | # ^^^^ meta.function-call.ruby entity.name.function.ruby 26 | 27 | foo(bar(baz())) 28 | # <-- meta.function-call.ruby entity.name.function.ruby 29 | # ^ punctuation.section.function.ruby 30 | # ^^^ meta.function-call.ruby entity.name.function.ruby 31 | # ^ punctuation.section.function.ruby 32 | # ^^^ meta.function-call.ruby entity.name.function.ruby 33 | # ^^^^ punctuation.section.function.ruby 34 | 35 | !!!!true('foo') 36 | # <---- keyword.operator.logical.ruby 37 | -------------------------------------------------------------------------------- /packages/vscode-ruby/test/syntax.erb: -------------------------------------------------------------------------------- 1 | # SYNTAX TEST "text.html.erb" 2 | 3 | 4 | <%= title %> 5 | # ^^^^^^^^^^^^ meta.embedded.line.erb 6 | # ^^^ punctuation.section.embedded.begin.erb 7 | # ^^^^^ source.ruby.embedded.erb 8 | # ^^ punctuation.section.embedded.end.erb 9 | 10 | 11 |

12 | <%# comment %> 13 | # ^^^ punctuation.definition.comment.begin.erb 14 | # ^^^^^^^ comment.block.erb 15 | # ^^ punctuation.definition.comment.end.erb 16 | <% if @foo -%> 17 | # ^^^^^^ text.html.erb 18 | <% else %> 19 | <% end %> 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/vscode-ruby/test/syntax.rb: -------------------------------------------------------------------------------- 1 | # SYNTAX TEST "source.ruby" 2 | require 'open-uri' 3 | 4 | =begin 5 | # <------ comment.block.documentation.ruby punctuation.definition.comment.ruby 6 | foo bar baz 7 | # <------------ comment.block.documentation.ruby 8 | =end 9 | # <---- comment.block.documentation.ruby punctuation.definition.comment.ruby 10 | 11 | module Blam 12 | # <------ meta.module.ruby keyword.control.module.ruby 13 | # ^^^^ meta.module.ruby entity.name.type.module.ruby 14 | class Rawr 15 | # ^^^^^ meta.class.ruby keyword.control.class.ruby 16 | # ^^^^ meta.class.ruby entity.name.type.class.ruby 17 | end 18 | # ^^^ keyword.control.ruby 19 | end 20 | class MoreExamples 21 | FOO = :bar 22 | # ^^^ variable.other.constant.ruby 23 | # ^^^^ constant.language.symbol.ruby 24 | end 25 | class ExampleClass < AnotherClass 26 | # ^ entity.other.inherited-class.ruby 27 | # ^^^^^^^^^^^^ entity.other.inherited-class.ruby 28 | attr_reader :foo, :bar 29 | # ^^^^^^^^^^^ keyword.other.special-method.ruby 30 | attr_writer :baz, :bam 31 | # ^^^^^^^^^^^ keyword.other.special-method.ruby 32 | attr_accessor :boom 33 | # ^^^^^^^^^^^^^ keyword.other.special-method.ruby 34 | 35 | def initialize 36 | # ^^^ keyword.control.def.ruby 37 | # ^^^^^^^^^^ meta.function.method.without-arguments.ruby entity.name.function.ruby 38 | a_method(arg1) 39 | # ^^^^^^^^ meta.function-call.ruby entity.name.function.ruby 40 | # ^ punctuation.section.function.ruby 41 | # ^^^^ variable.other.ruby 42 | # ^ punctuation.section.function.ruby 43 | end 44 | 45 | def a_method(arg1) 46 | # ^^^^^^^^ meta.function.method.with-arguments.ruby entity.name.function.ruby 47 | hello_world = 10 48 | # ^^^^^^^^^^^ variable.other.ruby 49 | # ^ keyword.operator.assignment.ruby 50 | # ^^ constant.numeric.ruby 51 | hello_world += 1 52 | # ^^ keyword.operator.assignment.augmented.ruby 53 | SomeClass::AnotherClass.new(key: 10, another_key: "value") 54 | # ^^^^^^^^^ support.class.ruby 55 | # ^^ punctuation.separator.namespace.ruby 56 | # ^^^^^^^^^^^^ support.class.ruby 57 | # ^ punctuation.separator.method.ruby 58 | # ^^^ keyword.other.special-method.ruby 59 | return :some_symbol 60 | # ^^^^^^ keyword.control.pseudo-method.ruby 61 | 62 | [].each do 63 | # ^ punctuation.section.array.begin.ruby 64 | # ^ punctuation.section.array.end.ruby 65 | # ^^ keyword.control.start-block.ruby 66 | end 67 | 68 | end 69 | 70 | def true?(obj) 71 | !!obj 72 | # ^^ keyword.operator.logical.ruby 73 | end 74 | 75 | def self.do 76 | # ^^^ keyword.control.def.ruby 77 | # ^^^^^^^ meta.function.method.without-arguments.ruby entity.name.function.ruby 78 | @do ||= {} 79 | # ^ variable.other.readwrite.instance.ruby punctuation.definition.variable.ruby 80 | # ^^ variable.other.readwrite.instance.ruby 81 | # ^^^ keyword.operator.assignment.augmented.ruby 82 | # ^ punctuation.section.scope.begin.ruby 83 | # ^ punctuation.section.scope.end.ruby 84 | end 85 | # ^^^ keyword.control.ruby 86 | end 87 | 88 | 89 | weird_method :do || true 90 | # ^^^^^^^^^^^^ variable.other.ruby 91 | # ^ constant.language.symbol.ruby punctuation.definition.constant.ruby 92 | # ^^ constant.language.symbol.ruby 93 | # ^^ keyword.operator.logical.ruby 94 | # ^^^^ constant.language.boolean.ruby 95 | -------------------------------------------------------------------------------- /packages/web-tree-sitter-ruby/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyide/vscode-ruby/79031948d032e97cf2a5116e27c167ade5125e15/packages/web-tree-sitter-ruby/README.md -------------------------------------------------------------------------------- /packages/web-tree-sitter-ruby/index.d.ts: -------------------------------------------------------------------------------- 1 | declare const _default: string; 2 | export default _default; 3 | -------------------------------------------------------------------------------- /packages/web-tree-sitter-ruby/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const path = require('path'); 4 | exports.default = path.resolve(__dirname, 'tree-sitter-ruby.wasm'); 5 | -------------------------------------------------------------------------------- /packages/web-tree-sitter-ruby/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-tree-sitter-ruby", 3 | "version": "0.19.0", 4 | "description": "Tree-sitter bindings for the web", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/rubyide/vscode-ruby" 10 | }, 11 | "scripts": { 12 | "build": "tree-sitter build-wasm --docker ../../node_modules/tree-sitter-ruby" 13 | }, 14 | "keywords": [ 15 | "incremental", 16 | "parsing" 17 | ], 18 | "author": "Stafford Brunk ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/rubyide/vscode-ruby/issues" 22 | }, 23 | "homepage": "https://github.com/rubyide/vscode-ruby", 24 | "devDependencies": { 25 | "tree-sitter-cli": "^0.19.5", 26 | "tree-sitter-ruby": "^0.19.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/web-tree-sitter-ruby/tree-sitter-ruby.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyide/vscode-ruby/79031948d032e97cf2a5116e27c167ade5125e15/packages/web-tree-sitter-ruby/tree-sitter-ruby.wasm --------------------------------------------------------------------------------