[platform] lambda-friendly WriteCommandAction
[idea/community.git] / platform / core-api / src / com / intellij / openapi / command / WriteCommandAction.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.command;
17
18 import com.intellij.codeInsight.FileModificationService;
19 import com.intellij.openapi.application.*;
20 import com.intellij.openapi.diagnostic.Logger;
21 import com.intellij.openapi.progress.ProcessCanceledException;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.util.Computable;
24 import com.intellij.openapi.util.Ref;
25 import com.intellij.openapi.util.ThrowableComputable;
26 import com.intellij.psi.PsiFile;
27 import com.intellij.util.ArrayUtil;
28 import com.intellij.util.ThrowableRunnable;
29 import org.jetbrains.annotations.NotNull;
30 import org.jetbrains.annotations.Nullable;
31
32 import java.util.Arrays;
33 import java.util.Collection;
34
35 public abstract class WriteCommandAction<T> extends BaseActionRunnable<T> {
36   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.command.WriteCommandAction");
37
38   private static final String DEFAULT_COMMAND_NAME = "Undefined";
39   private static final String DEFAULT_GROUP_ID = null;
40
41   public interface Builder {
42     @NotNull Builder withName(@Nullable String name);
43     @NotNull Builder withGroupId(@Nullable String groupId);
44
45     <E extends Throwable> void run(@NotNull ThrowableRunnable<E> action) throws E;
46     <R, E extends Throwable> R compute(@NotNull ThrowableComputable<R, E> action) throws E;
47   }
48
49   private static class BuilderImpl implements Builder {
50     private final Project myProject;
51     private final PsiFile[] myFiles;
52     private String myCommandName = DEFAULT_COMMAND_NAME;
53     private String myGroupId = DEFAULT_GROUP_ID;
54
55     private BuilderImpl(Project project, PsiFile... files) {
56       myProject = project;
57       myFiles = files;
58     }
59
60     @NotNull
61     @Override
62     public Builder withName(String name) {
63       myCommandName = name;
64       return this;
65     }
66
67     @NotNull
68     @Override
69     public Builder withGroupId(String groupId) {
70       myGroupId = groupId;
71       return this;
72     }
73
74     @Override
75     public <E extends Throwable> void run(@NotNull final ThrowableRunnable<E> action) throws E {
76       new WriteCommandAction(myProject, myCommandName, myGroupId, myFiles) {
77         @Override
78         protected void run(@NotNull Result result) throws Throwable {
79           action.run();
80         }
81       }.execute();
82     }
83
84     @Override
85     public <R, E extends Throwable> R compute(@NotNull final ThrowableComputable<R, E> action) throws E {
86       return new WriteCommandAction<R>(myProject, myCommandName, myGroupId, myFiles) {
87         @Override
88         protected void run(@NotNull Result<R> result) throws Throwable {
89           result.setResult(action.compute());
90         }
91       }.execute().getResultObject();
92     }
93   }
94
95   @NotNull
96   public static Builder writeCommandAction(Project project) {
97     return new BuilderImpl(project);
98   }
99
100   @NotNull
101   public static Builder writeCommandAction(@NotNull PsiFile first, @NotNull PsiFile... others) {
102     return new BuilderImpl(first.getProject(), ArrayUtil.prepend(first, others));
103   }
104
105   private final String myCommandName;
106   private final String myGroupID;
107   private final Project myProject;
108   private final PsiFile[] myPsiFiles;
109
110   protected WriteCommandAction(@Nullable Project project, /*@NotNull*/ PsiFile... files) {
111     this(project, DEFAULT_COMMAND_NAME, files);
112   }
113
114   protected WriteCommandAction(@Nullable Project project, @Nullable String commandName, /*@NotNull*/ PsiFile... files) {
115     this(project, commandName, DEFAULT_GROUP_ID, files);
116   }
117
118   protected WriteCommandAction(@Nullable Project project, @Nullable String commandName, @Nullable String groupID, /*@NotNull*/ PsiFile... files) {
119     myCommandName = commandName;
120     myGroupID = groupID;
121     myProject = project;
122     if (files == null) LOG.warn("'files' parameter must not be null", new Throwable());
123     myPsiFiles = files == null || files.length == 0 ? PsiFile.EMPTY_ARRAY : files;
124   }
125
126   public final Project getProject() {
127     return myProject;
128   }
129
130   public final String getCommandName() {
131     return myCommandName;
132   }
133
134   public String getGroupID() {
135     return myGroupID;
136   }
137
138   @NotNull
139   @Override
140   public RunResult<T> execute() {
141     Application application = ApplicationManager.getApplication();
142     boolean dispatchThread = application.isDispatchThread();
143
144     if (!dispatchThread && application.isReadAccessAllowed()) {
145       LOG.error("Must not start write action from within read action in the other thread - deadlock is coming");
146       throw new IllegalStateException();
147     }
148
149     final RunResult<T> result = new RunResult<T>(this);
150     if (dispatchThread) {
151       performWriteCommandAction(result);
152     }
153     else {
154       try {
155         TransactionGuard.getInstance().submitTransactionAndWait(new Runnable() {
156           @Override
157           public void run() {
158             performWriteCommandAction(result);
159           }
160         });
161       }
162       catch (ProcessCanceledException ignored) { }
163     }
164     return result;
165   }
166
167   private void performWriteCommandAction(@NotNull RunResult<T> result) {
168     if (!FileModificationService.getInstance().preparePsiElementsForWrite(Arrays.asList(myPsiFiles))) return;
169
170     // this is needed to prevent memory leak, since the command is put into undo queue
171     final RunResult[] results = {result};
172
173     doExecuteCommand(new Runnable() {
174       @Override
175       public void run() {
176         //noinspection deprecation
177         ApplicationManager.getApplication().runWriteAction(new Runnable() {
178           @Override
179           public void run() {
180             results[0].run();
181             results[0] = null;
182           }
183         });
184       }
185     });
186   }
187
188   protected boolean isGlobalUndoAction() {
189     return false;
190   }
191
192   protected UndoConfirmationPolicy getUndoConfirmationPolicy() {
193     return UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION;
194   }
195
196   /**
197    * See {@link CommandProcessor#executeCommand(Project, Runnable, String, Object, UndoConfirmationPolicy, boolean)} for details.
198    */
199   protected boolean shouldRecordActionForActiveDocument() {
200     return true;
201   }
202
203   public void performCommand() throws Throwable {
204     //this is needed to prevent memory leak, since command
205     // is put into undo queue
206     final RunResult[] results = {new RunResult<T>(this)};
207     final Ref<Throwable> exception = new Ref<Throwable>();
208
209     doExecuteCommand(new Runnable() {
210       @Override
211       public void run() {
212         exception.set(results[0].run().getThrowable());
213         results[0] = null;
214       }
215     });
216
217     Throwable throwable = exception.get();
218     if (throwable != null) throw throwable;
219   }
220
221   private void doExecuteCommand(final Runnable runnable) {
222     Runnable wrappedRunnable = new Runnable() {
223       @Override
224       public void run() {
225         if (isGlobalUndoAction()) CommandProcessor.getInstance().markCurrentCommandAsGlobal(getProject());
226         runnable.run();
227       }
228     };
229     CommandProcessor.getInstance().executeCommand(getProject(), wrappedRunnable, getCommandName(), getGroupID(),
230                                                   getUndoConfirmationPolicy(), shouldRecordActionForActiveDocument());
231   }
232
233   /**
234    * WriteCommandAction without result
235    */
236   public abstract static class Simple<T> extends WriteCommandAction<T> {
237     protected Simple(Project project, /*@NotNull*/ PsiFile... files) {
238       super(project, files);
239     }
240
241     protected Simple(Project project, String commandName, /*@NotNull*/ PsiFile... files) {
242       super(project, commandName, files);
243     }
244
245     protected Simple(Project project, String name, String groupID, /*@NotNull*/ PsiFile... files) {
246       super(project, name, groupID, files);
247     }
248
249     @Override
250     protected void run(@NotNull Result<T> result) throws Throwable {
251       run();
252     }
253
254     protected abstract void run() throws Throwable;
255   }
256
257   public static void runWriteCommandAction(Project project, @NotNull Runnable runnable) {
258     runWriteCommandAction(project, DEFAULT_COMMAND_NAME, DEFAULT_GROUP_ID, runnable);
259   }
260
261   public static void runWriteCommandAction(Project project,
262                                            @Nullable final String commandName,
263                                            @Nullable final String groupID,
264                                            @NotNull final Runnable runnable,
265                                            @NotNull PsiFile... files) {
266     new Simple(project, commandName, groupID, files) {
267       @Override
268       protected void run() throws Throwable {
269         runnable.run();
270       }
271     }.execute();
272   }
273
274   @SuppressWarnings("LambdaUnfriendlyMethodOverload")
275   public static <T> T runWriteCommandAction(Project project, @NotNull final Computable<T> computable) {
276     return new WriteCommandAction<T>(project) {
277       @Override
278       protected void run(@NotNull Result<T> result) throws Throwable {
279         result.setResult(computable.compute());
280       }
281     }.execute().getResultObject();
282   }
283
284   @SuppressWarnings("LambdaUnfriendlyMethodOverload")
285   public static <T, E extends Throwable> T runWriteCommandAction(Project project, @NotNull final ThrowableComputable<T, E> computable) throws E {
286     RunResult<T> result = new WriteCommandAction<T>(project, "") {
287       @Override
288       protected void run(@NotNull Result<T> result) throws Throwable {
289         result.setResult(computable.compute());
290       }
291     }.execute();
292     Throwable t = result.getThrowable();
293     if (t != null) { @SuppressWarnings("unchecked") E e = (E)t; throw e; }
294     return result.throwException().getResultObject();
295   }
296
297   //<editor-fold desc="Deprecated stuff.">
298   /** @deprecated use {@link FileModificationService#preparePsiElementsForWrite(Collection)} (to be removed in IDEA 2018) */
299   @SuppressWarnings("unused")
300   public static boolean ensureFilesWritable(@NotNull Project project, @NotNull Collection<PsiFile> psiFiles) {
301     return FileModificationService.getInstance().preparePsiElementsForWrite(psiFiles);
302   }
303   //</editor-fold>
304 }