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

28 add basic autocompletion #51

Merged
merged 10 commits into from
Mar 7, 2018
Merged

Conversation

laginha87
Copy link
Contributor

@laginha87 laginha87 commented Mar 1, 2018

This PR improves scry's autocompletion by adding method completion for some cases.
The strategy applied was inspired by https://github.com/techMagister/cracker/ where:

  1. Methods from all the files required by the file that is being edited are preloaded in a db.
  2. When method completion is activated it tries to guess the type of the object the method it's being called on by using a bunch of regexes on the text from the beginning to the file until the point the completion is being called on.
  3. After identifying the type it queries the db for the methods for that type matching the text.

For now it only covers the following scenarios:

  1. Variable was assigned a boolean (a = true; a.m)
  2. Variable was assigned an int (a = 1; a.m)
  3. The var's type is declared in the method definition (ex: def(a: File); a.me)
  4. The object where its called is a class. (ex: File.exp)
  5. The method is defined as a property/getter/setter

Also it doesn't cover:

  1. Namespaces
  2. Method documentation
  3. Returns of method calls
  4. Toplevel methods
  5. Instance methods inside own class

@laginha87
Copy link
Contributor Author

Still want to had some more tests but wanted to have some input on the direction I took this.

@faustinoaq faustinoaq requested a review from a team March 1, 2018 23:05
@faustinoaq
Copy link
Member

Hi @laginha87 Thank you for this PR 👍

I'm trying to test this on VSCode but I got an error:

