eclipse.jdt.ls icon indicating copy to clipboard operation
eclipse.jdt.ls copied to clipboard

No delegateCommandHandler for java.apply.workspaceEdit

Open idanarye opened this issue 8 years ago • 20 comments

I'm using jdt.ls via LanguageClient-neovim, and I'm trying to use code actions. This is my file:

public class App {
    public static void main(String[] args) {
        JFrame frame = new JFrame();
    }
}

JFrame is linted because I did not import it, and the LS suggests some code actions(this is from the log of the client - I just formatted the JSON lines to make them readable):

17:06:19 INFO    [MainThread] Begin textDocument/codeAction
17:06:19 DEBUG   [MainThread] =>
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "textDocument/codeAction",
  "params": {
    "textDocument": {
      "uri": "file:///files/tests/java/45/src/main/java/App.java"
    },
    "range": {
      "start": {
        "line": 2,
        "character": 8
      },
      "end": {
        "line": 2,
        "character": 14
      }
    },
    "context": {
      "diagnostics": [
        {
          "range": {
            "start": {
              "line": 2,
              "character": 8
            },
            "end": {
              "line": 2,
              "character": 14
            }
          },
          "severity": 1,
          "code": "16777218",
          "source": "Java",
          "message": "JFrame cannot be resolved to a type"
        }
      ]
    }
  }
}
17:06:19 DEBUG   [RPC-java  ] <= {"jsonrpc":"2.0","method":"window/logMessage","params":{"type":3,"message":"Oct 3, 2017 5:05:52 PM \u003e\u003e document/codeAction"}}
17:06:19 DEBUG   [RPC-java  ] <=
{
  "jsonrpc": "2.0",
  "id": "1",
  "result": [
    {
      "title": "Change to 'JobName' (javax.print.attribute.standard)",
      "command": "java.apply.workspaceEdit",
      "arguments": [
        {
          "changes": {
            "file:///files/tests/java/45/src/main/java/App.java": [
              {
                "range": {
                  "start": {
                    "line": 0,
                    "character": 0
                  },
                  "end": {
                    "line": 0,
                    "character": 0
                  }
                },
                "newText": "import javax.print.attribute.standard.JobName;"
              },
              {
                "range": {
                  "start": {
                    "line": 0,
                    "character": 0
                  },
                  "end": {
                    "line": 0,
                    "character": 0
                  }
                },
                "newText": "\n\n"
              },
              {
                "range": {
                  "start": {
                    "line": 2,
                    "character": 8
                  },
                  "end": {
                    "line": 2,
                    "character": 8
                  }
                },
                "newText": "JobName"
              },
              {
                "range": {
                  "start": {
                    "line": 2,
                    "character": 8
                  },
                  "end": {
                    "line": 2,
                    "character": 14
                  }
                },
                "newText": ""
              }
            ]
          }
        }
      ]
    },
    {
      "title": "Import 'JFrame' (javax.swing)",
      "command": "java.apply.workspaceEdit",
      "arguments": [
        {
          "changes": {
            "file:///files/tests/java/45/src/main/java/App.java": [
              {
                "range": {
                  "start": {
                    "line": 0,
                    "character": 0
                  },
                  "end": {
                    "line": 0,
                    "character": 0
                  }
                },
                "newText": "import javax.swing.JFrame;"
              },
              {
                "range": {
                  "start": {
                    "line": 0,
                    "character": 0
                  },
                  "end": {
                    "line": 0,
                    "character": 0
                  }
                },
                "newText": "\n\n"
              }
            ]
          }
        }
      ]
    },
    {
      "title": "Add type parameter 'JFrame' to 'main(String[])'",
      "command": "java.apply.workspaceEdit",
      "arguments": [
        {
          "changes": {
            "file:///files/tests/java/45/src/main/java/App.java": [
              {
                "range": {
                  "start": {
                    "line": 1,
                    "character": 18
                  },
                  "end": {
                    "line": 1,
                    "character": 18
                  }
                },
                "newText": "<"
              },
              {
                "range": {
                  "start": {
                    "line": 1,
                    "character": 18
                  },
                  "end": {
                    "line": 1,
                    "character": 18
                  }
                },
                "newText": "JFrame"
              },
              {
                "range": {
                  "start": {
                    "line": 1,
                    "character": 18
                  },
                  "end": {
                    "line": 1,
                    "character": 18
                  }
                },
                "newText": "> "
              }
            ]
          }
        }
      ]
    }
  ]
}
17:06:19 DEBUG   [MainThread] state.codeActionCommands: [] =>
[
  {
    "title": "Change to 'JobName' (javax.print.attribute.standard)",
    "command": "java.apply.workspaceEdit",
    "arguments": [
      {
        "changes": {
          "file:///files/tests/java/45/src/main/java/App.java": [
            {
              "range": {
                "start": {
                  "line": 0,
                  "character": 0
                },
                "end": {
                  "line": 0,
                  "character": 0
                }
              },
              "newText": "import javax.print.attribute.standard.JobName;"
            },
            {
              "range": {
                "start": {
                  "line": 0,
                  "character": 0
                },
                "end": {
                  "line": 0,
                  "character": 0
                }
              },
              "newText": "\n\n"
            },
            {
              "range": {
                "start": {
                  "line": 2,
                  "character": 8
                },
                "end": {
                  "line": 2,
                  "character": 8
                }
              },
              "newText": "JobName"
            },
            {
              "range": {
                "start": {
                  "line": 2,
                  "character": 8
                },
                "end": {
                  "line": 2,
                  "character": 14
                }
              },
              "newText": ""
            }
          ]
        }
      }
    ]
  },
  {
    "title": "Import 'JFrame' (javax.swing)",
    "command": "java.apply.workspaceEdit",
    "arguments": [
      {
        "changes": {
          "file:///files/tests/java/45/src/main/java/App.java": [
            {
              "range": {
                "start": {
                  "line": 0,
                  "character": 0
                },
                "end": {
                  "line": 0,
                  "character": 0
                }
              },
              "newText": "import javax.swing.JFrame;"
            },
            {
              "range": {
                "start": {
                  "line": 0,
                  "character": 0
                },
                "end": {
                  "line": 0,
                  "character": 0
                }
              },
              "newText": "\n\n"
            }
          ]
        }
      }
    ]
  },
  {
    "title": "Add type parameter 'JFrame' to 'main(String[])'",
    "command": "java.apply.workspaceEdit",
    "arguments": [
      {
        "changes": {
          "file:///files/tests/java/45/src/main/java/App.java": [
            {
              "range": {
                "start": {
                  "line": 1,
                  "character": 18
                },
                "end": {
                  "line": 1,
                  "character": 18
                }
              },
              "newText": "<"
            },
            {
              "range": {
                "start": {
                  "line": 1,
                  "character": 18
                },
                "end": {
                  "line": 1,
                  "character": 18
                }
              },
              "newText": "JFrame"
            },
            {
              "range": {
                "start": {
                  "line": 1,
                  "character": 18
                },
                "end": {
                  "line": 1,
                  "character": 18
                }
              },
              "newText": "> "
            }
          ]
        }
      }
    ]
  }
]
17:06:19 INFO    [MainThread] End textDocument/codeAction

So I pick Import 'JFrame' (javax.swing):

17:06:44 INFO    [MainThread] Begin workspace/executeCommand
17:06:44 DEBUG   [MainThread] =>
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "workspace/executeCommand",
  "params": {
    "command": "java.apply.workspaceEdit",
    "arguments": [
      {
        "changes": {
          "file:///files/tests/java/45/src/main/java/App.java": [
            {
              "range": {
                "start": {
                  "line": 0,
                  "character": 0
                },
                "end": {
                  "line": 0,
                  "character": 0
                }
              },
              "newText": "import javax.print.attribute.standard.JobName;"
            },
            {
              "range": {
                "start": {
                  "line": 0,
                  "character": 0
                },
                "end": {
                  "line": 0,
                  "character": 0
                }
              },
              "newText": "\n\n"
            },
            {
              "range": {
                "start": {
                  "line": 2,
                  "character": 8
                },
                "end": {
                  "line": 2,
                  "character": 8
                }
              },
              "newText": "JobName"
            },
            {
              "range": {
                "start": {
                  "line": 2,
                  "character": 8
                },
                "end": {
                  "line": 2,
                  "character": 14
                }
              },
              "newText": ""
            }
          ]
        }
      }
    ]
  }
}
17:06:44 DEBUG   [RPC-java  ] <= {"jsonrpc":"2.0","method":"window/logMessage","params":{"type":3,"message":"Oct 3, 2017 5:05:52 PM \u003e\u003e workspace/executeCommand"}}
17:06:44 DEBUG   [RPC-java  ] <= {"jsonrpc":"2.0","id":"2","error":{"code":-32601,"message":"No delegateCommandHandler for java.apply.workspaceEdit"}}
17:06:44 ERROR   [MainThread] {"jsonrpc": "2.0", "id": "2", "error": {"code": -32601, "message": "No delegateCommandHandler for java.apply.workspaceEdit"}}
17:06:44 INFO    [MainThread] End workspace/executeCommand

And get the error message - No delegateCommandHandler for java.apply.workspaceEdit

The LSP specs does not seem to refer to delegateCommandHandlers, and from #346 it looks like it's jdt.ls' extension - but I don't see any instructions on how to use it.

So... what do I need to do to add support for it in a client?

idanarye avatar Oct 03 '17 17:10 idanarye

Yeah documenting jdt.ls specific commands is in our backlog (#314), sorry about that.

java.apply.workspaceEdit is not related to delegateCommandHandlers actually (those are server side extensions). It's a command that needs to be implemented by clients. See how it's done in vscode-java: https://github.com/redhat-developer/vscode-java/blob/9b0f0aca80cbefabad4c034fb5dd365d029f6170/src/extension.ts#L155-L160

Basically, the server sends workspace edits to the client, and the client is responsible for applying them.

fbricon avatar Oct 03 '17 17:10 fbricon

Isn't that how code actions work? Why do you need a special command for that?

idanarye avatar Oct 03 '17 18:10 idanarye

I think the issue is that protocol doesn't specify clearly how client is going to consume a Command(https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#command) after user select one, which could have at least two cases here,

  1. client sends the selected Command to server, server responds back with workspaceEdit, then client applies those edits
  2. client itself figures out how to apply edits based on selected Command

sourcegraph/javascript-typescript-langserver chosen option 1 and LanguageClient-neovim followed the same pattern. And it looks here eclipse.jdt did option 2.

autozimu avatar Nov 04 '17 21:11 autozimu

So much for having a unified protocol that solves the matrix problem...

idanarye avatar Nov 04 '17 23:11 idanarye

The main difference between 1 and 2 is when the TextEdits are the calculated otherwise both require a custom command to be implemented by the client. 2nd way works well for jdt.ls at the moment because the way we determine code actions gets us very close to calculating TextEdits anyway hence more efficient.

gorkem avatar Feb 13 '18 22:02 gorkem

But option 2 doesn't conform with the protocol. https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction

When the command is selected the server should be contacted again (via the workspace/executeCommand) request to execute the command.

If the need for option 2 is real shouldn't it be pushed to the protocol?

amiramw avatar Feb 14 '18 12:02 amiramw

There is a proposal to support returning an edit instead of a command: https://github.com/Microsoft/language-server-protocol/issues/178

natebosch avatar Jun 05 '18 04:06 natebosch

But option 2 doesn't conform with the protocol. https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction

When the command is selected the server should be contacted again (via the workspace/executeCommand) request to execute the command.

If the need for option 2 is real shouldn't it be pushed to the protocol?

This provision was added AFTER jdt.ls implemented code actions this way. So "option 2" was at one time in the protocol, but was removed. We should probably move to using WorkspaceEdits, now that the can contain resource changes, but that would break existing clients.

tsmaeder avatar Oct 16 '18 08:10 tsmaeder

Hi guys - any update on this one? I see that lots of other clients are busy implementing custom commands (i.e. https://github.com/autozimu/LanguageClient-neovim/issues/158) which as mentioned above is unnecessary.

Won't most clients be able to handle WorkspaceEdits now? If so then moving across should be fine?

Cheers

gns26 avatar Jan 17 '19 09:01 gns26

Won't most clients be able to handle WorkspaceEdits now?

What makes you think that?

puremourning avatar Jan 17 '19 11:01 puremourning

Won't most clients be able to handle WorkspaceEdits now?

What makes you think that?

I saw the linked ticket had been closed back in August. But if most have not then it would have to wait, yes. (https://github.com/prabirshrestha/vim-lsp this one for instance does seem to)

gns26 avatar Jan 17 '19 12:01 gns26

Was testing JDT-LS integration with LSP4E and ran into this issue as well. Would it be possible to replace usage of "java.apply.workspaceEdit" in codeAction with workspaceEdit? It seems to be mentioned in CodeActionHandler.java:

if (preferenceManager.getClientPreferences().isSupportedCodeActionKind(proposal.getKind())) {
			// TODO: Should set WorkspaceEdit directly instead of Command
			CodeAction codeAction = new CodeAction(name);
			codeAction.setKind(proposal.getKind());
			codeAction.setCommand(command);
			codeAction.setDiagnostics(context.getDiagnostics());
			return Optional.of(Either.forRight(codeAction));
		} else {
			return Optional.of(Either.forLeft(command));
		}

Also, could we retitle this issue to something like Replace usage of java.apply.workspaceEdit in codeAction by workspaceEdit?

AObuchow avatar Sep 25 '19 15:09 AObuchow

The fix was reverted because of a regression. See https://github.com/eclipse/eclipse.jdt.ls/pull/1256#issuecomment-553582214

fbricon avatar Nov 13 '19 20:11 fbricon

Reverted the fix once again, sorry. Because of a formatting regression on vscode. See https://github.com/eclipse/eclipse.jdt.ls/pull/1059#issuecomment-559403554

fbricon avatar Nov 28 '19 18:11 fbricon

For the record, this can be fixed, while staying... ugh... bug compatible with vscode*.

JDTDelegateCommandHandler is a catch-all command handler. It's pretty trivial to extend it to support java.apply.workspaceEdit

diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTDelegateCommandHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTDelegateCommandHandler.java
index ec43feba..d0794361 100644
--- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTDelegateCommandHandler.java
+++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTDelegateCommandHandler.java
@@ -49,6 +49,14 @@ public class JDTDelegateCommandHandler implements IDelegateCommandHandler {
                                                // workspaceEdit on the custom command.
                                                return result;
                                        }
+                               case "java.command.applyEdit":^M
+                                       final WorkspaceEdit r = (WorkspaceEdit) arguments.get(0);^M
+                                       if (JavaLanguageServerPlugin.getPreferencesManager().getClientPreferences().isWorkspaceApplyEditSupported()) {^M
+                                               JavaLanguageServerPlugin.getInstance().getClientConnection().applyWorkspaceEdit((WorkspaceEdit) r);^M
+                                               return new Object();^M
+                                       } else {^M
+                                               return r;^M
+                                       }^M
                                case "java.project.resolveSourceAttachment":
                                        return SourceAttachmentCommand.resolveSourceAttachment(arguments, monitor);
                                case "java.project.updateSourceAttachment":

Note that there's also org.eclipse.jdt.ls.core/plugin.xml, which I don't know the purpose of. Anyway...

diff --git a/org.eclipse.jdt.ls.core/plugin.xml b/org.eclipse.jdt.ls.core/plugin.xml
index 75036602..209f41f9 100644
--- a/org.eclipse.jdt.ls.core/plugin.xml
+++ b/org.eclipse.jdt.ls.core/plugin.xml
@@ -54,6 +54,9 @@
             <command
                   id="java.edit.organizeImports">
             </command>
+            <command
+                  id="java.apply.workspaceEdit">
+            </command>
             <command
                   id="java.project.updateSourceAttachment">
             </command>

Doing this, the response to textDocument/codeAction remains the same and the hacks in vscode still "work" (quotation marks are there because hacks don't work, ever), but clients who respect the protocol can still make a workspace/executeCommand request.

I may have missed something (after compiling jdt, I have simply copied org.eclipse.jdt.ls.product/target/repository/ to where my client expects it to be), but the WorkspaceExecuteCommandHandler still doesn't know that JDTDelegateCommandHandler can handle java.apply.workspaceEdit.

I don't think I'll be working any further on this bug, but I would like to point out that not supporting executeCommand in this case is a protocol violation, hence why so many clients have ran into this bug.

* That basically was the reason why #1278 was reverted.

bstaletic avatar May 02 '20 09:05 bstaletic

Any update on this? Seems to have been open for a long time - I guess it's not ever going to get done?

gns26 avatar Jan 11 '21 10:01 gns26

Highly required thing, wish to see it fixed. Thanks in advance for all developers!

stychos avatar Jan 19 '21 02:01 stychos

With the new insertTextModeSupport client capability, jdtls should be able to determine what whitespace strategy to use, no?

rwols avatar Aug 22 '21 11:08 rwols

Are there any plans to fix this behavior?

nrabulinski avatar Apr 23 '23 09:04 nrabulinski

@fbricon given that you looked at previous attempts at fixing this could you take a look at linked PR, it should leave this compatible with vscode while allowing compliant clients to work as well. It basically does what @bstaletic suggested above 3 years ago

theli-ua avatar Jun 21 '23 17:06 theli-ua