ruby-lsp 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 25bad06533efa8072f0d501520e7429220be622d06a9061eb02171e1d515a498
4
- data.tar.gz: dc8cc9436d3b57b362661ba36b8f02b90495287386f94ac5b20916ce5e6025bf
3
+ metadata.gz: 58ed87185da0a5d1da74b10f1525f1ca1341f1001f3a4cbdf2b086dd93750ad9
4
+ data.tar.gz: 1210f736e4cd8450cbe1e1d796dbd22a889c244c6ea5902bc6082f3791064ddc
5
5
  SHA512:
6
- metadata.gz: b8ae688533a36c018cfbca467f7db2b54d121787cff48b7fd56723c03790df34c99a2ace3a0770ee6e3971b5ac1aa5e559b9264d1466ba93556f3d9ec051abaa
7
- data.tar.gz: 1db946d571c62209b40fc4aef7048ea55f39c446bf87ff638d44aefcbdf2e8abdfad79c60ec5e9f0c3dc1fdda78f867582e4f58b56c861941197651bd286e6b3
6
+ metadata.gz: a1ed47a3e4d53dbf5250beaa98b3095c89099e67829f2ecac3149b34ecc87ae579c67ed6cd67035ee9930c0fffa0db6c0d4fdc3c3398520196005b180f28ea6f
7
+ data.tar.gz: 18d07a7c5426fa8cfad11cfab44dbabf05fd3a1352f1bf9272b04fb35c78f0bb0b4860dd4b7bbfda89efb9cfa99fc3ef77fbb679ff2b21bbd4cffb3c53a14bd5
@@ -4,7 +4,7 @@ on: [push, pull_request]
4
4
 
5
5
  jobs:
6
6
  build:
7
- runs-on: self-hosted
7
+ runs-on: ubuntu-latest
8
8
  strategy:
9
9
  fail-fast: false
10
10
  matrix:
@@ -17,6 +17,8 @@ jobs:
17
17
  with:
18
18
  ruby-version: ${{ matrix.ruby }}
19
19
  bundler-cache: true
20
+ - name: Check if documentation is up to date
21
+ run: bundle exec rake check_docs
20
22
  - name: Lint Ruby files
21
23
  run: bin/rubocop
22
24
  - name: Run tests
@@ -0,0 +1,32 @@
1
+ name: Publish docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ name: Publish documentation website
11
+ steps:
12
+ - uses: actions/checkout@v3
13
+
14
+ - name: Set up Ruby
15
+ uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: 3.1.1
18
+ bundler-cache: true
19
+
20
+ - name: Configure git
21
+ run: |
22
+ git config user.name github-actions
23
+ git config user.email github-actions@github.com
24
+
25
+ - name: Generate documentation
26
+ run: bundle exec rake yard
27
+
28
+ - name: Commit to gh-pages
29
+ run: |
30
+ git add docs
31
+ git commit -m "Publish website $(git log --format=format:%h -1)"
32
+ git push --force origin main:gh-pages
data/Gemfile CHANGED
@@ -8,6 +8,7 @@ gem "debug", "~> 1.5"
8
8
  gem "minitest", "~> 5.15"
9
9
  gem "minitest-reporters", "~> 1.5"
10
10
  gem "rake", "~> 13.0"
11
- gem "rubocop-shopify", "~> 2.5"
11
+ gem "rubocop-shopify", "~> 2.6"
12
12
  gem "rubocop-minitest", "~> 0.19.1"
13
13
  gem "rubocop-rake", "~> 0.6.0"
14
+ gem "yard", "~> 0.9"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ruby-lsp (0.0.2)
4
+ ruby-lsp (0.0.3)
5
5
  language_server-protocol
6
6
  rubocop (>= 1.0)
7
7
  syntax_tree (>= 2.3)
@@ -28,32 +28,37 @@ GEM
28
28
  parallel (1.22.1)
29
29
  parser (3.1.2.0)
30
30
  ast (~> 2.4.1)
31
+ prettier_print (0.1.0)
31
32
  rainbow (3.1.1)
32
33
  rake (13.0.6)
33
- regexp_parser (2.3.1)
34
+ regexp_parser (2.4.0)
34
35
  reline (0.3.1)
35
36
  io-console (~> 0.5)
36
37
  rexml (3.2.5)
37
- rubocop (1.28.1)
38
+ rubocop (1.29.1)
38
39
  parallel (~> 1.10)
39
40
  parser (>= 3.1.0.0)
40
41
  rainbow (>= 2.2.2, < 4.0)
41
42
  regexp_parser (>= 1.8, < 3.0)
42
- rexml
43
+ rexml (>= 3.2.5, < 4.0)
43
44
  rubocop-ast (>= 1.17.0, < 2.0)
44
45
  ruby-progressbar (~> 1.7)
45
46
  unicode-display_width (>= 1.4.0, < 3.0)
46
- rubocop-ast (1.17.0)
47
+ rubocop-ast (1.18.0)
47
48
  parser (>= 3.1.1.0)
48
49
  rubocop-minitest (0.19.1)
49
50
  rubocop (>= 0.90, < 2.0)
50
51
  rubocop-rake (0.6.0)
51
52
  rubocop (~> 1.0)
52
- rubocop-shopify (2.5.0)
53
- rubocop (~> 1.25)
53
+ rubocop-shopify (2.6.0)
54
+ rubocop (~> 1.29)
54
55
  ruby-progressbar (1.11.0)
55
- syntax_tree (2.3.1)
56
+ syntax_tree (2.7.1)
57
+ prettier_print
56
58
  unicode-display_width (2.1.0)
59
+ webrick (1.7.0)
60
+ yard (0.9.27)
61
+ webrick (~> 1.7.0)
57
62
 
58
63
  PLATFORMS
59
64
  arm64-darwin-21
@@ -66,8 +71,9 @@ DEPENDENCIES
66
71
  rake (~> 13.0)
67
72
  rubocop-minitest (~> 0.19.1)
68
73
  rubocop-rake (~> 0.6.0)
69
- rubocop-shopify (~> 2.5)
74
+ rubocop-shopify (~> 2.6)
70
75
  ruby-lsp!
76
+ yard (~> 0.9)
71
77
 
72
78
  BUNDLED WITH
73
79
  2.3.7
data/README.md CHANGED
@@ -1,4 +1,21 @@
1
- # ruby-lsp
1
+ ![Build Status](https://github.com/Shopify/ruby-lsp/workflows/CI/badge.svg)
2
+
3
+ # Ruby LSP
4
+
5
+ This gem is an implementation of the language server protocol specification for Ruby, used to improve editor features.
6
+
7
+ ## Usage
8
+
9
+ Install the gem. There's no need to require it, since the server is used as a standalone executable.
10
+
11
+ ```ruby
12
+ group :development do
13
+ gem "ruby-lsp", require: false
14
+ end
15
+ ```
16
+
17
+ If using VS Code, install the [Ruby LSP plugin](https://github.com/Shopify/vscode-ruby-lsp) to get the extra features in
18
+ the editor.
2
19
 
3
20
  ## Contributing
4
21
 
data/Rakefile CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/testtask"
5
+ require "yard"
5
6
 
6
7
  Rake::TestTask.new(:test) do |t|
7
8
  t.libs << "test"
@@ -9,6 +10,10 @@ Rake::TestTask.new(:test) do |t|
9
10
  t.test_files = FileList["test/**/*_test.rb"]
10
11
  end
11
12
 
13
+ YARD::Rake::YardocTask.new do |t|
14
+ t.options = ["--markup", "markdown", "--output-dir", "docs"]
15
+ end
16
+
12
17
  require "rubocop/rake_task"
13
18
 
14
19
  RuboCop::RakeTask.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.2
1
+ 0.0.3
data/exe/ruby-lsp CHANGED
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "syntax_tree"
5
+ require "ruby-lsp"
5
6
  require "ruby_lsp/cli"
6
7
 
7
8
  RubyLsp::Cli.start(ARGV)
data/lib/ruby-lsp.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLsp
4
+ VERSION = File.read(File.expand_path("../VERSION", __dir__)).strip
4
5
  end
data/lib/ruby_lsp/cli.rb CHANGED
@@ -12,13 +12,15 @@ module RubyLsp
12
12
  handler.config do
13
13
  on("initialize") do |request|
14
14
  store.clear
15
- respond_with_capabilities(request.dig(:params, :initializationOptions).fetch(:enabledFeatures, []))
15
+ initialization_options = request.dig(:params, :initializationOptions)
16
+
17
+ configure_options(initialization_options)
18
+ respond_with_capabilities(initialization_options.fetch(:enabledFeatures, []))
16
19
  end
17
20
 
18
21
  on("textDocument/didChange") do |request|
19
22
  uri = request.dig(:params, :textDocument, :uri)
20
- text = request.dig(:params, :contentChanges, 0, :text)
21
- store.set(uri, text)
23
+ store.push_edits(uri, request.dig(:params, :contentChanges))
22
24
 
23
25
  send_diagnostics(uri)
24
26
  nil
@@ -48,6 +50,13 @@ module RubyLsp
48
50
  respond_with_folding_ranges(request.dig(:params, :textDocument, :uri))
49
51
  end
50
52
 
53
+ on("textDocument/selectionRange") do |request|
54
+ respond_with_selection_ranges(
55
+ request.dig(:params, :textDocument, :uri),
56
+ request.dig(:params, :positions),
57
+ )
58
+ end
59
+
51
60
  on("textDocument/semanticTokens/full") do |request|
52
61
  respond_with_semantic_highlighting(request.dig(:params, :textDocument, :uri))
53
62
  end
@@ -1,14 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "strscan"
4
+
3
5
  module RubyLsp
4
6
  class Document
5
- attr_reader :tree, :parser, :source
7
+ attr_reader :tree, :source, :syntax_error_edits
6
8
 
7
9
  def initialize(source)
8
- @source = source
9
- @parser = SyntaxTree::Parser.new(source)
10
- @tree = @parser.parse
10
+ @tree = SyntaxTree.parse(source)
11
11
  @cache = {}
12
+ @syntax_error_edits = []
13
+ @source = source
14
+ @parsable_source = source.dup
12
15
  end
13
16
 
14
17
  def ==(other)
@@ -23,5 +26,65 @@ module RubyLsp
23
26
  @cache[request_name] = result
24
27
  result
25
28
  end
29
+
30
+ def push_edits(edits)
31
+ # Apply the edits on the real source
32
+ edits.each { |edit| apply_edit(@source, edit[:range], edit[:text]) }
33
+
34
+ @cache.clear
35
+ @tree = SyntaxTree.parse(@source)
36
+ @syntax_error_edits.clear
37
+ @parsable_source = @source.dup
38
+ nil
39
+ rescue SyntaxTree::Parser::ParseError
40
+ update_parsable_source(edits)
41
+ end
42
+
43
+ def syntax_errors?
44
+ @syntax_error_edits.any?
45
+ end
46
+
47
+ private
48
+
49
+ def update_parsable_source(edits)
50
+ # If the new edits caused a syntax error, make all edits blank spaces and line breaks to adjust the line and
51
+ # column numbers. This is attempt to make the document parsable while partial edits are being applied
52
+ edits.each do |edit|
53
+ @syntax_error_edits << edit
54
+ next if edit[:text].empty? # skip deletions, since they may have caused the syntax error
55
+
56
+ apply_edit(@parsable_source, edit[:range], edit[:text].gsub(/[^\r\n]/, " "))
57
+ end
58
+
59
+ @tree = SyntaxTree.parse(@parsable_source)
60
+ rescue SyntaxTree::Parser::ParseError
61
+ # If we can't parse the source even after emptying the edits, then just fallback to the previous source
62
+ end
63
+
64
+ def apply_edit(source, range, text)
65
+ scanner = Scanner.new(source)
66
+ start_position = scanner.find_position(range[:start])
67
+ end_position = scanner.find_position(range[:end])
68
+
69
+ source[start_position...end_position] = text
70
+ end
71
+
72
+ class Scanner
73
+ def initialize(source)
74
+ @source = source
75
+ @scanner = StringScanner.new(source)
76
+ @current_line = 0
77
+ end
78
+
79
+ def find_position(position)
80
+ # Move the string scanner counting line breaks until we reach the right line
81
+ until @current_line == position[:line]
82
+ @scanner.scan_until(/\R/)
83
+ @current_line += 1
84
+ end
85
+
86
+ @scanner.pos + position[:character]
87
+ end
88
+ end
26
89
  end
27
90
  end
@@ -2,9 +2,15 @@
2
2
 
3
3
  require "ruby_lsp/requests"
4
4
  require "ruby_lsp/store"
5
+ require "benchmark"
5
6
 
6
7
  module RubyLsp
7
8
  class Handler
9
+ IGNORED_FOR_TELEMETRY = [
10
+ "initialized",
11
+ "$/cancelRequest",
12
+ ].freeze
13
+
8
14
  attr_reader :store
9
15
 
10
16
  Interface = LanguageServer::Protocol::Interface
@@ -21,7 +27,7 @@ module RubyLsp
21
27
  def start
22
28
  $stderr.puts "Starting Ruby LSP..."
23
29
  @reader.read do |request|
24
- handle(request)
30
+ with_telemetry(request) { handle(request) }
25
31
  end
26
32
  end
27
33
 
@@ -76,9 +82,10 @@ module RubyLsp
76
82
  Interface::InitializeResult.new(
77
83
  capabilities: Interface::ServerCapabilities.new(
78
84
  text_document_sync: Interface::TextDocumentSyncOptions.new(
79
- change: Constant::TextDocumentSyncKind::FULL,
85
+ change: Constant::TextDocumentSyncKind::INCREMENTAL,
80
86
  open_close: true,
81
87
  ),
88
+ selection_range_provider: enabled_features.include?("selectionRanges"),
82
89
  document_symbol_provider: document_symbol_provider,
83
90
  folding_range_provider: folding_ranges_provider,
84
91
  semantic_tokens_provider: semantic_tokens_provider,
@@ -100,9 +107,25 @@ module RubyLsp
100
107
  end
101
108
  end
102
109
 
110
+ def respond_with_selection_ranges(uri, positions)
111
+ ranges = store.cache_fetch(uri, :selection_ranges) do |document|
112
+ Requests::SelectionRanges.run(document)
113
+ end
114
+
115
+ # Per the selection range request spec (https://microsoft.github.io/language-server-protocol/specification#textDocument_selectionRange),
116
+ # every position in the positions array should have an element at the same index in the response
117
+ # array. For positions without a valid selection range, the corresponding element in the response
118
+ # array will be nil.
119
+ positions.map do |position|
120
+ ranges.find do |range|
121
+ range.cover?(position)
122
+ end
123
+ end
124
+ end
125
+
103
126
  def respond_with_semantic_highlighting(uri)
104
127
  store.cache_fetch(uri, :semantic_highlighting) do |document|
105
- Requests::SemanticHighlighting.run(document)
128
+ Requests::SemanticHighlighting.new(document, encoder: Requests::Support::SemanticTokenEncoder.new).run
106
129
  end
107
130
  end
108
131
 
@@ -129,5 +152,30 @@ module RubyLsp
129
152
  Requests::CodeActions.run(uri, document, range)
130
153
  end
131
154
  end
155
+
156
+ def configure_options(initialization_options)
157
+ @telemetry_enabled = initialization_options.fetch(:telemetryEnabled, false)
158
+ end
159
+
160
+ def with_telemetry(request)
161
+ return yield unless @telemetry_enabled && !IGNORED_FOR_TELEMETRY.include?(request[:method])
162
+
163
+ result = nil
164
+ request_time = Benchmark.realtime do
165
+ result = yield
166
+ end
167
+
168
+ params = {
169
+ request: request[:method],
170
+ requestTime: request_time,
171
+ lspVersion: RubyLsp::VERSION,
172
+ }
173
+
174
+ uri = request.dig(:params, :textDocument, :uri)
175
+ params[:uri] = uri if uri
176
+
177
+ @writer.write(method: "telemetry/event", params: params)
178
+ result
179
+ end
132
180
  end
133
181
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module RubyLsp
4
4
  module Requests
5
+ # :nodoc:
5
6
  class BaseRequest < SyntaxTree::Visitor
6
7
  def self.run(document)
7
8
  new(document).run
@@ -2,6 +2,17 @@
2
2
 
3
3
  module RubyLsp
4
4
  module Requests
5
+ # The [code actions](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction)
6
+ # request informs the editor of RuboCop quick fixes that can be applied. These are accesible by hovering over a
7
+ # specific diagnostic.
8
+ #
9
+ # # Example
10
+ #
11
+ # ```ruby
12
+ # def say_hello
13
+ # puts "Hello" # --> code action: quick fix indentation
14
+ # end
15
+ # ```
5
16
  class CodeActions
6
17
  def self.run(uri, document, range)
7
18
  new(uri, document, range).run
@@ -16,7 +27,7 @@ module RubyLsp
16
27
  def run
17
28
  diagnostics = Diagnostics.run(@uri, @document)
18
29
  corrections = diagnostics.select { |diagnostic| diagnostic.correctable? && diagnostic.in_range?(@range) }
19
- return if corrections.empty?
30
+ return [] if corrections.empty?
20
31
 
21
32
  corrections.map!(&:to_lsp_code_action)
22
33
  end
@@ -2,95 +2,34 @@
2
2
 
3
3
  module RubyLsp
4
4
  module Requests
5
+ # The
6
+ # [diagnostics](https://microsoft.github.io/language-server-protocol/specification#textDocument_publishDiagnostics)
7
+ # request informs the editor of RuboCop offenses for a given file.
8
+ #
9
+ # # Example
10
+ #
11
+ # ```ruby
12
+ # def say_hello
13
+ # puts "Hello" # --> diagnostics: incorrect indentantion
14
+ # end
15
+ # ```
5
16
  class Diagnostics < RuboCopRequest
6
- RUBOCOP_TO_LSP_SEVERITY = {
7
- convention: LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION,
8
- info: LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION,
9
- refactor: LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION,
10
- warning: LanguageServer::Protocol::Constant::DiagnosticSeverity::WARNING,
11
- error: LanguageServer::Protocol::Constant::DiagnosticSeverity::ERROR,
12
- fatal: LanguageServer::Protocol::Constant::DiagnosticSeverity::ERROR,
13
- }.freeze
14
-
15
17
  def run
18
+ return syntax_error_diagnostics if @document.syntax_errors?
19
+
16
20
  super
17
21
 
18
22
  @diagnostics
19
23
  end
20
24
 
21
25
  def file_finished(_file, offenses)
22
- @diagnostics = offenses.map { |offense| Diagnostic.new(offense, @uri) }
26
+ @diagnostics = offenses.map { |offense| Support::RuboCopDiagnostic.new(offense, @uri) }
23
27
  end
24
28
 
25
- class Diagnostic
26
- attr_reader :replacements
27
-
28
- def initialize(offense, uri)
29
- @offense = offense
30
- @uri = uri
31
- @replacements = offense.correctable? ? offense_replacements : []
32
- end
33
-
34
- def correctable?
35
- @offense.correctable?
36
- end
37
-
38
- def in_range?(range)
39
- range.cover?(@offense.line - 1)
40
- end
41
-
42
- def to_lsp_code_action
43
- LanguageServer::Protocol::Interface::CodeAction.new(
44
- title: "Autocorrect #{@offense.cop_name}",
45
- kind: LanguageServer::Protocol::Constant::CodeActionKind::QUICK_FIX,
46
- edit: LanguageServer::Protocol::Interface::WorkspaceEdit.new(
47
- document_changes: [
48
- LanguageServer::Protocol::Interface::TextDocumentEdit.new(
49
- text_document: LanguageServer::Protocol::Interface::OptionalVersionedTextDocumentIdentifier.new(
50
- uri: @uri,
51
- version: nil
52
- ),
53
- edits: @replacements
54
- ),
55
- ]
56
- ),
57
- is_preferred: true,
58
- )
59
- end
60
-
61
- def to_lsp_diagnostic
62
- LanguageServer::Protocol::Interface::Diagnostic.new(
63
- message: @offense.message,
64
- source: "RuboCop",
65
- code: @offense.cop_name,
66
- severity: RUBOCOP_TO_LSP_SEVERITY[@offense.severity.name],
67
- range: LanguageServer::Protocol::Interface::Range.new(
68
- start: LanguageServer::Protocol::Interface::Position.new(
69
- line: @offense.line - 1,
70
- character: @offense.column
71
- ),
72
- end: LanguageServer::Protocol::Interface::Position.new(
73
- line: @offense.last_line - 1,
74
- character: @offense.last_column
75
- )
76
- )
77
- )
78
- end
79
-
80
- private
29
+ private
81
30
 
82
- def offense_replacements
83
- @offense.corrector.as_replacements.map do |range, replacement|
84
- LanguageServer::Protocol::Interface::TextEdit.new(
85
- range: LanguageServer::Protocol::Interface::Range.new(
86
- start: LanguageServer::Protocol::Interface::Position.new(line: range.line - 1, character: range.column),
87
- end: LanguageServer::Protocol::Interface::Position.new(line: range.last_line - 1,
88
- character: range.last_column)
89
- ),
90
- new_text: replacement
91
- )
92
- end
93
- end
31
+ def syntax_error_diagnostics
32
+ @document.syntax_error_edits.map { |e| Support::SyntaxErrorDiagnostic.new(e) }
94
33
  end
95
34
  end
96
35
  end
@@ -2,6 +2,27 @@
2
2
 
3
3
  module RubyLsp
4
4
  module Requests
5
+ # The [document
6
+ # symbol](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol) request
7
+ # informs the editor of all the important symbols, such as classes, variables, and methods, defined in a file. With
8
+ # this information, the editor can populate breadcrumbs, file outline and allow for fuzzy symbol searches.
9
+ #
10
+ # In VS Code, fuzzy symbol search can be accessed by opened the command palette and inserting an `@` symbol.
11
+ #
12
+ # # Example
13
+ #
14
+ # ```ruby
15
+ # class Person # --> document symbol: class
16
+ # attr_reader :age # --> document symbol: field
17
+ #
18
+ # def initialize
19
+ # @age = 0 # --> document symbol: variable
20
+ # end
21
+ #
22
+ # def age # --> document symbol: method
23
+ # end
24
+ # end
25
+ # ```
5
26
  class DocumentSymbol < BaseRequest
6
27
  SYMBOL_KIND = {
7
28
  file: 1,
@@ -2,6 +2,15 @@
2
2
 
3
3
  module RubyLsp
4
4
  module Requests
5
+ # The [folding ranges](https://microsoft.github.io/language-server-protocol/specification#textDocument_foldingRange)
6
+ # request informs the editor of the ranges where code can be folded.
7
+ #
8
+ # # Example
9
+ # ```ruby
10
+ # def say_hello # <-- folding range start
11
+ # puts "Hello"
12
+ # end # <-- folding range end
13
+ # ```
5
14
  class FoldingRanges < BaseRequest
6
15
  SIMPLE_FOLDABLES = [
7
16
  SyntaxTree::ArrayLiteral,
@@ -2,6 +2,17 @@
2
2
 
3
3
  module RubyLsp
4
4
  module Requests
5
+ # The [formatting](https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting)
6
+ # request uses RuboCop to fix auto-correctable offenses in the document. This requires enabling format on save and
7
+ # registering the ruby-lsp as the Ruby formatter.
8
+ #
9
+ # # Example
10
+ #
11
+ # ```ruby
12
+ # def say_hello
13
+ # puts "Hello" # --> formatting: fixes the indentation on save
14
+ # end
15
+ # ```
5
16
  class Formatting < RuboCopRequest
6
17
  RUBOCOP_FLAGS = (COMMON_RUBOCOP_FLAGS + ["--auto-correct"]).freeze
7
18
 
@@ -5,6 +5,7 @@ require "cgi"
5
5
 
6
6
  module RubyLsp
7
7
  module Requests
8
+ # :nodoc:
8
9
  class RuboCopRequest < RuboCop::Runner
9
10
  COMMON_RUBOCOP_FLAGS = [
10
11
  "--stderr", # Print any output to stderr so that our stdout does not get polluted
@@ -20,6 +21,7 @@ module RubyLsp
20
21
 
21
22
  def initialize(uri, document)
22
23
  @file = CGI.unescape(URI.parse(uri).path)
24
+ @document = document
23
25
  @text = document.source
24
26
  @uri = uri
25
27
 
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Requests
5
+ # The [selection ranges](https://microsoft.github.io/language-server-protocol/specification#textDocument_selectionRange)
6
+ # request informs the editor of ranges that the user may want to select based on the location(s)
7
+ # of their cursor(s).
8
+ #
9
+ # Trigger this request with: Ctrl + Shift + -> or Ctrl + Shift + <-
10
+ #
11
+ # # Example
12
+ #
13
+ # ```ruby
14
+ # def foo # --> The next selection range encompasses the entire method definition.
15
+ # puts "Hello, world!" # --> Cursor is on this line
16
+ # end
17
+ # ```
18
+ class SelectionRanges < BaseRequest
19
+ NODES_THAT_CAN_BE_PARENTS = [
20
+ SyntaxTree::Assign,
21
+ SyntaxTree::ArrayLiteral,
22
+ SyntaxTree::Begin,
23
+ SyntaxTree::BraceBlock,
24
+ SyntaxTree::Call,
25
+ SyntaxTree::Case,
26
+ SyntaxTree::ClassDeclaration,
27
+ SyntaxTree::Command,
28
+ SyntaxTree::Def,
29
+ SyntaxTree::Defs,
30
+ SyntaxTree::DoBlock,
31
+ SyntaxTree::Elsif,
32
+ SyntaxTree::Else,
33
+ SyntaxTree::EmbDoc,
34
+ SyntaxTree::Ensure,
35
+ SyntaxTree::FCall,
36
+ SyntaxTree::For,
37
+ SyntaxTree::HashLiteral,
38
+ SyntaxTree::Heredoc,
39
+ SyntaxTree::HeredocBeg,
40
+ SyntaxTree::HshPtn,
41
+ SyntaxTree::If,
42
+ SyntaxTree::In,
43
+ SyntaxTree::Lambda,
44
+ SyntaxTree::MethodAddBlock,
45
+ SyntaxTree::ModuleDeclaration,
46
+ SyntaxTree::Params,
47
+ SyntaxTree::Rescue,
48
+ SyntaxTree::RescueEx,
49
+ SyntaxTree::StringConcat,
50
+ SyntaxTree::StringLiteral,
51
+ SyntaxTree::Unless,
52
+ SyntaxTree::Until,
53
+ SyntaxTree::VCall,
54
+ SyntaxTree::When,
55
+ SyntaxTree::While,
56
+ ].freeze
57
+
58
+ def initialize(document)
59
+ super(document)
60
+
61
+ @ranges = []
62
+ @stack = []
63
+ end
64
+
65
+ def run
66
+ visit(@document.tree)
67
+ @ranges.reverse!
68
+ end
69
+
70
+ private
71
+
72
+ def visit(node)
73
+ return if node.nil?
74
+
75
+ range = create_selection_range(node.location, @stack.last)
76
+
77
+ @ranges << range
78
+ return if node.child_nodes.empty?
79
+
80
+ @stack << range if NODES_THAT_CAN_BE_PARENTS.include?(node.class)
81
+ visit_all(node.child_nodes)
82
+ @stack.pop if NODES_THAT_CAN_BE_PARENTS.include?(node.class)
83
+ end
84
+
85
+ def create_selection_range(location, parent = nil)
86
+ RubyLsp::Requests::Support::SelectionRange.new(
87
+ range: LanguageServer::Protocol::Interface::Range.new(
88
+ start: LanguageServer::Protocol::Interface::Position.new(
89
+ line: location.start_line - 1,
90
+ character: location.start_column,
91
+ ),
92
+ end: LanguageServer::Protocol::Interface::Position.new(
93
+ line: location.end_line - 1,
94
+ character: location.end_column,
95
+ ),
96
+ ),
97
+ parent: parent
98
+ )
99
+ end
100
+ end
101
+ end
102
+ end
@@ -2,6 +2,19 @@
2
2
 
3
3
  module RubyLsp
4
4
  module Requests
5
+ # The [semantic
6
+ # highlighting](https://microsoft.github.io/language-server-protocol/specification#textDocument_semanticTokens)
7
+ # request informs the editor of the correct token types to provide consistent and accurate highlighting for themes.
8
+ #
9
+ # # Example
10
+ #
11
+ # ```ruby
12
+ # def foo
13
+ # var = 1 # --> semantic highlighting: local variable
14
+ # some_invocation # --> semantic highlighting: method invocation
15
+ # var # --> semantic highlighting: local variable
16
+ # end
17
+ # ```
5
18
  class SemanticHighlighting < BaseRequest
6
19
  TOKEN_TYPES = [
7
20
  :variable,
@@ -9,18 +22,21 @@ module RubyLsp
9
22
  ].freeze
10
23
  TOKEN_MODIFIERS = [].freeze
11
24
 
12
- def initialize(document)
13
- super
25
+ SemanticToken = Struct.new(:location, :length, :type, :modifier)
14
26
 
27
+ def initialize(document, encoder: nil)
28
+ super(document)
29
+
30
+ @encoder = encoder
15
31
  @tokens = []
16
32
  @tree = document.tree
17
- @current_row = 0
18
- @current_column = 0
19
33
  end
20
34
 
21
35
  def run
22
36
  visit(@tree)
23
- LanguageServer::Protocol::Interface::SemanticTokens.new(data: @tokens)
37
+ return @tokens unless @encoder
38
+
39
+ @encoder.encode(@tokens)
24
40
  end
25
41
 
26
42
  def visit_m_assign(node)
@@ -73,44 +89,10 @@ module RubyLsp
73
89
  add_token(node.value.location, :method)
74
90
  end
75
91
 
76
- def add_token(location, classification)
92
+ def add_token(location, type)
77
93
  length = location.end_char - location.start_char
78
-
79
- compute_delta(location) do |delta_line, delta_column|
80
- @tokens.push(delta_line, delta_column, length, TOKEN_TYPES.index(classification), 0)
81
- end
82
- end
83
-
84
- # The delta array is computed according to the LSP specification:
85
- # > The protocol for the token format relative uses relative
86
- # > positions, because most tokens remain stable relative to
87
- # > each other when edits are made in a file. This simplifies
88
- # > the computation of a delta if a server supports it. So each
89
- # > token is represented using 5 integers.
90
-
91
- # For more information on how each number is calculated, read:
92
- # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens
93
- def compute_delta(location)
94
- row = location.start_line - 1
95
- column = location.start_column
96
-
97
- if row < @current_row
98
- raise InvalidTokenRowError, "Invalid token row detected: " \
99
- "Ensure tokens are added in the expected order."
100
- end
101
-
102
- delta_line = row - @current_row
103
-
104
- delta_column = column
105
- delta_column -= @current_column if delta_line == 0
106
-
107
- yield delta_line, delta_column
108
-
109
- @current_row = row
110
- @current_column = column
94
+ @tokens.push(SemanticToken.new(location, length, TOKEN_TYPES.index(type), 0))
111
95
  end
112
-
113
- class InvalidTokenRowError < StandardError; end
114
96
  end
115
97
  end
116
98
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Requests
5
+ module Support
6
+ class RuboCopDiagnostic
7
+ RUBOCOP_TO_LSP_SEVERITY = {
8
+ convention: LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION,
9
+ info: LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION,
10
+ refactor: LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION,
11
+ warning: LanguageServer::Protocol::Constant::DiagnosticSeverity::WARNING,
12
+ error: LanguageServer::Protocol::Constant::DiagnosticSeverity::ERROR,
13
+ fatal: LanguageServer::Protocol::Constant::DiagnosticSeverity::ERROR,
14
+ }.freeze
15
+
16
+ attr_reader :replacements
17
+
18
+ def initialize(offense, uri)
19
+ @offense = offense
20
+ @uri = uri
21
+ @replacements = offense.correctable? ? offense_replacements : []
22
+ end
23
+
24
+ def correctable?
25
+ @offense.correctable?
26
+ end
27
+
28
+ def in_range?(range)
29
+ range.cover?(@offense.line - 1)
30
+ end
31
+
32
+ def to_lsp_code_action
33
+ LanguageServer::Protocol::Interface::CodeAction.new(
34
+ title: "Autocorrect #{@offense.cop_name}",
35
+ kind: LanguageServer::Protocol::Constant::CodeActionKind::QUICK_FIX,
36
+ edit: LanguageServer::Protocol::Interface::WorkspaceEdit.new(
37
+ document_changes: [
38
+ LanguageServer::Protocol::Interface::TextDocumentEdit.new(
39
+ text_document: LanguageServer::Protocol::Interface::OptionalVersionedTextDocumentIdentifier.new(
40
+ uri: @uri,
41
+ version: nil
42
+ ),
43
+ edits: @replacements
44
+ ),
45
+ ]
46
+ ),
47
+ is_preferred: true,
48
+ )
49
+ end
50
+
51
+ def to_lsp_diagnostic
52
+ LanguageServer::Protocol::Interface::Diagnostic.new(
53
+ message: @offense.message,
54
+ source: "RuboCop",
55
+ code: @offense.cop_name,
56
+ severity: RUBOCOP_TO_LSP_SEVERITY[@offense.severity.name],
57
+ range: LanguageServer::Protocol::Interface::Range.new(
58
+ start: LanguageServer::Protocol::Interface::Position.new(
59
+ line: @offense.line - 1,
60
+ character: @offense.column
61
+ ),
62
+ end: LanguageServer::Protocol::Interface::Position.new(
63
+ line: @offense.last_line - 1,
64
+ character: @offense.last_column
65
+ )
66
+ )
67
+ )
68
+ end
69
+
70
+ private
71
+
72
+ def offense_replacements
73
+ @offense.corrector.as_replacements.map do |range, replacement|
74
+ LanguageServer::Protocol::Interface::TextEdit.new(
75
+ range: LanguageServer::Protocol::Interface::Range.new(
76
+ start: LanguageServer::Protocol::Interface::Position.new(line: range.line - 1, character: range.column),
77
+ end: LanguageServer::Protocol::Interface::Position.new(line: range.last_line - 1,
78
+ character: range.last_column)
79
+ ),
80
+ new_text: replacement
81
+ )
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Requests
5
+ module Support
6
+ class SelectionRange < LanguageServer::Protocol::Interface::SelectionRange
7
+ def cover?(position)
8
+ line_range = (range.start.line..range.end.line)
9
+ character_range = (range.start.character..range.end.character)
10
+
11
+ line_range.cover?(position[:line]) && character_range.cover?(position[:character])
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Requests
5
+ module Support
6
+ class SemanticTokenEncoder
7
+ def initialize
8
+ @current_row = 0
9
+ @current_column = 0
10
+ end
11
+
12
+ def encode(tokens)
13
+ delta = tokens
14
+ .sort_by do |token|
15
+ [token.location.start_line, token.location.start_column]
16
+ end
17
+ .flat_map do |token|
18
+ compute_delta(token)
19
+ end
20
+
21
+ LanguageServer::Protocol::Interface::SemanticTokens.new(data: delta)
22
+ end
23
+
24
+ # The delta array is computed according to the LSP specification:
25
+ # > The protocol for the token format relative uses relative
26
+ # > positions, because most tokens remain stable relative to
27
+ # > each other when edits are made in a file. This simplifies
28
+ # > the computation of a delta if a server supports it. So each
29
+ # > token is represented using 5 integers.
30
+
31
+ # For more information on how each number is calculated, read:
32
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens
33
+ def compute_delta(token)
34
+ row = token.location.start_line - 1
35
+ column = token.location.start_column
36
+ delta_line = row - @current_row
37
+
38
+ delta_column = column
39
+ delta_column -= @current_column if delta_line == 0
40
+
41
+ [delta_line, delta_column, token.length, token.type, token.modifier]
42
+ ensure
43
+ @current_row = row
44
+ @current_column = column
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Requests
5
+ module Support
6
+ class SyntaxErrorDiagnostic
7
+ def initialize(edit)
8
+ @edit = edit
9
+ end
10
+
11
+ def correctable?
12
+ false
13
+ end
14
+
15
+ def to_lsp_diagnostic
16
+ LanguageServer::Protocol::Interface::Diagnostic.new(
17
+ message: "Syntax error",
18
+ source: "SyntaxTree",
19
+ severity: LanguageServer::Protocol::Constant::DiagnosticSeverity::ERROR,
20
+ range: @edit[:range]
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -5,10 +5,18 @@ module RubyLsp
5
5
  autoload :BaseRequest, "ruby_lsp/requests/base_request"
6
6
  autoload :DocumentSymbol, "ruby_lsp/requests/document_symbol"
7
7
  autoload :FoldingRanges, "ruby_lsp/requests/folding_ranges"
8
+ autoload :SelectionRanges, "ruby_lsp/requests/selection_ranges"
8
9
  autoload :SemanticHighlighting, "ruby_lsp/requests/semantic_highlighting"
9
10
  autoload :RuboCopRequest, "ruby_lsp/requests/rubocop_request"
10
11
  autoload :Formatting, "ruby_lsp/requests/formatting"
11
12
  autoload :Diagnostics, "ruby_lsp/requests/diagnostics"
12
13
  autoload :CodeActions, "ruby_lsp/requests/code_actions"
14
+
15
+ module Support
16
+ autoload :RuboCopDiagnostic, "ruby_lsp/requests/support/rubocop_diagnostic"
17
+ autoload :SelectionRange, "ruby_lsp/requests/support/selection_range"
18
+ autoload :SemanticTokenEncoder, "ruby_lsp/requests/support/semantic_token_encoder"
19
+ autoload :SyntaxErrorDiagnostic, "ruby_lsp/requests/support/syntax_error_diagnostic"
20
+ end
13
21
  end
14
22
  end
@@ -24,6 +24,10 @@ module RubyLsp
24
24
  # Do not update the store if there are syntax errors
25
25
  end
26
26
 
27
+ def push_edits(uri, edits)
28
+ @state[uri].push_edits(edits)
29
+ end
30
+
27
31
  def clear
28
32
  @state.clear
29
33
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc "Check if all LSP requests are documented"
4
+ task :check_docs do
5
+ require "language_server-protocol"
6
+ require "syntax_tree"
7
+ require "logger"
8
+ require "ruby_lsp/requests/base_request"
9
+ require "ruby_lsp/requests/rubocop_request"
10
+
11
+ Dir["#{Dir.pwd}/lib/ruby_lsp/requests/*.rb"].each do |file|
12
+ require(file)
13
+ YARD.parse(file, [], Logger::Severity::FATAL)
14
+ end
15
+
16
+ spec_matcher = %r{\(https://microsoft.github.io/language-server-protocol/specification#.*\)}
17
+ error_messages = RubyLsp::Requests.constants.each_with_object(Hash.new { |h, k| h[k] = [] }) do |request, errors|
18
+ full_name = "RubyLsp::Requests::#{request}"
19
+ docs = YARD::Registry.at(full_name).docstring
20
+ next if /:nodoc:/.match?(docs)
21
+
22
+ if docs.empty?
23
+ errors[full_name] << "Missing documentation for request handler class"
24
+ elsif !spec_matcher.match?(docs)
25
+ errors[full_name] << <<~MESSAGE
26
+ Documentation for request handler classes must link to the official LSP specification.
27
+
28
+ For example, if your request handles text document hover, you should add a link to
29
+ https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover.
30
+ MESSAGE
31
+ elsif !/# Example/.match?(docs)
32
+ errors[full_name] << <<~MESSAGE
33
+ Documentation for request handler class must contain an example.
34
+
35
+ = Example
36
+ def my_method # <-- something happens here
37
+ end
38
+ MESSAGE
39
+ end
40
+ end
41
+
42
+ formatted_errors = error_messages.map { |name, errors| "#{name}: #{errors.join(", ")}" }
43
+
44
+ if error_messages.any?
45
+ puts <<~MESSAGE
46
+ The following requests have invalid documentation:
47
+
48
+ #{formatted_errors.join("\n")}
49
+ MESSAGE
50
+
51
+ exit!
52
+ end
53
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-04-26 00:00:00.000000000 Z
11
+ date: 2022-05-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -64,6 +64,7 @@ files:
64
64
  - ".github/probots.yml"
65
65
  - ".github/pull_request_template.md"
66
66
  - ".github/workflows/ci.yml"
67
+ - ".github/workflows/publish_docs.yml"
67
68
  - ".gitignore"
68
69
  - ".rubocop.yml"
69
70
  - ".vscode/extensions.json"
@@ -93,8 +94,14 @@ files:
93
94
  - lib/ruby_lsp/requests/folding_ranges.rb
94
95
  - lib/ruby_lsp/requests/formatting.rb
95
96
  - lib/ruby_lsp/requests/rubocop_request.rb
97
+ - lib/ruby_lsp/requests/selection_ranges.rb
96
98
  - lib/ruby_lsp/requests/semantic_highlighting.rb
99
+ - lib/ruby_lsp/requests/support/rubocop_diagnostic.rb
100
+ - lib/ruby_lsp/requests/support/selection_range.rb
101
+ - lib/ruby_lsp/requests/support/semantic_token_encoder.rb
102
+ - lib/ruby_lsp/requests/support/syntax_error_diagnostic.rb
97
103
  - lib/ruby_lsp/store.rb
104
+ - rakelib/check_docs.rake
98
105
  - ruby-lsp.gemspec
99
106
  - service.yml
100
107
  - shipit.production.yml