Received message which is neither a response nor a notification message:
{
    "jsonrpc": "2.0",
    "error": {
        "code": -32001,
        "message": "Missing hash key: \"/home/main/Projects/Console/program.cr\"",
        "data": [
            "Hash(String, Tuple(Scry::TextDocument, Scry::Completion::MethodDB))",
            "Hash(String, Tuple(Scry::TextDocument, Scry::Completion::MethodDB))",
            "Scry::Workspace#get_file<Scry::TextDocumentIdentifier>:Tuple(Scry::TextDocument, Scry::Completion::MethodDB)",
            "Scry::Context#dispatchRequest<Scry::TextDocumentPositionParams, Scry::RequestMessage>:(Scry::ResponseMessage | Nil)",
            "Scry::Context#dispatch<Scry::RequestMessage>:(Scry::Initialize | Scry::ResponseMessage | Nil)",
            "Scry::start:(IO+ | Int32 | Nil)",
            "__crystal_main",
            "_crystal_main<Int32, Pointer(Pointer(UInt8))>:Nil",
            "Crystal::main_user_code<Int32, Pointer(Pointer(UInt8))>:Nil",
            "Crystal::main<Int32, Pointer(Pointer(UInt8))>:Int32",
            "main",
            "__libc_start_main",
            "_start",
            "???"
        ]
    }

Here is my scry log file

@faustinoaq
Copy link
Member

Oh, I'm testing this 👇 code

def foo(bar : Int32)
  bar
end

foo(42)

loading...

@laginha87
Copy link
Contributor Author

laginha87 commented Mar 2, 2018

😢
@faustinoaq thanks for the feedback, I will take a look.
I'm still not familiar with somethings in vscode and LSP. In your example you don't have a workspace right? Cause there's a lot of code that runs when the workspace opens that preloads a bunch of stuff.

@faustinoaq
Copy link
Member

Hi @laginha87

In your example you don't have a workspace right?

No, I'm using a workspace in that example, look at titlebar, the workspace name is Console

@laginha87 I tried sublime as well, and same error happens, maybe we're missing some json field? 😅

screenshot_20180302_161605

Scry logs on Sublime Text 3 LSP

@laginha87
Copy link
Contributor Author

@faustinoaq I was preloading every crystal file under src/ your example doesn't have that changed it to preloading every cr file under the workspace root.

@faustinoaq
Copy link
Member

faustinoaq commented Mar 3, 2018

Hi @laginha87, Thank you for yout last commit! Scry server is running now 🎉

althought completion doesn't work yet on vscode nor sublime

I tried with/without a workspace and I got this:

Variable was assigned a boolean (a = true; a.m)

a = true; a.to_s
D, [2018-03-03 11:01:48 -05:00 #9095] DEBUG -- : Content-Length: 171
D, [2018-03-03 11:01:48 -05:00 #9095] DEBUG -- :
D, [2018-03-03 11:01:48 -05:00 #9095] DEBUG -- : {"jsonrpc":"2.0","id":93,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///home/main/Projects/Foo/foo.cr"},"position":{"line":2,"character":12}}}
D, [2018-03-03 11:01:48 -05:00 #9095] DEBUG -- : textDocument/completion
D, [2018-03-03 11:01:48 -05:00 #9095] DEBUG -- : Couldn't find type Bool
D, [2018-03-03 11:01:48 -05:00 #9095] DEBUG -- : Scry::ResponseMessage(@jsonrpc="2.0", @id=93, @result=[], @error=nil)
D, [2018-03-03 11:01:48 -05:00 #9095] DEBUG -- : Content SEND: Content-Length: 37

{"jsonrpc":"2.0","id":93,"result":[]}

Variable was assigned an int (a = 1; a.m)

b = 1; b.to_s
D, [2018-03-03 11:04:40 -05:00 #9095] DEBUG -- : Content-Length: 172
D, [2018-03-03 11:04:40 -05:00 #9095] DEBUG -- :
D, [2018-03-03 11:04:40 -05:00 #9095] DEBUG -- : {"jsonrpc":"2.0","id":115,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///home/main/Projects/Foo/foo.cr"},"position":{"line":18,"character":9}}}
D, [2018-03-03 11:04:40 -05:00 #9095] DEBUG -- : textDocument/completion
D, [2018-03-03 11:04:40 -05:00 #9095] DEBUG -- : Couldn't find type Int32
D, [2018-03-03 11:04:40 -05:00 #9095] DEBUG -- : Scry::ResponseMessage(@jsonrpc="2.0", @id=115, @result=[], @error=nil)
D, [2018-03-03 11:04:40 -05:00 #9095] DEBUG -- : Content SEND: Content-Length: 38

{"jsonrpc":"2.0","id":115,"result":[]}

The var's type is declared in the method definition (ex: def(a: File); a.me)

def foo(c : File)
  c
end

foo(File.open("./foo.cr"))
D, [2018-03-03 11:11:05 -05:00 #9095] DEBUG -- : Content-Length: 171
D, [2018-03-03 11:11:05 -05:00 #9095] DEBUG -- : 
D, [2018-03-03 11:11:05 -05:00 #9095] DEBUG -- : {"jsonrpc":"2.0","id":184,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///home/main/Projects/Foo/foo.cr"},"position":{"line":1,"character":4}}}
D, [2018-03-03 11:11:05 -05:00 #9095] DEBUG -- : textDocument/completion
D, [2018-03-03 11:11:05 -05:00 #9095] DEBUG -- : Couldn't find type File
D, [2018-03-03 11:11:05 -05:00 #9095] DEBUG -- : Scry::ResponseMessage(@jsonrpc="2.0", @id=184, @result=[], @error=nil)
D, [2018-03-03 11:11:05 -05:00 #9095] DEBUG -- : Content SEND: Content-Length: 38

{"jsonrpc":"2.0","id":184,"result":[]}

The object where its called is a class. (ex: File.exp)

File.exists?("./foo.cr")
D, [2018-03-03 11:11:47 -05:00 #9095] DEBUG -- : Content-Length: 172
D, [2018-03-03 11:11:47 -05:00 #9095] DEBUG -- : 
D, [2018-03-03 11:11:47 -05:00 #9095] DEBUG -- : {"jsonrpc":"2.0","id":192,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///home/main/Projects/Foo/foo.cr"},"position":{"line":59,"character":6}}}
D, [2018-03-03 11:11:47 -05:00 #9095] DEBUG -- : textDocument/completion
D, [2018-03-03 11:11:47 -05:00 #9095] DEBUG -- : Couldn't find type File.class
D, [2018-03-03 11:11:47 -05:00 #9095] DEBUG -- : Scry::ResponseMessage(@jsonrpc="2.0", @id=192, @result=[], @error=nil)
D, [2018-03-03 11:11:47 -05:00 #9095] DEBUG -- : Content SEND: Content-Length: 38

{"jsonrpc":"2.0","id":192,"result":[]}

The method is defined as a property/getter/setter

class Foo
  property bar : Int32?
end

baz = Foo.new

baz.bar
D, [2018-03-03 11:13:50 -05:00 #9095] DEBUG -- : Content-Length: 172
D, [2018-03-03 11:13:50 -05:00 #9095] DEBUG -- : 
D, [2018-03-03 11:13:50 -05:00 #9095] DEBUG -- : {"jsonrpc":"2.0","id":215,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///home/main/Projects/Foo/foo.cr"},"position":{"line":81,"character":6}}}
D, [2018-03-03 11:13:50 -05:00 #9095] DEBUG -- : textDocument/completion
D, [2018-03-03 11:13:50 -05:00 #9095] DEBUG -- : Couldn't find type Foo
D, [2018-03-03 11:13:50 -05:00 #9095] DEBUG -- : Scry::ResponseMessage(@jsonrpc="2.0", @id=215, @result=[], @error=nil)
D, [2018-03-03 11:13:50 -05:00 #9095] DEBUG -- : Content SEND: Content-Length: 38

{"jsonrpc":"2.0","id":215,"result":[]}

Full scry logs here


BTW, I see you're using four spaces to indent crystal code

Use two spaces to indent code inside namespaces, methods, blocks or other nested contexts

Crystal code guidelines says 2 spaces 😅

Try using crystal tool format in your code 👍

@faustinoaq
Copy link
Member

I tried to setup CRYSTAL_PATH as a global env using /etc/profile but still no completion and same debug output.

BTW, require completion works like a charm ❤️

@laginha87
Copy link
Contributor Author

@faustinoaq Thanks for trying this out.
I'm really sorry it didn't work, don't want to waste your time.
I tried it locally using a single file and it works for me.
I added some logging to the class that preloads the completion data.
Would you mind trying it out and sending me the logs to figure out what's going on?

@laginha87
Copy link
Contributor Author

About the CRYSTAL_PATH it should be set up automatically by scry.

@faustinoaq
Copy link
Member

I tried it locally using a single file and it works for me.

@laginha87 Thank you for all your effort! 👍

I tried creating a new crystal project using crystal init app and looks like completion is working now 🎉

Seems that src folder did the trick 😎

screenshot_20180303_154315

screenshot_20180303_155418

screenshot_20180303_155606

Would you mind trying it out and sending me the logs to figure out what's going on?

Here is my latest debug logs (testing this completion feature)

I approve this PR 😄 , although I think more people should try it.

BTW, Scry completion is not working on sublime, I don't know why, sublime has other issues with scry though

Maybe someone can try Atom (@keplersj) or NVIM (@bew) 👀 😅

@faustinoaq
Copy link
Member

This is awesome! ✨

screenshot_20180303_163138

screenshot_20180303_163212

Very useful to get some module or class method in standard library 💯

Copy link
Member

@faustinoaq faustinoaq left a comment

Choose a reason for hiding this comment

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

Very nice PR, Just waiting for more reviews 👍

@faustinoaq
Copy link
Member

faustinoaq commented Mar 5, 2018

BTW, Scry completion is not working on sublime, I don't know why, sublime has other issues with scry though

I did it! Scry completion is working in sublime as well! 🎉 ✨

screenshot_20180305_085828
screenshot_20180305_085913
screenshot_20180305_085954
screenshot_20180305_090733
screenshot_20180305_090943
screenshot_20180305_091216

I had to tweak LSP sublime client with some config copied from scry logs:

// Settings in here override those in "LSP/LSP.sublime-settings",

{
  "clients":
  {
    // Each new client must have the following structure.
    "crystal":
    {
      
  
      // The command line required to run the server.
      "command": ["/home/main/Projects/scry/scry"],
  
      // Use: "Show Scope Name" from Sublime's Developer menu
      "scopes": ["source.crystal", "source.cr"],
  
      // Run: view.settings().get("syntax") in console
      "syntaxes": ["Packages/Crystal/Crystal.tmLanguage"],
  
      // See: https://github.com/Microsoft/language-server-protocol/issues/213
      "languageId": "crystal",

      "only_show_lsp_completions": true,

      "log_payloads": true,

      "show_diagnostics_phantoms": true,
      
  
      // Sent to server once using workspace/didConfigurationChange notification
      "settings": {
        "problems":"syntax",
        "implementations":false,
        "hover":false,
        "completion":false,
        "mainFile":"",
        "maxNumberOfProblems":100,
        "processesLimit":3,
        "compiler":"crystal",
        "server":"/home/main/Projects/scry/scry",
        "logLevel":"debug",
        "bashOnWindows":false
       },
  
      // Sent once to server in initialize request
      "initializationOptions": {
        "capabilities": {
          "workspace": {
            "applyEdit": true
          },
          "textDocument": {
            "synchronization": {
              "didSave": true
            },
            "completion": {
              "completionItem": {
                "snippetSupport": true
              }
            }
          }
        }
      }
    }
  }
}

@faustinoaq
Copy link
Member

@keplersj @bmulvihill Can you review this?, I would like to get it merged 😅

@bmulvihill
Copy link
Contributor

bmulvihill commented Mar 6, 2018

@faustinoaq @laginha87 This is a big PR. One thing sticks out to me, a lot of the code here is building up a dependency graph

Is that necessary? Are we able to just use some sort of main file or root path for the project? From there we can get all the .cr files and parse them.

It seems like maybe that is a simpler approach. Although I don't have all the context regarding this PR, and maybe that won't work for this.

@faustinoaq
Copy link
Member

Hi @bmulvihill 😄

I think @laginha87 is trying to implement auto completion based on cracker by @TechMagister

Cracker is using a similar mechanism, I don't know if is the best way, though

@bmulvihill
Copy link
Contributor

@faustinoaq I took a look at cracker, I didn't notice any use of dependency graphs, unless I missed something.

@laginha87
Copy link
Contributor Author

laginha87 commented Mar 6, 2018

@bmulvihill you're right the dependancy graph stuff isn't in cracker. In cracker they only define a preload method and your integration with the editor handles what to preload.

I added the dependancy graph cause I didn't like that the autocomplete suggestions could be inaccurate by providing completion hints for files that are not required by the file you're working on.

I understand that this would be picked up by the compiler but since you can reopen classes it can lead to some strange behaviour the compiler just tells the method doesn't exist on the class you know you're requiring correctly.

I was also worried with the memory footprint specially if you're in a project with a lot of libraries.

The dependancy graph might have been a premature optimization and it can be handled later.

If we just preload the files from the main root file we lose completion for files with no workspace and for any helper methods defined in the specs.

@laginha87
Copy link
Contributor Author

@bmulvihill I can try it out without the graph to shorten the PR and it can be added later if it seems like a reasonable approach.

I have done a lot more work around completion in scry so I split the work in smaller chunks so they'd be easier to review.

@bmulvihill
Copy link
Contributor

bmulvihill commented Mar 6, 2018

@laginha87 Thank you for the explanation, like I said I definitely don't have all the context, I am just curious about the approach.

Would some of the problems mentioned be solved by always updating the method DB every time we update a file? What I mean is we create the method DB when opening the project based on some root path/main file and then make updates to it as we update files via the DidChangeTextDocument notification. Or am I misunderstanding the issue?

I was thinking whatever approach is used here eventually could also be used to solve Workspace symbols as well.

Copy link
Contributor

@keplersj keplersj left a comment

Choose a reason for hiding this comment

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

Looks okay to me. Looks like a good implementation. And of course once it's out there we can dogfood it and having an implementation like this starts making life easier

@laginha87
Copy link
Contributor Author

@bmulvihill those are all valid points.
I think if we preload everything the method db should be a more complex structure because there will be a lot more stuff to search through, also updating the method db on a file change might involve some complex logic as well.
Those can all be managed anyway, but the major issue I have with an overall methodDB is because of scenarios like:
if you have this 2 files.

# a/a.cr

class A
    def a
    end
end
# b/a.cr

class A
    def b
    end
end

and you're editing this:

require "a/a.cr"

foo = A.new
foo.

The completion will suggest both a and b which is incorrect as it should only suggest the a method.

And also thought this approach would be better to later store all this info in the filesystems for a faster boot up.

Right now the solution isn't very dependant on the graph and I can easily remove it and we can add it later if there's an actual need for it.

@bmulvihill
Copy link
Contributor

@laginha87 Ok cool, thanks again for explaining. I think it is good the way it is 👍

@faustinoaq
Copy link
Member

Seems this PR is enough approved, I'll merge it

Please if you found some problem trying these new features open a new issue or a PR fixing it.

Thank you so much for these contributions 😄 👏 ✨

@faustinoaq faustinoaq merged commit c48f421 into master Mar 7, 2018
@faustinoaq faustinoaq deleted the 28-add-basic-autocompletion branch March 7, 2018 00:41
@faustinoaq
Copy link
Member

I had to tweak LSP sublime client with some config copied from scry logs:

Looks like those settings aren't needed at all

screenshot_20180307_105006

😅

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants