ruby-lsp 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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