-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathSharedAppMapWebViewMessages.java
More file actions
245 lines (215 loc) · 10.1 KB
/
Copy pathSharedAppMapWebViewMessages.java
File metadata and controls
245 lines (215 loc) · 10.1 KB
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
package appland.webviews;
import appland.AppMapBundle;
import appland.actions.OpenInRightSplit;
import appland.files.FileLocation;
import appland.files.FileLookup;
import appland.notifications.AppMapNotifications;
import appland.settings.AppMapProjectSettingsService;
import appland.settings.AppMapWebViewFilter;
import appland.telemetry.TelemetryEvent;
import appland.telemetry.TelemetryService;
import appland.utils.GsonUtils;
import appland.webviews.appMap.ExportSvgUtil;
import com.google.gson.JsonObject;
import com.intellij.ide.actions.RevealFileAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import static com.intellij.openapi.ui.Messages.showErrorDialog;
/**
* Handling of common {@link WebviewEditor} messages for AppMap views, i.e. for the AppMap editor and the
* AppMap view embedded in the {@link appland.webviews.navie.NavieEditor} webview.
*/
public final class SharedAppMapWebViewMessages {
private SharedAppMapWebViewMessages() {
}
private static final Logger LOG = Logger.getInstance(SharedAppMapWebViewMessages.class);
private final static Set<String> BASE_MESSAGES = Set.of(
"clearSelection",
"clickFilterButton",
"clickTab",
"defaultFilter",
"deleteFilter",
"exportJSON",
"exportSVG",
"resetDiagram",
"saveFilter",
"selectObjectInSidebar",
"sidebarSearchFocused",
"viewSource");
/**
* @param additionalMessages Additional messages handled by the calling WebView editor
* @return Basic messages support by this class and additional messages supported by the {@link WebviewEditor} calling this method.
*/
public static @NotNull Set<String> withBaseMessages(@NotNull String... additionalMessages) {
var result = new HashSet<>(BASE_MESSAGES);
Collections.addAll(result, additionalMessages);
return result;
}
/**
* Handle the given webview message.
*
* @param project Project
* @param editor WebView editor
* @param messageId Message to handle
* @param message Message payload
* @return {@code true} if this message was successfully handled by this method. {@code false} indicates that the caller is supposed to handle it afterward.
*/
public static boolean handleMessage(@NotNull Project project,
@NotNull WebviewEditor<?> editor,
@NotNull String messageId,
@Nullable JsonObject message) {
if (!BASE_MESSAGES.contains(messageId)) {
return false;
}
var gson = editor.gson;
switch (messageId) {
case "clearSelection":
// set empty state to the editor to restore with cleared selection
editor.clearState();
return true;
case "viewSource":
// message is {..., location: {location:"path/file.java", externalSource="path/file.java"}}
assert message != null;
assert message.has("location");
showSource(project, editor, message.getAsJsonObject("location").getAsJsonPrimitive("location").getAsString());
return true;
// known message, but not handled
case "sidebarSearchFocused":
return true;
// known message, but not handled
case "clickFilterButton":
return true;
case "clickTab":
if (message != null) {
var tabId = message.getAsJsonPrimitive("tabId");
if (tabId.isString()) {
TelemetryService.getInstance().sendEvent(new TelemetryEvent("click_tab")
.withProperty("appmap.click_tab.tabId", tabId.getAsString()));
}
}
return true;
// known message, but not handled
case "selectObjectInSidebar":
return true;
// known message, but not handled
case "resetDiagram":
return true;
case "exportSVG":
if (message != null) {
exportSVG(project, editor, message);
}
return true;
case "exportJSON":
if (message != null) {
exportJSON(project, message);
}
return true;
// filters
case "saveFilter":
if (message != null && message.has("filter")) {
var filter = gson.fromJson(message.getAsJsonObject("filter"), AppMapWebViewFilter.class);
AppMapProjectSettingsService.getState(project).saveAppMapWebViewFilter(filter);
}
return true;
case "defaultFilter":
if (message != null && message.has("filter")) {
var filter = gson.fromJson(message.getAsJsonObject("filter"), AppMapWebViewFilter.class);
AppMapProjectSettingsService.getState(project).saveDefaultFilter(filter);
}
return true;
case "deleteFilter":
if (message != null && message.has("filter")) {
var filter = gson.fromJson(message.getAsJsonObject("filter"), AppMapWebViewFilter.class);
AppMapProjectSettingsService.getState(project).removeAppMapWebViewFilter(filter);
}
return true;
default:
LOG.debug("Unexpected AppMap webview message: " + messageId);
return false;
}
}
@RequiresBackgroundThread
private static void showSource(@NotNull Project project,
@NotNull WebviewEditor<?> editor,
@NotNull String relativePath) {
var location = FileLocation.parse(relativePath);
if (location == null) {
showShowSourceError(relativePath);
return;
}
var referencedFile = ReadAction.compute(() -> {
return FileLookup.findRelativeFile(project, editor.getFile(), FileUtil.toSystemIndependentName(location.filePath));
});
if (referencedFile == null) {
showShowSourceError(relativePath);
return;
}
LOG.debug("Resolved file for " + relativePath + ": " + referencedFile.getPath());
ApplicationManager.getApplication().invokeLater(() -> {
int line = location.getZeroBasedLine(-1);
// Ignore line numbers for .class files - decompiled text layout rarely matches
// the original source line numbers, so jumping to a specific line usually lands
// in the wrong place. Open at the top instead.
if (referencedFile.getName().endsWith(".class")) {
line = -1;
}
// IntelliJ's lines are 0-based, AppMap lines seem to be 1-based
var descriptor = new OpenFileDescriptor(project, referencedFile, line, -1);
OpenInRightSplit.openInRightSplit(project, referencedFile, descriptor);
}, ModalityState.defaultModalityState());
}
private static void showShowSourceError(@NotNull String relativePath) {
ApplicationManager.getApplication().invokeLater(() -> {
var title = AppMapBundle.get("appmap.editor.showSourceFileMissing.title");
var message = AppMapBundle.get("appmap.editor.showSourceFileMissing.text", relativePath);
showErrorDialog(message, title);
}, ModalityState.defaultModalityState());
}
private static void exportSVG(@NotNull Project project, @NotNull WebviewEditor<?> editor, @NotNull JsonObject message) {
var svgString = message.getAsJsonPrimitive("svgString");
assert svgString.isString();
// choose new or existing file, write content, then open editor with the new file
ApplicationManager.getApplication().invokeLater(() -> {
ExportSvgUtil.exportToFile(project, "appMap.svg", editor.getFile(), svgString::getAsString, file -> {
new OpenFileDescriptor(project, file).navigate(true);
});
}, ModalityState.defaultModalityState());
}
private static void exportJSON(@NotNull Project project, @NotNull JsonObject message) {
if (!message.has("appmapData")) {
return;
}
var appMapData = message.getAsJsonObject("appmapData");
var metadata = appMapData.has("metadata") ? appMapData.getAsJsonObject("metadata") : null;
var basename = metadata != null && metadata.has("name")
? metadata.getAsJsonPrimitive("name").getAsString()
: createRandomString(16);
try {
var tempFile = FileUtil.createTempFile(basename.replaceAll("[^a-zA-Z0-9\\-_ ]", "_"), ".appmap.json", false);
FileUtil.writeToFile(tempFile, GsonUtils.GSON.toJson(appMapData));
RevealFileAction.openFile(tempFile);
} catch (IOException e) {
LOG.debug("Exception creating or writing to AppMap JSON file", e);
AppMapNotifications.showAppMapJsonExportFailedNotification(project, e.getLocalizedMessage());
}
}
private static @NotNull String createRandomString(int length) {
var buffer = new byte[length];
new Random().nextBytes(buffer);
return StringUtil.toHexString(buffer);
}
}