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

Camelcade / Perl5-IDEA / #525521650

25 Jul 2025 09:58AM UTC coverage: 82.196% (-0.09%) from 82.289%
#525521650

push

github

hurricup
Migrated module packaging to pluginModule

30956 of 37661 relevant lines covered (82.2%)

0.82 hits per line

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

85.07
/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 org.jetbrains.annotations.Contract;
44
import org.jetbrains.annotations.NotNull;
45
import org.jetbrains.annotations.Nullable;
46
import org.jetbrains.annotations.TestOnly;
47

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

286
  @TestOnly
287
  public BooleanSupplier updateInTestMode() {
288
    var semaphore = new Semaphore(1);
1✔
289
    myTestSemaphore = semaphore;
1✔
290
    updateModel();
1✔
291
    return semaphore::isUp;
1✔
292
  }
293
}
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

© 2026 Coveralls, Inc