Skip to content
This repository has been archived by the owner on Jun 1, 2023. It is now read-only.

Add workspace symbols #106

Merged
merged 12 commits into from
May 6, 2018
2 changes: 1 addition & 1 deletion spec/scry/response_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ module Scry
io = IO::Memory.new
response = Response.new(results)
response.write(io)
io.to_s.should eq(%(Content-Length: 245\r\n\r\n{"jsonrpc":"2.0","id":32,"result":{"capabilities":{"textDocumentSync":1,"documentFormattingProvider":true,"definitionProvider":true,"documentSymbolProvider":true,"completionProvider":{"resolveProvider":true,"triggerCharacters":[".","\\\"","/"]}}}}))
io.to_s.should eq(%(Content-Length: 276\r\n\r\n{"jsonrpc":"2.0","id":32,"result":{"capabilities":{"textDocumentSync":1,"documentFormattingProvider":true,"definitionProvider":true,"documentSymbolProvider":true,"workspaceSymbolProvider":true,"completionProvider":{"resolveProvider":true,"triggerCharacters":[".","\\\"","/"]}}}}))
end
end
end
37 changes: 30 additions & 7 deletions spec/scry/symbol_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,39 @@ module Scry
text_document = TextDocument.new("uri", ["class Test; end"])
processor = SymbolProcessor.new(text_document)
response = processor.run
result = response.result.as(Array(SymbolInformation)).try(&.first)
result = response.result.as(Array(SymbolInformation)).first
result.kind.is_a?(SymbolKind::Class).should be_true
end

it "returns Struct symbols as a Class" do
text_document = TextDocument.new("uri", ["struct Test; end"])
processor = SymbolProcessor.new(text_document)
response = processor.run
result = response.result.as(Array(SymbolInformation)).try(&.first)
result = response.result.as(Array(SymbolInformation)).first
result.kind.is_a?(SymbolKind::Class).should be_true
end

it "returns Module symbols" do
text_document = TextDocument.new("uri", ["module Test; end"])
processor = SymbolProcessor.new(text_document)
response = processor.run
result = response.result.as(Array(SymbolInformation)).try(&.first)
result = response.result.as(Array(SymbolInformation)).first
result.kind.is_a?(SymbolKind::Module).should be_true
end

it "returns Method symbols" do
text_document = TextDocument.new("uri", ["def test; end"])
processor = SymbolProcessor.new(text_document)
response = processor.run
result = response.result.as(Array(SymbolInformation)).try(&.first)
result = response.result.as(Array(SymbolInformation)).first
result.kind.is_a?(SymbolKind::Method).should be_true
end

it "returns instance vars as Variable symbols" do
text_document = TextDocument.new("uri", ["@bar = nil"])
processor = SymbolProcessor.new(text_document)
response = processor.run
result = response.result.as(Array(SymbolInformation)).try(&.first)
result = response.result.as(Array(SymbolInformation)).first
result.as(SymbolInformation).kind.is_a?(SymbolKind::Variable).should be_true
end

Expand Down Expand Up @@ -86,17 +86,40 @@ module Scry
text_document = TextDocument.new("uri", [%(HELLO = "world")])
processor = SymbolProcessor.new(text_document)
response = processor.run
result = response.result.as(Array(SymbolInformation)).try(&.first)
result = response.result.as(Array(SymbolInformation)).first
result.kind.is_a?(SymbolKind::Constant).should be_true
end

it "returns alias as Constant symbols" do
text_document = TextDocument.new("uri", [%(alias Hello = World)])
processor = SymbolProcessor.new(text_document)
response = processor.run
result = response.result.as(Array(SymbolInformation)).try(&.first)
result = response.result.as(Array(SymbolInformation)).first
result.kind.is_a?(SymbolKind::Constant).should be_true
end
end

describe "WorkspaceSymbols" do
it "return empty Symbols list if no query" do
processor = WorkspaceSymbolProcessor.new(0, ROOT_PATH, "")
response = processor.run
result = response.result.as(Array(SymbolInformation))
result.empty?.should be_true
end

it "return Symbols list with query match" do
processor = WorkspaceSymbolProcessor.new(0, "#{ROOT_PATH}/src", "salut")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use regex here? Or add another spec for a regex query?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! 👍

Copy link
Contributor

@bew bew May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little Use File.join commit ? 😃

response = processor.run
result = response.result.as(Array(SymbolInformation)).first
result.kind.is_a?(SymbolKind::Method).should be_true
end

it "return Symbols list with regex query match" do
processor = WorkspaceSymbolProcessor.new(0, "#{ROOT_PATH}/src", "sal*")
response = processor.run
result = response.result.as(Array(SymbolInformation)).first
result.kind.is_a?(SymbolKind::Method).should be_true
end
end
end
end
12 changes: 12 additions & 0 deletions src/scry/context.cr
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ module Scry
end
end

private def dispatch_request(params : WorkspaceSymbolParams, msg)
case msg.method
when "workspace/symbol"
query = params.query
root_path = TextDocument.uri_to_filename(@workspace.root_uri)
workspace_symbol_processor = WorkspaceSymbolProcessor.new(msg.id, root_path, query)
response = workspace_symbol_processor.run
Log.logger.debug(response)
response
end
end

private def dispatch_request(params : CompletionItem, msg)
case msg.method
when "completionItem/resolve"
Expand Down
22 changes: 9 additions & 13 deletions src/scry/protocol/server_capabilities.cr
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,27 @@ module Scry
end
end

# Specify Sever capabilities supported,
# Currently Crystal supports:
# Specify supported sever capabilities:
#
# - Go to definition or Implementation (WIP)
# - Go to Implementation (WIP)
# - Diagnostics (WIP)
# - Formatting (WIP)
# - Symbols https://github.com/crystal-lang/crystal/blob/1f3e8b0e742b55c1feb5584dc932e87034365f48/samples/compiler/visitor_example.cr
# - Rename https://github.com/crystal-lang/crystal/blob/1f3e8b0e742b55c1feb5584dc932e87034365f48/samples/compiler/transformer_example.cr
# - Completion https://github.com/TechMagister/cracker
# - Hover
# - Code Actions
# - Signature Help
# - Range Formatting
# - Symbols (WIP)
# - Completion (WIP)
# - Hover (WIP)
# - Code Actions (WIP)
#
# Features not supported by Crystal yet:
#
# - CodeLens
# - Rename
# - Find References
#
# To enable more capabilities, See: https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md
struct ServerCapabilities
JSON.mapping(
textDocumentSync: TextDocumentSyncKind,
documentFormattingProvider: Bool,
definitionProvider: Bool,
documentSymbolProvider: Bool,
workspaceSymbolProvider: Bool,
completionProvider: CompletionOptions
)

Expand All @@ -53,6 +48,7 @@ module Scry
@documentFormattingProvider = true
@definitionProvider = true
@documentSymbolProvider = true
@workspaceSymbolProvider = true
@completionProvider = CompletionOptions.new
end
end
Expand Down
9 changes: 9 additions & 0 deletions src/scry/protocol/workspace_symbol_params.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "json"

module Scry
struct WorkspaceSymbolParams
JSON.mapping({
query: String,
}, true)
end
end
23 changes: 23 additions & 0 deletions src/scry/symbol.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "compiler/crystal/syntax"
require "./protocol/workspace_symbol_params"

module Scry
class SymbolVisitor < Crystal::Visitor
Expand Down Expand Up @@ -115,4 +116,26 @@ module Scry
ResponseMessage.new(@text_document.id, visitor.symbols)
end
end

class WorkspaceSymbolProcessor
def initialize(@msg_id : Int32 | String, @root_path : String, @query : String)
@all_files = Dir.glob("#{root_path}/**/*.cr")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why doesn't this use the workspace, and find files based on prelude and requires?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and find files based on prelude and requires?

What do you mean? 😅 This will get all "workspace" files to analyze all "workspace" symbols

Copy link
Contributor

@bew bew May 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh ok, so this symbol feature is just to search in your project (your own code), not everywhere (stdlib, other shards, etc) ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, just your code 😅

Although this should work on shards as well since lib is analyzed too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok it's fine then. I think I'd even restrict to src, to not show implementation detail symbols from shards

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bew Well, I guess I like your idea of listing stdlib symbols, so, this feature will support listing symbols from:

  1. Your code
  2. Your shards
  3. Crystal stdlib

screenshot_20180505_181554

And works very fast!!! 🎉 and stdlib is memoized ✨

end

def run
symbols = [] of SymbolInformation
unless @query.empty?
@all_files.each do |file|
visitor = SymbolVisitor.new("file://#{file}")
parser = Crystal::Parser.new(File.read(file))
parser.filename = file
node = parser.parse
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if there is a syntax error in one of the file? I think the exception needs to be rescued and the file skipped.

Is it possible to send errors in addition to valid results? (say, here are the results for files X & Y, but there is an error with file Z)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I can handle the error, but the response should still be managed by diagnostic provider

I think I can return an empty array if syntax error is found

Copy link
Contributor

@bew bew May 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at https://microsoft.github.io/language-server-protocol/specification#workspace-symbols-request-leftwards_arrow_with_hook you should be able to return null and an error in case an exception happens during the workspace symbol request.

I think it could be possible to return a result & an error? but how the valid_result/error is handled would vastly depend on the language client implementation..

Copy link
Member Author

@faustinoaq faustinoaq May 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, Actually we're managing that via response_error.cr, although I think for better user experience we should ignore parsing errors here since the diagnostics already do this work.

I don't want to see a popup saying "LSP server has an error" every time I try to list symbols on files with syntax errors 😅

node.accept(visitor)
symbols.concat visitor.symbols.select(&.name.match(Regex.new(@query)))
end
end

ResponseMessage.new(@msg_id, symbols)
end
end
end