Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Account for server request ID strings #906

Merged
merged 2 commits into from
Mar 12, 2020
Merged

Account for server request ID strings #906

merged 2 commits into from
Mar 12, 2020

Conversation

rwols
Copy link
Member

@rwols rwols commented Mar 11, 2020

Request IDs can be strings or integers.
We, the text editor, always use integers for request IDs.
But some servers may send out e.g. GUIDs as request IDs.
I decided to type-erase the request ID in various places,
because all we're doing in most functions is passing through
the received request ID from the server all the way to our
Response object, finally serializing it back to JSON again.

close #905

@jwortmann, mind giving this branch a try?

Request IDs can be strings or integers.
We, the text editor, always use integers for request IDs.
But some servers may send out e.g. GUIDs as request IDs.
I decided to type-erase the request ID in various places,
because all we're doing in most functions is passing through
the received request ID from the server all the way to our
Response object, finally serializing it back to JSON again.
@jwortmann
Copy link
Member

jwortmann commented Mar 11, 2020

Thanks, I tried this branch and now it seems like LSP responds with the correct request ID. However, the server immediately crashes after that. Ironically it looks like the server itself cannot handle incoming string IDs as well. I don't see a related issue for that on the server's repo though.

Here is what I get in the output panel (the server is implemented in Julia):

:: --> julials initialize(1): {'rootUri': 'file:///D:/data/code/Julia/test', 'rootPath': 'D:\\data\\code\\Julia\\test', 'workspaceFolders': [{'uri': 'file:///D:/data/code/Julia/test', 'name': 'test'}], 'processId': 9308, 'capabilities': {'textDocument': {'references': {}, 'colorProvider': {}, 'documentHighlight': {}, 'rangeFormatting': {}, 'completion': {'completionItemKind': {'valueSet': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]}, 'completionItem': {'snippetSupport': True}}, 'documentSymbol': {'symbolKind': {'valueSet': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]}}, 'signatureHelp': {'signatureInformation': {'documentationFormat': ['markdown', 'plaintext'], 'parameterInformation': {'labelOffsetSupport': True}}}, 'typeDefinition': {'linkSupport': True}, 'codeAction': {'codeActionLiteralSupport': {'codeActionKind': {'valueSet': []}}}, 'declaration': {'linkSupport': True}, 'formatting': {}, 'publishDiagnostics': {'relatedInformation': True}, 'hover': {'contentFormat': ['markdown', 'plaintext']}, 'definition': {'linkSupport': True}, 'implementation': {'linkSupport': True}, 'rename': {}, 'synchronization': {'willSaveWaitUntil': True, 'didSave': True, 'willSave': True}}, 'workspace': {'workspaceFolders': True, 'executeCommand': {}, 'applyEdit': True, 'didChangeConfiguration': {}, 'symbol': {'symbolKind': {'valueSet': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]}}, 'configuration': True}}}
julials: [ Info: Received new data from Julia Symbol Server.
:: <-- julials 1: {'capabilities': {'documentOnTypeFormattingProvider': None, 'experimental': None, 'codeLensProvider': None, 'declarationProvider': False, 'completionProvider': {'resolveProvider': False, 'triggerCharacters': ['.']}, 'textDocumentSync': {'change': 2, 'willSaveWaitUntil': False, 'save': {'includeText': True}, 'openClose': True, 'willSave': False}, 'foldingRangeProvider': False, 'typeDefinitionProvider': False, 'implementationProvider': False, 'codeActionProvider': True, 'definitionProvider': True, 'workspace': {'workspaceFolders': {'changeNotifications': True, 'supported': True}}, 'colorProvider': False, 'documentLinkProvider': None, 'documentRangeFormattingProvider': False, 'renameProvider': True, 'documentHighlightProvider': False, 'hoverProvider': True, 'documentFormattingProvider': True, 'executeCommandProvider': {'commands': ['ExplicitPackageVarImport', 'ExpandFunction', 'AddDefaultConstructor', 'ReexportModule']}, 'referencesProvider': True, 'documentSymbolProvider': True, 'signatureHelpProvider': {'triggerCharacters': ['(', ',']}, 'workspaceSymbolProvider': True}}
:: --> julials initialized: {}
:: --> julials textDocument/didOpen
:: <-- julials textDocument/publishDiagnostics: {'uri': 'file:///D%3A/data/code/Julia/test/test.jl', 'version': 0, 'diagnostics': []}
:: <-- julials workspace/configuration(ece400e2-17b6-4f00-b105-42c03b41de25): {'items': [{'scopeUri': None, 'section': 'julia.format.indent'}, {'scopeUri': None, 'section': 'julia.format.indents'}, {'scopeUri': None, 'section': 'julia.format.ops'}, {'scopeUri': None, 'section': 'julia.format.tuples'}, {'scopeUri': None, 'section': 'julia.format.curly'}, {'scopeUri': None, 'section': 'julia.format.calls'}, {'scopeUri': None, 'section': 'julia.format.iterOps'}, {'scopeUri': None, 'section': 'julia.format.comments'}, {'scopeUri': None, 'section': 'julia.format.docs'}, {'scopeUri': None, 'section': 'julia.format.lineends'}, {'scopeUri': None, 'section': 'julia.format.kw'}, {'scopeUri': None, 'section': 'julia.lint.run'}, {'scopeUri': None, 'section': 'julia.lint.call'}, {'scopeUri': None, 'section': 'julia.lint.iter'}, {'scopeUri': None, 'section': 'julia.lint.nothingcomp'}, {'scopeUri': None, 'section': 'julia.lint.constif'}, {'scopeUri': None, 'section': 'julia.lint.lazy'}, {'scopeUri': None, 'section': 'julia.lint.datadecl'}, {'scopeUri': None, 'section': 'julia.lint.typeparam'}, {'scopeUri': None, 'section': 'julia.lint.modname'}, {'scopeUri': None, 'section': 'julia.lint.pirates'}]}
:: --> julials ece400e2-17b6-4f00-b105-42c03b41de25: [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]
julials: ERROR: MethodError: Cannot `convert` an object of type Dict{String,Any} to an object of type Int64
julials: Closest candidates are:
julials: convert(::Type{T}, !Matched::T) where T<:Number at number.jl:6
julials: convert(::Type{T}, !Matched::Number) where T<:Number at number.jl:7
julials: convert(::Type{T}, !Matched::Ptr) where T<:Integer at pointer.jl:23
julials: ...
julials: Stacktrace:
julials: [1] DocumentFormat.FormatOptions(::Dict{String,Any}, ::Dict{String,Any}, ::Dict{String,Any}, ::Dict{String,Any}, ::Dict{String,Any}, ::Dict{String,Any}, ::Dict{String,Any}, ::Dict{String,Any}, ::Dict{String,Any}, ::Dict{String,Any}, ::Dict{String,Any}) at C:\Users\Janos\.julia\packages\DocumentFormat\Geymf\src\DocumentFormat.jl:9
julials: [2] request_julia_config(::LanguageServerInstance) at C:\Users\Janos\.julia\packages\LanguageServer\wMPrf\src\requests\workspace.jl:77
julials: [3] process(::LanguageServer.JSONRPC.Request{Val{:initialized},Dict{String,Any}}, ::LanguageServerInstance) at C:\Users\Janos\.julia\packages\LanguageServer\wMPrf\src\requests\init.jl:132
julials: [4] run(::LanguageServerInstance) at C:\Users\Janos\.julia\packages\LanguageServer\wMPrf\src\languageserverinstance.jl:217
julials: [5] top-level scope at none:1

I'm not that familiar with the protocol specification, do you believe this line is a valid response from LSP?

:: --> julials ece400e2-17b6-4f00-b105-42c03b41de25: [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]

@jwortmann
Copy link
Member

From a look into the language server's code I found that it expects an array of one integer as first value and then 20 boolean values for that workspace/configuration request. Unfortunately the server's documentation is very sparse and doesn't include any infos about that. However, it will use fallback values if the array's entries are null values. It seems like LSP responds with an array, which contains repeatedly the whole "settings" object from the LSP.sublime-settings for the server. Is that intentional? Since I had no "settings" entry in my configuration, the respond consisted of empty dicts, which causes the error. When I set "settings": null in my server configuration, LSP sends

--> julials d52c1f5e-2d67-4ee3-8b3a-50bfe23ab977: [None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None]

and the server does not crash anymore. But right the next log entry then is

:: unhandled julials client/registerCapability(66c04853-ac31-4b6f-9c9a-d5fe653f30e9): {'registrations': [{'id': '28c6550c-bd7b-11e7-abc4-cec278b6b50a', 'method': 'workspace/didChangeWorkspaceFolders'}]}

and the server is still unresponsive after that.

I think the workspace/configuration implementation in LSP should not respond with {}, as the specifications says that it should return null if it can't provide a configuration setting.
Could I perhaps add the workspace support to the "disabled_capabilities" somehow?

Maybe related: #699

@rchl
Copy link
Member

rchl commented Mar 12, 2020

I think the code here:

items.append(self.config.settings) # ???

should probably be:

items.append(self.config.settings or None)

@rwols
Copy link
Member Author

rwols commented Mar 12, 2020

You guys are correct, we should set None instead of {}. Quoting from the spec:

If the client can’t provide a configuration setting for a given scope then null need to be present in the returned array.

But I consider this a separate issue, let's make another pull request for that.

@jwortmann
Copy link
Member

jwortmann commented Mar 12, 2020

I don't think that items.append(self.config.settings or None) is enough, because it would still wrongly paste the whole "settings" object repeatedly into each array entry. We need to parse the "settings" only for the requested keys instead, and if not present, use None as the corresponding value.

Edit: this works for me:

if requested_item['section'] in self.config.settings:
    items.append(self.config.settings[requested_item['section']])
else:
    items.append(None)

Should I make a pull request for it?

After that, I still get an unhandled client/registerCapability though, but I guess this is yet another issue.


This PR here looks good from my side and is working for me so far.

I just have some suggestions for the ServerLog.sublime-syntax. As GitHub allows me only to comment on the changed lines, I paste the whole file here for simplicity:

%YAML 1.2
---
# [Subl]: https://www.sublimetext.com/docs/3/syntax.html
# [LSP]: https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md
hidden: true
scope: output.lsp.log

variables:
  method: '[[:alpha:]][[:alnum:]/]*'
  servername: '[[:alnum:]_-]+'
  uuid: '\h{8}\-\h{4}\-\h{4}\-\h{4}\-\h{12}'

contexts:
  main:
    - match: ^({{servername}})(:)
      captures:
        1: variable.function.lsp
        2: punctuation.separator.lsp
      push:
        - meta_scope: meta.block.lsp
        - match: $
          pop: true
    - match: '^::'
      scope: punctuation.accessor.lsp
      push:
      - - meta_scope: meta.group.lsp
        - match: $
          pop: true
        # responses
      - - match: '{{uuid}}'
          scope: constant.numeric.uuid.lsp
          set:
            - match: ':'
              scope: punctuation.separator.lsp
              set: maybe-payload
            - match: ''
              pop: true
        - match: \d+
          scope: constant.numeric.integer.decimal.lsp
          set:
            - match: ':'
              scope: punctuation.separator.lsp
              set: maybe-payload
            - match: ''
              pop: true
        # notifications or requests
        - match: (?=\w)
          set:
            # requests
          - - match: \(
              scope: punctuation.section.parens.begin.lsp
              set:
                - match: \)
                  scope: punctuation.section.parens.end.lsp
                  set:
                    - match: ':'
                      scope: punctuation.separator.lsp
                      set: maybe-payload
                    - match: ''
                      pop: true
                - match: '{{uuid}}'
                  scope: constant.numeric.uuid.lsp
                - match: \d+
                  scope: constant.numeric.integer.decimal.lsp
            # notifications
            - match: ':'
              scope: punctuation.separator.lsp
              set: maybe-payload
            - match: ''
              pop: true
          - - match: '{{method}}'
              scope: keyword.control.lsp
              pop: true
        # language server name
      - - match: \S+
          scope: variable.function.lsp
          pop: true
        # arrows
      - - match: -->
          scope: storage.modifier.lsp
          pop: true
        - match: <--
          scope: storage.modifier.lsp
          pop: true
        - match: ==>
          scope: storage.modifier.lsp
          pop: true
        - match: unhandled
          scope: invalid.deprecated.lsp
          pop: true

  maybe-payload:
    - match: \s*(?=\S)
      set:
        - match: $
          pop: true
        - include: scope:source.python#constants  # e.g. shutdown request
        - include: scope:source.python#lists
        - include: scope:source.python#dictionaries-and-sets
    - match: ''
      pop: true
...

I needed to disambiguate between responses and notifications, because
request IDs can be any string! Solved this by using separate arrow
symbols for notifications/responses/requests. This way, we can
disambiguate based on the arrow symbol.
@rwols
Copy link
Member Author

rwols commented Mar 12, 2020

Thanks for the heads up about the syntax highlighting. I decided to disambiguate based on the arrow symbol. It generally looks like this now

Screenshot from 2020-03-12 23-42-49

@rwols rwols merged commit cee7ff9 into master Mar 12, 2020
@rwols rwols deleted the request-id-strings branch March 12, 2020 22:49
@rwols
Copy link
Member Author

rwols commented Mar 12, 2020

I don't think that items.append(self.config.settings or None) is enough, because it would still wrongly paste the whole "settings" object repeatedly into each array entry. We need to parse the "settings" only for the requested keys instead, and if not present, use None as the corresponding value.

Should I make a pull request for it?

Go for it! :)

After that, I still get an unhandled client/registerCapability though, but I guess this is yet another issue.

We should ideally respond with a MethodNotFound error code, but I don't know how various servers in the wild would react to that response.

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

Successfully merging this pull request may close these issues.

Error while handling server requests with non-integer request id
3 participants