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

devonfw / IDEasy / 30557576551

30 Jul 2026 03:37PM UTC coverage: 72.642% (+0.05%) from 72.593%
30557576551

Pull #2237

github

web-flow
Merge f9a4ab35f into ec264ffca
Pull Request #2237: #2180: Add *_PLUGINS_EXTRA variable to install extra IDE plugins per user

5011 of 7621 branches covered (65.75%)

Branch coverage included in aggregate %.

12891 of 17023 relevant lines covered (75.73%)

3.21 hits per line

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

86.73
cli/src/main/java/com/devonfw/tools/ide/tool/plugin/PluginBasedCommandlet.java
1
package com.devonfw.tools.ide.tool.plugin;
2

3
import java.nio.file.Files;
4
import java.nio.file.Path;
5
import java.util.Collection;
6
import java.util.List;
7
import java.util.Set;
8

9
import org.slf4j.Logger;
10
import org.slf4j.LoggerFactory;
11

12
import com.devonfw.tools.ide.cli.CliException;
13
import com.devonfw.tools.ide.common.Tag;
14
import com.devonfw.tools.ide.context.IdeContext;
15
import com.devonfw.tools.ide.environment.EnvironmentVariables;
16
import com.devonfw.tools.ide.environment.VariableLine;
17
import com.devonfw.tools.ide.io.FileAccess;
18
import com.devonfw.tools.ide.process.ProcessContext;
19
import com.devonfw.tools.ide.process.ProcessErrorHandling;
20
import com.devonfw.tools.ide.property.FlagProperty;
21
import com.devonfw.tools.ide.step.Step;
22
import com.devonfw.tools.ide.tool.LocalToolCommandlet;
23
import com.devonfw.tools.ide.tool.ToolInstallRequest;
24
import com.devonfw.tools.ide.tool.ide.IdeToolCommandlet;
25

26
/**
27
 * Base class for {@link LocalToolCommandlet}s that support plugins. It can automatically install configured plugins for the tool managed by this commandlet.
28
 */
29
public abstract class PluginBasedCommandlet extends LocalToolCommandlet {
30

31
  private static final Logger LOG = LoggerFactory.getLogger(PluginBasedCommandlet.class);
4✔
32

33
  /** Suffix of the tool-specific variable listing additional plugins to activate (e.g. {@code VSCODE_PLUGINS_EXTRA}). */
34
  public static final String VARIABLE_SUFFIX_PLUGINS_EXTRA = "_PLUGINS_EXTRA";
35

36
  private ToolPlugins plugins;
37

38
  /** {@link FlagProperty} to force the reset and reinstallation of plugins as configured in the project settings. */
39
  public FlagProperty forcePluginReinstall;
40

41
  /**
42
   * The constructor.
43
   *
44
   * @param context the {@link IdeContext}.
45
   * @param tool the {@link #getName() tool name}.
46
   * @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of} method.
47
   */
48
  public PluginBasedCommandlet(IdeContext context, String tool, Set<Tag> tags) {
49

50
    super(context, tool, tags);
5✔
51
  }
1✔
52

53
  @Override
54
  protected void initProperties() {
55
    this.forcePluginReinstall = add(new FlagProperty("--force-plugin-reinstall"));
9✔
56
    super.initProperties();
2✔
57
  }
1✔
58

59
  /**
60
   * @return the {@link ToolPlugins} of this {@link PluginBasedCommandlet}.
61
   */
62
  public ToolPlugins getPlugins() {
63

64
    if (this.plugins == null) {
3✔
65
      ToolPlugins toolPlugins = new ToolPlugins();
4✔
66

67
      // Load project-specific plugins
68
      Path pluginsPath = getPluginsConfigPath();
3✔
69
      loadPluginsFromDirectory(toolPlugins, pluginsPath);
4✔
70

71
      // Load user-specific plugins, this is done after loading the project-specific plugins so the user can potentially
72
      // override plugins (e.g. change active flag).
73
      Path userPluginsPath = getUserHomePluginsConfigPath();
3✔
74
      loadPluginsFromDirectory(toolPlugins, userPluginsPath);
4✔
75

76
      activateExtraPlugins(toolPlugins);
3✔
77

78
      this.plugins = toolPlugins;
3✔
79
    }
80

81
    return this.plugins;
3✔
82
  }
83

84
  private void loadPluginsFromDirectory(ToolPlugins map, Path pluginsPath) {
85

86
    List<Path> children = this.context.getFileAccess()
5✔
87
        .listChildren(pluginsPath, p -> p.getFileName().toString().endsWith(IdeContext.EXT_PROPERTIES));
8✔
88
    for (Path child : children) {
10✔
89
      ToolPluginDescriptor descriptor = ToolPluginDescriptor.of(child, this.context, isPluginUrlNeeded());
7✔
90
      map.add(descriptor);
3✔
91
    }
1✔
92
  }
1✔
93

94
  /**
95
   * @return {@code true} if {@link ToolPluginDescriptor#url() plugin URL} property is needed, {@code false} otherwise.
96
   */
97
  protected boolean isPluginUrlNeeded() {
98

99
    return false;
2✔
100
  }
101

102
  /**
103
   * @return the {@link Path} to the folder with the plugin configuration files inside the settings.
104
   */
105
  protected Path getPluginsConfigPath() {
106

107
    return this.context.getSettingsPath().resolve(this.tool).resolve(IdeContext.FOLDER_PLUGINS);
9✔
108
  }
109

110
  private Path getUserHomePluginsConfigPath() {
111

112
    return this.context.getUserHomeIde().resolve(IdeContext.FOLDER_SETTINGS).resolve(this.tool).resolve(IdeContext.FOLDER_PLUGINS);
11✔
113
  }
114

115
  /**
116
   * @return the {@link Path} where the plugins of this {@link IdeToolCommandlet} shall be installed.
117
   */
118
  public Path getPluginsInstallationPath() {
119

120
    return this.context.getPluginsPath().resolve(this.tool);
7✔
121
  }
122

123
  @Override
124
  protected void postInstall(ToolInstallRequest request) {
125

126
    super.postInstall(request);
3✔
127
    Path pluginsInstallationPath = getPluginsInstallationPath();
3✔
128

129
    if (!request.isAlreadyInstalled() || this.forcePluginReinstall.isTrue()) {
7!
130
      LOG.info("Resetting all installed plugins...");
3✔
131
      deleteAllPlugins(pluginsInstallationPath);
3✔
132
    }
133
    this.context.getFileAccess().mkdirs(pluginsInstallationPath);
5✔
134
    installPlugins(request.getProcessContext());
4✔
135
  }
1✔
136

137
  /**
138
   * Deletes all installed plugins for this {@link IdeToolCommandlet} by deleting the plugins installation folder and all plugin marker files.
139
   *
140
   * @param pluginsInstallationPath the {@link Path} to the plugins installation folder.
141
   */
142
  private void deleteAllPlugins(Path pluginsInstallationPath) {
143

144
    FileAccess fileAccess = this.context.getFileAccess();
4✔
145
    fileAccess.delete(pluginsInstallationPath);
3✔
146
    List<Path> markerFiles = fileAccess.listChildren(this.context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE), Files::isRegularFile);
14✔
147
    for (Path path : markerFiles) {
10✔
148
      if (path.getFileName().toString().startsWith("plugin." + getName())) {
8!
149
        fileAccess.delete(path);
3✔
150
        LOG.debug("Plugin marker file {} got deleted.", path);
4✔
151
      }
152
    }
1✔
153
  }
1✔
154

155
  private void installPlugins(ProcessContext pc) {
156
    installPlugins(getPlugins().getPlugins(), pc);
6✔
157
  }
1✔
158

159
  /**
160
   * Method to install active plugins or to handle install for inactive plugins
161
   *
162
   * @param plugins as {@link Collection} of plugins to install.
163
   * @param pc the {@link ProcessContext} to use.
164
   */
165
  protected void installPlugins(Collection<ToolPluginDescriptor> plugins, ProcessContext pc) {
166
    long currentPluginIndex = 1;
2✔
167
    long totalActivePlugins = plugins.stream().filter(ToolPluginDescriptor::active).count();
6✔
168
    for (ToolPluginDescriptor plugin : plugins) {
10✔
169
      Path pluginMarkerFile = retrievePluginMarkerFilePath(plugin);
4✔
170
      boolean pluginMarkerFileExists = pluginMarkerFile != null && Files.exists(pluginMarkerFile);
11!
171
      if (pluginMarkerFileExists) {
2✔
172
        LOG.debug("Markerfile for IDE {} and plugin '{}' already exists.", getName(), plugin.name());
7✔
173
      }
174
      if (plugin.active()) {
3✔
175
        if (this.context.isForcePlugins() || !pluginMarkerFileExists) {
6✔
176
          String progressMarker = " (" + currentPluginIndex + "/" + totalActivePlugins + ")";
4✔
177
          Step step = this.context.newStep("Install plugin " + plugin.name() + progressMarker);
8✔
178
          step.run(() -> doInstallPluginStep(plugin, step, pc));
14✔
179
        } else {
1✔
180
          LOG.debug("Skipping installation of plugin '{}' due to existing marker file: {}", plugin.name(), pluginMarkerFile);
6✔
181
        }
182
        currentPluginIndex++;
5✔
183
      } else {
184
        if (!pluginMarkerFileExists) {
2!
185
          handleInstallForInactivePlugin(plugin);
3✔
186
        }
187
      }
188
    }
1✔
189
  }
