• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Camelcade / Perl5-IDEA / #525521743

22 Dec 2025 02:05AM UTC coverage: 75.96% (-0.04%) from 76.003%
#525521743

Pull #3142

github

web-flow
Merge aaa78dc27 into 05c8d6205
Pull Request #3142: Bump JetBrains/qodana-action from 2025.2.4 to 2025.3.1

14761 of 22633 branches covered (65.22%)

Branch coverage included in aggregate %.

31083 of 37720 relevant lines covered (82.4%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.73
/mojo/backend/src/main/java/com/perl5/lang/mojolicious/model/MojoProjectManager.java
1
/*
2
 * Copyright 2015-2025 Alexandr Evstigneev
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

17
package com.perl5.lang.mojolicious.model;
18

19
import com.intellij.ide.projectView.ProjectView;
20
import com.intellij.openapi.Disposable;
21
import com.intellij.openapi.application.ApplicationManager;
22
import com.intellij.openapi.application.ReadAction;
23
import com.intellij.openapi.diagnostic.Logger;
24
import com.intellij.openapi.progress.ProgressManager;
25
import com.intellij.openapi.project.DumbService;
26
import com.intellij.openapi.project.Project;
27
import com.intellij.openapi.roots.ProjectFileIndex;
28
import com.intellij.openapi.vfs.VfsUtilCore;
29
import com.intellij.openapi.vfs.VirtualFile;
30
import com.intellij.psi.search.GlobalSearchScope;
31
import com.intellij.psi.util.PsiUtilCore;
32
import com.intellij.util.concurrency.Semaphore;
33
import com.intellij.util.containers.ContainerUtil;
34
import com.intellij.util.messages.MessageBusConnection;
35
import com.intellij.util.ui.update.MergingUpdateQueue;
36
import com.intellij.util.ui.update.Update;
37
import com.perl5.lang.mojolicious.MojoUtil;
38
import com.perl5.lang.mojolicious.idea.modules.MojoTemplateMarkSourceRootAction;
39
import com.perl5.lang.perl.idea.actions.PerlMarkLibrarySourceRootAction;
40
import com.perl5.lang.perl.psi.PerlNamespaceDefinitionElement;
41
import com.perl5.lang.perl.util.PerlPackageUtil;
42
import com.perl5.lang.perl.util.PerlPackageUtilCore;
43
import com.perl5.lang.perl.util.PerlPluginUtil;
44
import org.jetbrains.annotations.Contract;
45
import org.jetbrains.annotations.NotNull;
46
import org.jetbrains.annotations.Nullable;
47
import org.jetbrains.annotations.TestOnly;
48

49
import java.util.*;
50
import java.util.concurrent.atomic.AtomicBoolean;
51
import java.util.function.BooleanSupplier;
52

53
import static com.perl5.lang.mojolicious.model.MojoProjectListener.MOJO_PROJECT_TOPIC;
54

55
public class MojoProjectManager implements Disposable {
56
  static final Logger LOG = Logger.getInstance(MojoProjectManager.class);
1✔
57
  private final @NotNull Project myProject;
58
  private final @NotNull MergingUpdateQueue myUpdateQueue;
59
  private final @NotNull AtomicBoolean myUpdatingModel = new AtomicBoolean(false);
1✔
60
  private volatile @NotNull Model myModel = new Model(Collections.emptySet());
1✔
61
  private volatile Semaphore myTestSemaphore;
62

63
  public MojoProjectManager(@NotNull Project project) {
1✔
64
    myProject = project;
1✔
65
    myUpdateQueue = new MergingUpdateQueue("mojo model updating queue", 500, true, null, this, null, false);
1✔
66
    MessageBusConnection connection = myProject.getMessageBus().connect(this);
1✔
67
    connection.subscribe(DumbService.DUMB_MODE, new DumbService.DumbModeListener() {
1✔
68
      @Override
69
      public void exitDumbMode() {
70
        LOG.debug("Exiting dumb mode");
1✔
71
        scheduleUpdate();
1✔
72
      }
1✔
73
    });
74

75
    connection.subscribe(MOJO_PROJECT_TOPIC, new MojoProjectListener() {
1✔
76
      @Override
77
      public void applicationCreated(@NotNull MojoApp mojoApp) {
78
        ApplicationManager.getApplication().invokeLater(() -> {
1✔
79
          if (PerlPluginUtil.isUnloaded(myProject) || !mojoApp.isValid()) {
1!
80
            return;
1✔
81
          }
82
          VirtualFile appRoot = mojoApp.getRoot();
×
83
          new PerlMarkLibrarySourceRootAction().markRoot(myProject, appRoot.findChild(PerlPackageUtilCore.DEFAULT_LIB_DIR));
×
84
          new MojoTemplateMarkSourceRootAction().markRoot(myProject, appRoot.findChild(MojoUtil.DEFAULT_TEMPLATES_DIR_NAME));
×
85
        });
×
86
      }
1✔
87

88
      @Override
89
      public void pluginCreated(@NotNull MojoPlugin mojoPlugin) {
90
        ApplicationManager.getApplication().invokeLater(() -> {
1✔
91
          if (myProject.isDisposed() || !mojoPlugin.isValid()) {
1!
92
            return;
1✔
93
          }
94
          new PerlMarkLibrarySourceRootAction().markRoot(myProject, mojoPlugin.getRoot().findChild(PerlPackageUtilCore.DEFAULT_LIB_DIR));
1✔
95
        });
1✔
96
      }
1✔
97
    });
98
  }
1✔
99

100
  @Override
101
  public void dispose() {
102
    // nothing to dispose
103
  }
1✔
104

105
  /**
106
   * @return true iff mojo is available for the project: perl is enabled and mojo installed
107
   */
108
  public boolean isMojoAvailable() {
109
    return MojoUtil.isMojoAvailable(myProject);
×
110
  }
111

112
  @Contract("null->null")
113
  public @Nullable MojoProject getMojoProject(@Nullable VirtualFile root) {
114
    return myModel.myProjectRoots.get(root);
×
115
  }
116

117
  public @NotNull List<MojoProject> getMojoProjects() {
118
    return List.copyOf(myModel.myProjectRoots.values());
1!
119
  }
120

121
  /**
122
   * Queues model update
123
   */
124
  void scheduleUpdate() {
125
    LOG.debug("Scheduling update");
1✔
126
    myUpdateQueue.queue(Update.create(this, this::updateModel));
1✔
127
  }
1✔
128

129
  /**
130
   * Attempts to update a model in read action with smart mode. If update fails, re-schedules it
131
   */
132
  private void updateModel() {
133
    LOG.debug("Attempting to update");
1✔
134
    if (!myUpdatingModel.getAndSet(true)) {
1!
135
      try {
136
        //noinspection deprecation
137
        var isFinished = ReadAction.nonBlocking(() -> {
1✔
138
          if (myProject.isDisposed()) {
1✔
139
            LOG.warn("Project is disposed");
1✔
140
            return true;
1✔
141
          }
142
          if (DumbService.getInstance(myProject).isDumb()) {
1✔
143
            LOG.debug("Dumb mode, rescheduling");
1✔
144
            scheduleUpdate();
1✔
145
            return false;
1✔
146
          }
147
          else {
148
            LOG.debug("Performing model update");
1✔
149
            doUpdateModel();
1✔
150
            return true;
1✔
151
          }
152
        }).executeSynchronously();
1✔
153
        if (myTestSemaphore != null && isFinished) {
1✔
154
          myTestSemaphore.up();
1✔
155
          myTestSemaphore = null;
1✔
156
        }
157
      }
158
      finally {
159
        myUpdatingModel.set(false);
1✔
160
      }
1✔
161
    }
162
    else {
163
      LOG.debug("Another update in progress, rescheduling");
×
164
      scheduleUpdate();
×
165
    }
166
  }
1✔
167

168
  /**
169
   * Performs an update. This method invoked with guaranteed conditions: smart mode, read action and project is alive
170
   */
171
  private void doUpdateModel() {
172
    LOG.debug("Updating model");
1✔
173
    Set<MojoProject> newProjects = findAllProjects();
1✔
174
    Set<MojoProject> oldProjects = myModel.getProjects();
1✔
175
    if (oldProjects.equals(newProjects)) {
1✔
176
      LOG.debug("Model was not changed");
1✔
177
      return;
1✔
178
    }
179

180
    LOG.debug("Current projects: ", newProjects);
1✔
181
    LOG.debug("Old projects: ", oldProjects);
1✔
182
    MojoProjectListener projectListener = myProject.getMessageBus().syncPublisher(MOJO_PROJECT_TOPIC);
1✔
183
    Collection<MojoProject> removedProjects = ContainerUtil.subtract(oldProjects, newProjects);
1✔
184
    if (!removedProjects.isEmpty()) {
1!
185
      LOG.debug("Projects removed: ", removedProjects);
×
186
    }
187
    removedProjects.forEach(projectListener::projectDeleted);
1✔
188
    myModel = new Model(newProjects);
1✔
189
    Collection<MojoProject> createdProjects = ContainerUtil.subtract(newProjects, oldProjects);
1✔
190
    if (!createdProjects.isEmpty()) {
1!
191
      LOG.debug("Projects created: ", createdProjects);
1✔
192
    }
193
    createdProjects.forEach(projectListener::projectCreated);
1✔
194
    LOG.debug("Model updated");
1✔
195
    ApplicationManager.getApplication().invokeLater(() -> {
1✔
196
      if (!myProject.isDisposed() && !ApplicationManager.getApplication().isUnitTestMode()) {
1!
197
        ProjectView.getInstance(myProject).refresh();
×
198
      }
199
    });
1✔
200
  }
1✔
201

202
  /**
203
   * @return a set of mojo entities in project
204
   */
205
  private @NotNull Set<MojoProject> findAllProjects() {
206
    if (!MojoUtil.isMojoAvailable(myProject)) {
1✔
207
      LOG.debug("Mojo is not available in project");
1✔
208
      return Collections.emptySet();
1!
209
    }
210
    HashSet<MojoProject> result = new HashSet<>();
1✔
211
    List<PerlNamespaceDefinitionElement> applicationClasses = PerlPackageUtil.getChildNamespaces(
1✔
212
      myProject, MojoUtil.MOJO_PACKAGE_NAME, GlobalSearchScope.projectScope(myProject));
1✔
213
    for (PerlNamespaceDefinitionElement namespace : applicationClasses) {
1✔
214
      LOG.debug("Got application class: " + namespace);
1✔
215
      ProgressManager.checkCanceled();
1✔
216
      VirtualFile root = findLibContainer(namespace);
1✔
217
      if (root != null) {
1!
218
        LOG.debug("App root: " + root);
1✔
219
        result.add(new MojoApp(root));
1✔
220
      }
221
      else {
222
        LOG.debug("No app root");
×
223
      }
224
    }
1✔
225

226
    List<PerlNamespaceDefinitionElement> pluginClasses = PerlPackageUtil.getChildNamespaces(
1✔
227
      myProject, MojoUtil.MOJO_PLUGIN_PACKAGE_NAME, GlobalSearchScope.projectScope(myProject));
1✔
228
    for (PerlNamespaceDefinitionElement namespace : pluginClasses) {
1✔
229
      LOG.debug("Got plugin class: " + namespace);
1✔
230
      ProgressManager.checkCanceled();
1✔
231
      VirtualFile root = findLibContainer(namespace);
1✔
232
      if (root != null) {
1!
233
        LOG.debug("Plugin root: " + root);
1✔
234
        result.add(new MojoPlugin(root));
1✔
235
      }
236
      else {
237
        LOG.debug("No plugin root");
×
238
      }
239
    }
1✔
240
    return result;
1!
241
  }
242

243
  /**
244
   * @return a directory in project, containing a {@code lib} directory, containing current class or null
245
   */
246
  private @Nullable VirtualFile findLibContainer(@NotNull PerlNamespaceDefinitionElement namespaceDefinition) {
247
    VirtualFile namespaceFile = PsiUtilCore.getVirtualFile(namespaceDefinition);
1✔
248
    if (namespaceFile == null) {
1!
249
      LOG.debug("Namespace without a virtual file");
×
250
      return null;
×
251
    }
252
    VirtualFile libDirectory = VfsUtilCore.findContainingDirectory(namespaceFile, PerlPackageUtilCore.DEFAULT_LIB_DIR);
1✔
253
    if (libDirectory == null) {
1!
254
      LOG.debug("No containing lib dir found");
×
255
      return null;
×
256
    }
257
    VirtualFile root = libDirectory.getParent();
1✔
258
    if (root != null && ProjectFileIndex.getInstance(myProject).isInContent(root)) {
1!
259
      return root;
1✔
260
    }
261
    LOG.debug("No root or not in content: " + root);
×
262
    return null;
×
263
  }
264

265
  public static @NotNull MojoProjectManager getInstance(@NotNull Project project) {
266
    return project.getService(MojoProjectManager.class);
1!
267
  }
268

269
  private static class Model {
270
    private final @NotNull Map<VirtualFile, MojoProject> myProjectRoots;
271

272
    private Model(@NotNull Set<? extends MojoProject> mojoProjects) {
1✔
273
      if (mojoProjects.isEmpty()) {
1✔
274
        myProjectRoots = Collections.emptyMap();
1✔
275
        return;
1✔
276
      }
277
      Map<VirtualFile, MojoProject> map = new HashMap<>();
1✔
278
      mojoProjects.forEach(it -> map.put(it.getRoot(), it));
1✔
279
      myProjectRoots = Collections.unmodifiableMap(map);
1✔
280
    }
1✔
281

282
    private Set<MojoProject> getProjects() {
283
      return myProjectRoots.isEmpty() ? Collections.emptySet() : new HashSet<>(myProjectRoots.values());
1!
284
    }
285
  }
286

287
  @TestOnly
288
  public BooleanSupplier updateInTestMode() {
289
    var semaphore = new Semaphore(1);
1✔
290
    myTestSemaphore = semaphore;
1✔
291
    updateModel();
1✔
292
    return semaphore::isUp;
1✔
293
  }
294
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc