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

Camelcade / Perl5-IDEA / #525521581

06 Jun 2025 08:26AM UTC coverage: 82.33% (-0.02%) from 82.354%
#525521581

push

github

hurricup
Moved debugger properties to the respective bundle

18 of 26 new or added lines in 8 files covered. (69.23%)

158 existing lines in 31 files now uncovered.

30858 of 37481 relevant lines covered (82.33%)

0.82 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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