1✔
190

191
  private void doInstallPluginStep(ToolPluginDescriptor plugin, Step step, ProcessContext pc) {
192
    boolean result = installPlugin(plugin, step, pc);
6✔
193
    if (result) {
2!
194
      createPluginMarkerFile(plugin);
3✔
195
    }
196
  }
1✔
197

198
  /**
199
   * @param plugin the {@link ToolPluginDescriptor plugin} to search for.
200
   * @return Path to the plugin marker file.
201
   */
202
  public Path retrievePluginMarkerFilePath(ToolPluginDescriptor plugin) {
203
    if (this.context.getIdeHome() != null) {
4!
204
      String markerFileName = "plugin" + "." + getName() + "." + getInstalledEdition() + "." + plugin.name();
8✔
205
      String version = plugin.version();
3✔
206
      if ((version != null) && !version.isBlank()) {
5!
207
        markerFileName = markerFileName + ".version-" + normalizeMarkerFileSegment(version);
6✔
208
      }
209
      return this.context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve(markerFileName);
8✔
210
    }
211
    return null;
×
212
  }
213

214
  private String normalizeMarkerFileSegment(String value) {
215
    // replace all characters that are not allowed in filenames with "_"
216
    return value.replaceAll("[^A-Za-z0-9._-]", "_");
5✔
217
  }
218

219
  /**
220
   * Creates a marker file for a plugin in $IDE_HOME/.ide/plugin.«ide».«plugin-name»
221
   *
222
   * @param plugin the {@link ToolPluginDescriptor plugin} for which the marker file should be created.
223
   */
224
  public void createPluginMarkerFile(ToolPluginDescriptor plugin) {
225
    Path pluginMarkerFilePath = retrievePluginMarkerFilePath(plugin);
4✔
226
    if (pluginMarkerFilePath != null) {
2!
227
      FileAccess fileAccess = this.context.getFileAccess();
4✔
228
      fileAccess.mkdirs(pluginMarkerFilePath.getParent());
4✔
229
      deleteExistingPluginMarkerFiles(fileAccess, plugin, pluginMarkerFilePath);
5✔
230
      fileAccess.touch(pluginMarkerFilePath);
3✔
231
    }
232
  }
1✔
233

234
  private void deleteExistingPluginMarkerFiles(FileAccess fileAccess, ToolPluginDescriptor plugin, Path currentMarkerFilePath) {
235

236
    String markerFilePrefix = "plugin" + "." + getName() + "." + getInstalledEdition() + "." + plugin.name();
8✔
237
    List<Path> markerFiles = fileAccess.listChildren(currentMarkerFilePath.getParent(),
7✔
238
        p -> {
239
          String fileName = p.getFileName().toString();
4✔
240
          return Files.isRegularFile(p) && (fileName.equals(markerFilePrefix) || fileName.startsWith(markerFilePrefix + ".version-"));
18!
241
        });
242
    for (Path markerFile : markerFiles) {
10✔
243
      if (!markerFile.equals(currentMarkerFilePath)) {
4✔
244
        fileAccess.delete(markerFile);
3✔
245
        LOG.debug("Deleted stale plugin marker file {} before creating {}.", markerFile, currentMarkerFilePath);
5✔
246
      }
247
    }
1✔
248
  }
1✔
249

250
  /**
251
   * @param plugin the {@link ToolPluginDescriptor} to install.
252
   * @param step the {@link Step} for the plugin installation.
253
   * @param pc the {@link ProcessContext} to use.
254
   * @return boolean true if the installation of the plugin succeeded, false if not.
255
   */
256
  public abstract boolean installPlugin(ToolPluginDescriptor plugin, Step step, ProcessContext pc);
257

258
  /**
259
   * @param plugin the {@link ToolPluginDescriptor} to install.
260
   * @param step the {@link Step} for the plugin installation.
261
   */
262
  public void installPlugin(ToolPluginDescriptor plugin, final Step step) {
263
    ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI);
6✔
264
    ToolInstallRequest request = new ToolInstallRequest(true);
5✔
265
    request.setProcessContext(pc);
3✔
266
    install(request);
4✔
267
    installPlugin(plugin, step, pc);
6✔
268
  }
1✔
269

270
  /**
271
   * @param plugin the {@link ToolPluginDescriptor} to uninstall.
272
   */
273
  public void uninstallPlugin(ToolPluginDescriptor plugin) {
274

275
    boolean error = false;
2✔
276
    Path pluginsPath = getPluginsInstallationPath();
3✔
277
    if (!Files.isDirectory(pluginsPath)) {
5!
278
      LOG.debug("Omitting to uninstall plugin {} ({}) as plugins folder does not exist at {}",
×
279
          plugin.name(), plugin.id(), pluginsPath);
×
280
      error = true;
×
281
    }
282
    FileAccess fileAccess = this.context.getFileAccess();
4✔
283
    Path match = fileAccess.findFirst(pluginsPath, p -> p.getFileName().toString().startsWith(plugin.id()), false);
7✔
284
    if (match == null) {
2!
285
      LOG.debug("Omitting to uninstall plugin {} ({}) as plugins folder does not contain a match at {}",
8✔
286
          plugin.name(), plugin.id(), pluginsPath);
11✔
287
      error = true;
2✔
288
    }
289
    if (error) {
2!
290
      LOG.error("Could not uninstall plugin {} because we could not find an installation", plugin);
5✔
291
    } else {
292
      fileAccess.delete(match);
×
293
      LOG.info("Successfully uninstalled plugin {}", plugin);
×
294
    }
295
  }
1✔
296

297
  /**
298
   * @param key the filename of the properties file configuring the requested plugin (typically excluding the ".properties" extension).
299
   * @return the {@link ToolPluginDescriptor} for the given {@code key}.
300
   */
301
  public ToolPluginDescriptor getPlugin(String key) {
302

303
    if (key == null) {
2!
304
      return null;
×
305
    }
306
    if (key.endsWith(IdeContext.EXT_PROPERTIES)) {
4!
307
      key = key.substring(0, key.length() - IdeContext.EXT_PROPERTIES.length());
×
308
    }
309

310
    ToolPlugins toolPlugins = getPlugins();
3✔
311
    ToolPluginDescriptor pluginDescriptor = toolPlugins.getByName(key);
4✔
312
    if (pluginDescriptor == null) {
2!
313
      throw new CliException(
×
314
          "Could not find plugin " + key + " at " + getPluginsConfigPath().resolve(key) + ".properties");
×
315
    }
316
    return pluginDescriptor;
2✔
317
  }
318

319
  /**
320
   * @param plugin the in{@link ToolPluginDescriptor#active() active} {@link ToolPluginDescriptor} that is skipped for regular plugin installation.
321
   */
322
  protected void handleInstallForInactivePlugin(ToolPluginDescriptor plugin) {
323

324
    LOG.debug("Omitting installation of inactive plugin {} ({}).", plugin.name(), plugin.id());
7✔
325
  }
1✔
326

327
  /**
328
   * Activates the plugins configured in the tool-specific {@code «TOOL»}{@value #VARIABLE_SUFFIX_PLUGINS_EXTRA} variable (e.g.
329
   * {@code VSCODE_PLUGINS_EXTRA=copilot,docker}). This allows a user to permanently opt-in to plugins that are not {@link ToolPluginDescriptor#active() active}
330
   * in the project settings, without modifying the shared settings and without losing them when plugins are purged and reinstalled on IDE upgrade. Values refer
331
   * to the {@link ToolPluginDescriptor#name() name} of the plugin (the filename of its {@code .properties} file) and not to the
332
   * {@link ToolPluginDescriptor#id() id}. Names that do not resolve to a configured plugin are logged as a warning and skipped so that a single stale entry
333
   * cannot break the entire installation.
334
   *
335
   * @param toolPlugins the {@link ToolPlugins} to modify.
336
   */
337
  private void activateExtraPlugins(ToolPlugins toolPlugins) {
338

339
    String variable = EnvironmentVariables.getToolVariablePrefix(this.tool) + VARIABLE_SUFFIX_PLUGINS_EXTRA;
5✔
340
    String value = this.context.getVariables().get(variable);
6✔
341
    if ((value == null) || value.isBlank()) {
5!
342
      return;
1✔
343
    }
344
    for (String entry : VariableLine.parseArray(value)) {
11✔
345
      String name = entry;
2✔
346
      if (name.endsWith(IdeContext.EXT_PROPERTIES)) {
4!
347
        name = name.substring(0, name.length() - IdeContext.EXT_PROPERTIES.length());
×
348
      }
349
      if (toolPlugins.activate(name) == null) {
4✔
350
        LOG.warn("Undefined plugin '{}' configured in variable {} - no file {}{} found in {} or {}.", name, variable, name, IdeContext.EXT_PROPERTIES,
24✔
351
            getPluginsConfigPath(), getUserHomePluginsConfigPath());
7✔
352
      }
353
    }
1✔
354
  }
1✔
355
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc