forked from eclipse-lsp4e/lsp4e
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathCommandExecutor.java
259 lines (241 loc) · 10.1 KB
/
CommandExecutor.java
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
/*******************************************************************************
* Copyright (c) 2019, 2023 Fraunhofer FOKUS and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.lsp4e.command;
import static org.eclipse.lsp4e.command.LSPCommandHandler.LSP_COMMAND_PARAMETER_ID;
import static org.eclipse.lsp4e.command.LSPCommandHandler.LSP_PATH_PARAMETER_ID;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import org.eclipse.core.commands.Category;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.IParameter;
import org.eclipse.core.commands.NotEnabledException;
import org.eclipse.core.commands.NotHandledException;
import org.eclipse.core.commands.ParameterType;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.commands.common.NotDefinedException;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.text.IDocument;
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.eclipse.lsp4e.LanguageServerPlugin;
import org.eclipse.lsp4e.command.internal.CommandEventParameter;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.handlers.IHandlerService;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
/**
* This class provides methods to execute {@link Command} instances.
* <p>
* This class is deprecated and will likely be removed in the future,
* when the LSP protocol provides standardized support for `client/executeCommand`
* messages. See https://github.com/microsoft/language-server-protocol/pull/1119
*/
@Deprecated
public class CommandExecutor {
private static final String LSP_COMMAND_CATEGORY_ID = "org.eclipse.lsp4e.commandCategory"; //$NON-NLS-1$
private static final String LSP_COMMAND_PARAMETER_TYPE_ID = "org.eclipse.lsp4e.commandParameterType"; //$NON-NLS-1$
private static final String LSP_PATH_PARAMETER_TYPE_ID = "org.eclipse.lsp4e.pathParameterType"; //$NON-NLS-1$
/**
* @param languageServerId unused
* @deprecated use {@link #executeCommandClientSide(Command, IDocument)}
*/
@Deprecated(forRemoval = true)
public static CompletableFuture<Object> executeCommand(@Nullable Command command, @Nullable IDocument document, @Nullable String languageServerId) {
if (command != null && document != null) {
return executeCommandClientSide(command, document);
}
return CompletableFuture.completedFuture(null);
}
public static CompletableFuture<Object> executeCommandClientSide(@NonNull Command command, @NonNull IDocument document) {
IPath path = LSPEclipseUtils.toPath(document);
if (path == null) {
path = ResourcesPlugin.getWorkspace().getRoot().getLocation();
}
CompletableFuture<Object> r = executeCommandClientSide(command, path);
if (r != null) {
return r;
}
URI uri = LSPEclipseUtils.toUri(document);
if (uri != null) {
return CommandExecutor.executeFallbackClientSide(command, uri);
}
return CompletableFuture.completedFuture(null);
}
public static CompletableFuture<Object> executeCommandClientSide(@NonNull Command command, @NonNull IResource resource) {
CompletableFuture<Object> r = executeCommandClientSide(command, resource.getFullPath());
if (r != null) {
return r;
}
URI uri = LSPEclipseUtils.toUri(resource);
if (uri != null) {
return executeFallbackClientSide(command, uri);
}
return CompletableFuture.completedFuture(null);
}
@SuppressWarnings("unused") // ECJ compiler handlerService cannot be null because getService is declared as
// <T> T getService(Class<T> api), it infers the input is Class<@NonNull IHandlerService> and the output
// @NonNull IHandlerService, as it takes over the @NonNull annotation when inferring the return type, which
// is a bug in its implementation
private static CompletableFuture<Object> executeCommandClientSide(@NonNull Command command, @Nullable IPath path) {
IWorkbench workbench = PlatformUI.getWorkbench();
if (workbench == null) {
return null;
}
ParameterizedCommand parameterizedCommand = createEclipseCoreCommand(command, path, workbench);
if (parameterizedCommand == null) {
return null;
}
@Nullable
IHandlerService handlerService = workbench.getService(IHandlerService.class);
if (handlerService == null) {
return null;
}
try {
CompletableFuture<Object> r = CompletableFuture.completedFuture(handlerService.executeCommand(parameterizedCommand, null));
if (r != null) {
return r;
}
} catch (ExecutionException | NotDefinedException e) {
LanguageServerPlugin.logError(e);
} catch (NotEnabledException | NotHandledException e2) {
}
return null;
}
// tentative fallback
private static CompletableFuture<Object> executeFallbackClientSide(@NonNull Command command, @NonNull URI initialUri) {
if (command.getArguments() != null) {
WorkspaceEdit edit = createWorkspaceEdit(command.getArguments(), initialUri);
LSPEclipseUtils.applyWorkspaceEdit(edit, command.getTitle());
return CompletableFuture.completedFuture(null);
}
return null;
}
@SuppressWarnings("unused") // ECJ compiler thinks commandService cannot be null (see above)
private static ParameterizedCommand createEclipseCoreCommand(@NonNull Command command, IPath context,
@NonNull IWorkbench workbench) {
// Usually commands are defined via extension point, but we synthesize one on
// the fly for the command ID, since we do not want downstream users
// having to define them.
String commandId = command.getCommand();
@Nullable
ICommandService commandService = workbench.getService(ICommandService.class);
if (commandService == null) {
return null;
}
org.eclipse.core.commands.Command coreCommand = commandService.getCommand(commandId);
if (!coreCommand.isDefined()) {
ParameterType commandParamType = commandService.getParameterType(LSP_COMMAND_PARAMETER_TYPE_ID);
ParameterType pathParamType = commandService.getParameterType(LSP_PATH_PARAMETER_TYPE_ID);
Category category = commandService.getCategory(LSP_COMMAND_CATEGORY_ID);
IParameter[] parameters = {
new CommandEventParameter(commandParamType, command.getTitle(), LSP_COMMAND_PARAMETER_ID),
new CommandEventParameter(pathParamType, command.getTitle(), LSP_PATH_PARAMETER_ID)};
coreCommand.define(commandId, null, category, parameters);
}
final var parameters = new HashMap<Object, Object>();
parameters.put(LSP_COMMAND_PARAMETER_ID, command);
parameters.put(LSP_PATH_PARAMETER_ID, context);
ParameterizedCommand parameterizedCommand = ParameterizedCommand.generateCommand(coreCommand, parameters);
return parameterizedCommand;
}
// TODO consider using Entry/SimpleEntry instead
private static final class Pair<K, V> {
K key;
V value;
Pair(K key, V value) {
this.key = key;
this.value = value;
}
}
// this method may be turned public if needed elsewhere
/**
* Very empirical and unsafe heuristic to turn unknown command arguments into a
* workspace edit...
*/
private static WorkspaceEdit createWorkspaceEdit(List<Object> commandArguments, @NonNull URI initialUri) {
final var workspaceEdit = new WorkspaceEdit();
final var changes = new HashMap<String, List<TextEdit>>();
workspaceEdit.setChanges(changes);
final var currentEntry = new Pair<URI, List<TextEdit>>(initialUri, new ArrayList<>());
commandArguments.stream().flatMap(item -> {
if (item instanceof List<?> list) {
return list.stream();
} else {
return Collections.singleton(item).stream();
}
}).forEach(arg -> {
if (arg instanceof String argString) {
changes.put(currentEntry.key.toString(), currentEntry.value);
IResource res = LSPEclipseUtils.findResourceFor(argString);
if (res != null) {
currentEntry.key = res.getLocationURI();
currentEntry.value = new ArrayList<>();
}
} else if (arg instanceof WorkspaceEdit wsEdit) {
changes.putAll(wsEdit.getChanges());
} else if (arg instanceof TextEdit textEdit) {
currentEntry.value.add(textEdit);
} else if (arg instanceof Map) {
final var gson = new Gson(); // TODO? retrieve the GSon used by LS
TextEdit edit = gson.fromJson(gson.toJson(arg), TextEdit.class);
if (edit != null) {
currentEntry.value.add(edit);
}
} else if (arg instanceof JsonPrimitive json) {
if (json.isString()) {
changes.put(currentEntry.key.toString(), currentEntry.value);
IResource res = LSPEclipseUtils.findResourceFor(json.getAsString());
if (res != null) {
currentEntry.key = res.getLocationURI();
currentEntry.value = new ArrayList<>();
}
}
} else if (arg instanceof JsonArray jsonArray) {
final var gson = new Gson(); // TODO? retrieve the GSon used by LS
jsonArray.forEach(elt -> {
TextEdit edit = gson.fromJson(gson.toJson(elt), TextEdit.class);
if (edit != null) {
currentEntry.value.add(edit);
}
});
} else if (arg instanceof JsonObject jsonObject) {
final var gson = new Gson(); // TODO? retrieve the GSon used by LS
WorkspaceEdit wEdit = gson.fromJson(jsonObject, WorkspaceEdit.class);
Map<String, List<TextEdit>> entries = wEdit.getChanges();
if (wEdit != null && !entries.isEmpty()) {
changes.putAll(entries);
} else {
TextEdit edit = gson.fromJson(jsonObject, TextEdit.class);
if (edit != null && edit.getRange() != null) {
currentEntry.value.add(edit);
}
}
}
});
if (!currentEntry.value.isEmpty()) {
changes.put(currentEntry.key.toASCIIString(), currentEntry.value);
}
return workspaceEdit;
}
}