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

Camelcade / Perl5-IDEA / #525521543

22 May 2025 09:13AM UTC coverage: 82.34% (+0.1%) from 82.228%
#525521543

push

github

hurricup
Improved localization handling

Still imperfect, but good enough

0 of 2 new or added lines in 1 file covered. (0.0%)

113 existing lines in 7 files now uncovered.

30912 of 37542 relevant lines covered (82.34%)

0.82 hits per line

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

83.33
/plugin/core/src/main/java/com/perl5/lang/perl/idea/project/PerlProjectManager.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.perl.idea.project;
18

19
import com.intellij.notification.Notification;
20
import com.intellij.notification.NotificationType;
21
import com.intellij.notification.Notifications;
22
import com.intellij.openapi.Disposable;
23
import com.intellij.openapi.actionSystem.AnActionEvent;
24
import com.intellij.openapi.actionSystem.CommonDataKeys;
25
import com.intellij.openapi.actionSystem.DataContext;
26
import com.intellij.openapi.application.ReadAction;
27
import com.intellij.openapi.application.WriteAction;
28
import com.intellij.openapi.module.Module;
29
import com.intellij.openapi.module.ModuleManager;
30
import com.intellij.openapi.module.ModuleUtilCore;
31
import com.intellij.openapi.project.Project;
32
import com.intellij.openapi.project.RootsChangeRescanningInfo;
33
import com.intellij.openapi.projectRoots.ProjectJdkTable;
34
import com.intellij.openapi.projectRoots.Sdk;
35
import com.intellij.openapi.projectRoots.SdkModificator;
36
import com.intellij.openapi.projectRoots.impl.PerlModuleExtension;
37
import com.intellij.openapi.projectRoots.impl.PerlSdkTable;
38
import com.intellij.openapi.roots.ModuleRootEvent;
39
import com.intellij.openapi.roots.ModuleRootListener;
40
import com.intellij.openapi.roots.OrderRootType;
41
import com.intellij.openapi.roots.SyntheticLibrary;
42
import com.intellij.openapi.roots.ex.ProjectRootManagerEx;
43
import com.intellij.openapi.util.AtomicNotNullLazyValue;
44
import com.intellij.openapi.util.Disposer;
45
import com.intellij.openapi.util.NullableLazyValue;
46
import com.intellij.openapi.util.io.FileUtil;
47
import com.intellij.openapi.util.text.StringUtil;
48
import com.intellij.openapi.vfs.*;
49
import com.intellij.util.containers.FactoryMap;
50
import com.intellij.util.messages.MessageBusConnection;
51
import com.perl5.PerlBundle;
52
import com.perl5.lang.perl.idea.actions.PerlDumbAwareAction;
53
import com.perl5.lang.perl.idea.configuration.settings.PerlLocalSettings;
54
import com.perl5.lang.perl.idea.configuration.settings.sdk.Perl5SettingsConfigurable;
55
import com.perl5.lang.perl.idea.configuration.settings.sdk.PerlSdkLibrary;
56
import com.perl5.lang.perl.idea.modules.PerlLibrarySourceRootType;
57
import com.perl5.lang.perl.idea.modules.PerlSourceRootType;
58
import com.perl5.lang.perl.idea.sdk.PerlConfig;
59
import com.perl5.lang.perl.idea.sdk.PerlSdkType;
60
import org.jetbrains.annotations.Contract;
61
import org.jetbrains.annotations.NotNull;
62
import org.jetbrains.annotations.Nullable;
63

64
import java.io.File;
65
import java.util.*;
66

67
public class PerlProjectManager implements Disposable {
68
  private final @NotNull Project myProject;
69
  private final PerlLocalSettings myPerlSettings;
70
  private final Map<PerlSourceRootType, List<VirtualFile>> myModulesRootsProvider;
71
  private volatile NullableLazyValue<Sdk> mySdkProvider;
72
  private volatile AtomicNotNullLazyValue<Map<VirtualFile, PerlSourceRootType>> myAllModulesMapProvider;
73
  private volatile AtomicNotNullLazyValue<List<VirtualFile>> myExternalLibraryRootsProvider;
74
  private volatile AtomicNotNullLazyValue<List<VirtualFile>> mySdkLibraryRootsProvider;
75
  private volatile AtomicNotNullLazyValue<List<VirtualFile>> myLibraryRootsProvider;
76
  private volatile AtomicNotNullLazyValue<List<SyntheticLibrary>> myLibrariesProvider;
77

78
  public PerlProjectManager(@NotNull Project project) {
1✔
79
    myProject = project;
1✔
80
    myPerlSettings = PerlLocalSettings.getInstance(project);
1✔
81
    myModulesRootsProvider = FactoryMap.create(type -> type.getRoots(myProject));
1✔
82
    resetProjectCaches();
1✔
83
    MessageBusConnection connection = myProject.getMessageBus().connect(this);
1✔
84
    connection.subscribe(PerlSdkTable.PERL_TABLE_TOPIC, new ProjectJdkTable.Listener() {
1✔
85
      @Override
86
      public void jdkRemoved(@NotNull Sdk sdk) {
87
        if (StringUtil.equals(sdk.getName(), myPerlSettings.getPerlInterpreter())) {
1✔
88
          setProjectSdk(null);
1✔
89
        }
90
      }
1✔
91

92
      @Override
93
      public void jdkNameChanged(@NotNull Sdk jdk, @NotNull String previousName) {
UNCOV
94
        if (StringUtil.equals(myPerlSettings.getPerlInterpreter(), previousName)) {
×
UNCOV
95
          setProjectSdk(jdk);
×
96
        }
97
      }
×
98
    });
99
    connection.subscribe(ModuleRootListener.TOPIC, new ModuleRootListener() {
1✔
100
      @Override
101
      public void rootsChanged(@NotNull ModuleRootEvent event) {
102
        resetProjectCaches();
1✔
103
      }
1✔
104
    });
105
    VirtualFileListener listener = new VirtualFileListener() {
1✔
106
      @Override
107
      public void fileCreated(@NotNull VirtualFileEvent event) {
108
        resetProjectCaches();
1✔
109
      }
1✔
110

111
      @Override
112
      public void propertyChanged(@NotNull VirtualFilePropertyEvent event) {
113
        resetProjectCaches();
1✔
114
      }
1✔
115

116
      @Override
117
      public void fileDeleted(@NotNull VirtualFileEvent event) {
118
        resetProjectCaches();
1✔
119
      }
1✔
120

121
      @Override
122
      public void fileMoved(@NotNull VirtualFileMoveEvent event) {
UNCOV
123
        resetProjectCaches();
×
UNCOV
124
      }
×
125
    };
126
    LocalFileSystem.getInstance().addVirtualFileListener(listener);
1✔
127
    Disposer.register(this, () -> LocalFileSystem.getInstance().removeVirtualFileListener(listener));
1✔
128
  }
1✔
129

130
  @Override
131
  public void dispose() {
132
    // nothing to dispose
133
  }
1✔
134

135
  private void resetProjectCaches() {
136
    myModulesRootsProvider.clear();
1✔
137
    myAllModulesMapProvider = AtomicNotNullLazyValue.createValue(() -> {
1✔
UNCOV
138
      Map<VirtualFile, PerlSourceRootType> map = new HashMap<>();
×
UNCOV
139
      for (Module module : ModuleManager.getInstance(myProject).getModules()) {
×
140
        PerlModuleExtension.getInstance(module).getRoots().forEach(map::put);
×
141
      }
142
      return Collections.unmodifiableMap(map);
×
143
    });
144
    mySdkProvider = NullableLazyValue.atomicLazyNullable(() -> PerlSdkTable.getInstance().findJdk(myPerlSettings.getPerlInterpreter()));
1✔
145
    myExternalLibraryRootsProvider = AtomicNotNullLazyValue.createValue(() -> {
1✔
146
      var result = new ArrayList<VirtualFile>();
1✔
147

148
      var perlConfig = PerlConfig.from(getSdk(myProject));
1✔
149
      var versionString = perlConfig  == null ? null: perlConfig.getVersion();
1✔
150
      var archname = perlConfig == null ? null: perlConfig.getArchname();
1✔
151

152
      for (String externalPath : myPerlSettings.getExternalLibrariesPaths()) {
1✔
153
        VirtualFile externalLibDir = VfsUtil.findFileByIoFile(new File(externalPath), false);
1✔
154
        if (isAcceptableLibDir(externalLibDir)) {
1✔
155
          result.add(externalLibDir);
1✔
156

157
          var versionDir = versionString == null ? null : externalLibDir.findChild(versionString);
1✔
158
          if( isAcceptableLibDir(versionDir)){
1✔
159
            var versionArchDir = archname == null ? null: versionDir.findChild(archname);
1✔
160
            if (isAcceptableLibDir(versionArchDir)) {
1✔
161
              result.add(versionArchDir);
1✔
162
            }
163
            result.add(versionDir);
1✔
164
          }
165

166
          if (archname != null) {
1✔
167
            var archDir = externalLibDir.findChild(archname);
1✔
168
            if( isAcceptableLibDir(archDir)){
1✔
169
              result.add(archDir);
1✔
170
            }
171
          }
172
        }
173
      }
1✔
174
      return Collections.unmodifiableList(result);
1✔
175
    });
176
    mySdkLibraryRootsProvider = AtomicNotNullLazyValue.createValue(() -> {
1✔
177
      var result = new ArrayList<>(getExternalLibraryRoots());
1✔
178
      Sdk projectSdk = getProjectSdk();
1✔
179
      if (projectSdk != null) {
1✔
180
        result.addAll(Arrays.asList(projectSdk.getRootProvider().getFiles(OrderRootType.CLASSES)));
1✔
181
      }
182
      return Collections.unmodifiableList(result);
1✔
183
    });
184
    myLibraryRootsProvider = AtomicNotNullLazyValue.createValue(() -> {
1✔
185
      var result = new ArrayList<>(getModulesLibraryRoots());
1✔
186
      result.addAll(getProjectSdkLibraryRoots());
1✔
187
      return Collections.unmodifiableList(result);
1✔
188
    });
189
    myLibrariesProvider = AtomicNotNullLazyValue.createValue(() -> {
1✔
190
      var sdkLibs = getProjectSdkLibraryRoots();
1✔
191
      if (sdkLibs.isEmpty()) {
1✔
192
        return Collections.emptyList();
1✔
193
      }
194

195
      SyntheticLibrary sdkLibrary;
196
      Sdk sdk = getProjectSdk();
1✔
197
      if (sdk != null) {
1✔
198
        sdkLibrary = new PerlSdkLibrary(sdk, sdkLibs);
1✔
199
      }
200
      else {
201
        sdkLibrary = SyntheticLibrary.newImmutableLibrary(sdkLibs);
1✔
202
      }
203
      return Collections.singletonList(sdkLibrary);
1✔
204
    });
205
  }
1✔
206

207
  @Contract("null->false")
208
  private static boolean isAcceptableLibDir(@Nullable VirtualFile externalLibDir) {
209
    return externalLibDir != null && externalLibDir.isValid() && externalLibDir.isDirectory();
1✔
210
  }
211

212
  public Map<VirtualFile, PerlSourceRootType> getAllModulesRoots() {
UNCOV
213
    return myAllModulesMapProvider.getValue();
×
214
  }
215

216
  public List<VirtualFile> getExternalLibraryRoots() {
217
    return myExternalLibraryRootsProvider.getValue();
1✔
218
  }
219

220
  /**
221
   * @return list of in-project perl library roots, that should be included into modules lookup and {@code -I} argument when starting the
222
   * perl process
223
   */
224
  public List<VirtualFile> getModulesLibraryRoots() {
225
    return getModulesRootsOfType(PerlLibrarySourceRootType.INSTANCE);
1✔
226
  }
227

228
  /**
229
   * @return list of user-configured roots of {@code type}
230
   */
231
  public List<VirtualFile> getModulesRootsOfType(@NotNull PerlSourceRootType type) {
232
    return myModulesRootsProvider.get(type);
1✔
233
  }
234

235
  /**
236
   * @return list of library roots for the project, including:<ul>
237
   *   <li>default interpreter roots</li>
238
   *   <li>configured project external library roots</li>
239
   *   <li>in-project perl library roots, that should be included into modules lookup and {@code -I} argument when starting the process</li>
240
   * </ul>
241
   */
242
  public List<VirtualFile> getAllLibraryRoots() {
243
    return myLibraryRootsProvider.getValue();
1✔
244
  }
245

246
  /**
247
   * @return list of library roots including:<ul>
248
   *   <li>default interpreter roots</li>
249
   *   <li>configured project external library roots</li>
250
   * </ul>
251
   */
252
  public List<VirtualFile> getProjectSdkLibraryRoots() {
253
    return mySdkLibraryRootsProvider.getValue();
1✔
254
  }
255

256
  public List<SyntheticLibrary> getProjectLibraries() { return myLibrariesProvider.getValue();}
1✔
257

258
  public @Nullable Sdk getProjectSdk() {
259
    return mySdkProvider.getValue();
1✔
260
  }
261

262
  public void setProjectSdk(@Nullable Sdk sdk) {
263
    WriteAction.run(
1✔
264
      () -> ProjectRootManagerEx.getInstanceEx(myProject).makeRootsChange(
1✔
265
        () -> myPerlSettings.setPerlInterpreter(sdk == null ? null : sdk.getName()), RootsChangeRescanningInfo.TOTAL_RESCAN)
1✔
266
    );
267
  }
1✔
268

269
  public void addExternalLibrary(@NotNull VirtualFile root) {
270
    addExternalLibraries(Collections.singletonList(root));
1✔
271
  }
1✔
272

273
  public boolean isPerlEnabled() {
274
    return !myProject.isDefault() && getProjectSdk() != null;
1✔
275
  }
276

277
  public void setExternalLibraries(@NotNull List<? extends VirtualFile> roots) {
278
    WriteAction.run(() -> {
1✔
279
      myPerlSettings.setExternalLibrariesPaths(Collections.emptyList());
1✔
280
      addExternalLibraries(roots);
1✔
281
    });
1✔
282
  }
1✔
283

284
  public void addExternalLibraries(@NotNull List<? extends VirtualFile> roots) {
285
    WriteAction.run(
1✔
286
      () -> {
287
        List<String> paths = new ArrayList<>(myPerlSettings.getExternalLibrariesPaths());
1✔
288
        boolean doChange = false;
1✔
289
        for (VirtualFile root : roots) {
1✔
290
          if (!root.isValid() || !root.isDirectory()) {
1✔
UNCOV
291
            return;
×
292
          }
293

294
          String canonicalPath = root.getCanonicalPath();
1✔
295
          if (!paths.contains(canonicalPath)) {
1✔
296
            doChange = true;
1✔
297
            paths.add(canonicalPath);
1✔
298
          }
299
        }
1✔
300
        if (!doChange && !paths.isEmpty()) {
1✔
301
          return;
1✔
302
        }
303

304
        ProjectRootManagerEx.getInstanceEx(myProject).makeRootsChange(
1✔
305
          () -> myPerlSettings.setExternalLibrariesPaths(paths), RootsChangeRescanningInfo.RESCAN_DEPENDENCIES_IF_NEEDED);
1✔
306
      }
1✔
307
    );
308
  }
1✔
309

310
  public static PerlProjectManager getInstance(@NotNull Project project) {
311
    return project.getService(PerlProjectManager.class);
1✔
312
  }
313

314

315
  @Contract("null->null")
316
  public static @Nullable Sdk getSdk(@Nullable Module module) {
317
    return module == null ? null : getInstance(module.getProject()).getProjectSdk();
1✔
318
  }
319

320
  /**
321
   * @return sdk for project. If not configured - suggests to configure
322
   */
323
  public static Sdk getSdkWithNotification(@NotNull Project project) {
324
    Sdk sdk = getSdk(project);
1✔
325
    if (sdk != null) {
1✔
326
      return sdk;
1✔
327
    }
UNCOV
328
    showUnconfiguredInterpreterNotification(project);
×
UNCOV
329
    return null;
×
330
  }
331

332
  public static void showUnconfiguredInterpreterNotification(@NotNull Project project) {
UNCOV
333
    Notification notification = new Notification(
×
UNCOV
334
      PerlBundle.message("perl.select.sdk.notification"),
×
335
      PerlBundle.message("perl.select.sdk.notification.title"),
×
336
      PerlBundle.message("perl.select.sdk.notification.message"),
×
337
      NotificationType.ERROR
338
    );
UNCOV
339
    notification.addAction(new PerlDumbAwareAction(PerlBundle.message("perl.configure.interpreter.action")) {
×
340
      @Override
341
      public void actionPerformed(@NotNull AnActionEvent e) {
UNCOV
342
        Perl5SettingsConfigurable.open(project);
×
UNCOV
343
        notification.expire();
×
344
      }
×
345
    });
346
    Notifications.Bus.notify(notification, project);
×
UNCOV
347
  }
×
348

349
  @Contract("null->null")
350
  public static @Nullable Sdk getSdk(@Nullable Project project) {
351
    return project == null ? null : getInstance(project).getProjectSdk();
1✔
352
  }
353

354
  /**
355
   * Migration method for 2018.3. Updates sdk path to interpreter path, not bin dir
356
   *
357
   * @param sdk in question
358
   * @return path to interpreter
359
   */
360
  @Contract("null->null")
361
  public static @Nullable String getInterpreterPath(@Nullable Sdk sdk) {
362
    if (sdk == null) {
1✔
UNCOV
363
      return null;
×
364
    }
365
    String homePath = sdk.getHomePath();
1✔
366
    if (homePath != null && !StringUtil.contains(new File(homePath).getName(), "perl")) {
1✔
367
      homePath = FileUtil.join(homePath, PerlSdkType.INSTANCE.getPerlExecutableName());
1✔
368
      SdkModificator modificator = sdk.getSdkModificator();
1✔
369
      modificator.setHomePath(homePath);
1✔
370
      modificator.commitChanges();
1✔
371
    }
372
    return homePath;
1✔
373
  }
374

375
  public static @Nullable Sdk getSdk(@NotNull Project project, @Nullable VirtualFile virtualFile) {
376
    if (virtualFile == null) {
1✔
UNCOV
377
      return getSdk(project);
×
378
    }
379
    Module module = ReadAction.compute(() -> ModuleUtilCore.findModuleForFile(virtualFile, project));
1✔
380
    return module == null ? getSdk(project) : getSdk(module);
1✔
381
  }
382

383
  @Contract("null->false")
384
  public static boolean isPerlEnabled(@Nullable Project project) {
385
    return project != null && !project.isDisposed() && getInstance(project).isPerlEnabled();
1✔
386
  }
387

388
  public static boolean isPerlEnabled(@NotNull DataContext dataContext) {
389
    return isPerlEnabled(CommonDataKeys.PROJECT.getData(dataContext));
1✔
390
  }
391

392
  public static boolean isPerlEnabled(@Nullable Module module) {
393
    return module != null && isPerlEnabled(module.getProject());
1✔
394
  }
395

396
  @Contract("null->null")
397
  public static @Nullable Sdk getSdk(@Nullable AnActionEvent event) {
398
    return event == null ? null : getSdk(event.getProject());
1✔
399
  }
400
}
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