Skip to content

Commit 0a335bd

Browse files
committed
feat: add "Excluded path suggestion globs" preference option
1 parent 1c43965 commit 0a335bd

File tree

6 files changed

+161
-11
lines changed

6 files changed

+161
-11
lines changed

org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestMarkdown.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.nio.charset.StandardCharsets;
2020
import java.util.ArrayList;
21+
import java.util.Arrays;
2122
import java.util.Collections;
2223
import java.util.concurrent.atomic.AtomicReference;
2324
import java.util.stream.Collectors;
@@ -27,13 +28,17 @@
2728
import org.eclipse.core.resources.IResource;
2829
import org.eclipse.core.resources.ResourcesPlugin;
2930
import org.eclipse.core.runtime.CoreException;
31+
import org.eclipse.jface.text.IDocument;
32+
import org.eclipse.jface.text.contentassist.ICompletionProposal;
3033
import org.eclipse.lsp4e.LSPEclipseUtils;
3134
import org.eclipse.lsp4e.LanguageServerWrapper;
3235
import org.eclipse.lsp4e.LanguageServiceAccessor;
36+
import org.eclipse.lsp4e.operations.completion.LSContentAssistProcessor;
3337
import org.eclipse.ui.PlatformUI;
3438
import org.eclipse.ui.editors.text.TextEditor;
3539
import org.eclipse.ui.ide.IDE;
3640
import org.eclipse.ui.tests.harness.util.DisplayHelper;
41+
import org.eclipse.wildwebdeveloper.Activator;
3742
import org.junit.jupiter.api.Test;
3843
import org.junit.jupiter.api.extension.ExtendWith;
3944

@@ -123,4 +128,61 @@ void diagnosticsCoverTypicalMarkdownIssues() throws Exception {
123128

124129
assertTrue(markerTests.isEmpty(), "The following markers were not found: " + markerTests);
125130
}
131+
132+
@Test
133+
void workspaceHeaderCompletionsRespectExcludeGlobs() throws Exception {
134+
var project = ResourcesPlugin.getWorkspace().getRoot().getProject(getClass().getName() + ".hdr" + System.nanoTime());
135+
project.create(null);
136+
project.open(null);
137+
138+
// Configure exclusion: exclude docs/generated/** from workspace header completions
139+
Activator.getDefault().getPreferenceStore().setValue("markdown.suggest.paths.excludeGlobs", "docs/generated/**");
140+
141+
// Create markdown files with unique headers
142+
// Ensure folders exist
143+
var docsFolder = project.getFolder("docs");
144+
if (!docsFolder.exists())
145+
docsFolder.create(true, true, null);
146+
var genFolder = docsFolder.getFolder("generated");
147+
if (!genFolder.exists())
148+
genFolder.create(true, true, null);
149+
150+
IFile excluded = project.getFile("docs/generated/excluded.md");
151+
excluded.create("# Excluded Only\n".getBytes(StandardCharsets.UTF_8), true, false, null);
152+
153+
IFile included = project.getFile("docs/included.md");
154+
included.create("# Included Only\n".getBytes(StandardCharsets.UTF_8), true, false, null);
155+
156+
// File where we'll trigger completions (double hash to respect default preference)
157+
IFile index = project.getFile("index.md");
158+
index.create("[](##)\n".getBytes(StandardCharsets.UTF_8), true, false, null);
159+
160+
var editor = (TextEditor) IDE.openEditor(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(), index);
161+
var display = editor.getSite().getShell().getDisplay();
162+
IDocument document = editor.getDocumentProvider().getDocument(editor.getEditorInput());
163+
164+
// Ensure Markdown Language Server is started and connected
165+
var markdownLS = new AtomicReference<LanguageServerWrapper>();
166+
assertTrue(DisplayHelper.waitForCondition(display, 10_000, () -> {
167+
markdownLS.set(LanguageServiceAccessor.getStartedWrappers(document, null, false).stream() //
168+
.filter(w -> "org.eclipse.wildwebdeveloper.markdown".equals(w.serverDefinition.id)) //
169+
.findFirst().orElse(null));
170+
return markdownLS.get() != null //
171+
&& markdownLS.get().isActive() //
172+
&& markdownLS.get().isConnectedTo(LSPEclipseUtils.toUri(document));
173+
}), "Markdown LS did not start");
174+
175+
// Trigger content assist at the end of '##'
176+
int offset = document.get().indexOf("##") + 2;
177+
var cap = new LSContentAssistProcessor();
178+
179+
assertTrue(DisplayHelper.waitForCondition(display, 15_000, () -> {
180+
ICompletionProposal[] proposals = cap.computeCompletionProposals(Utils.getViewer(editor), offset);
181+
if (proposals == null || proposals.length == 0)
182+
return false;
183+
boolean hasIncluded = Arrays.stream(proposals).anyMatch(p -> "#included-only".equals(p.getDisplayString()));
184+
boolean hasExcluded = Arrays.stream(proposals).anyMatch(p -> "#excluded-only".equals(p.getDisplayString()));
185+
return hasIncluded && !hasExcluded;
186+
}), "Workspace header completions did not respect exclude globs");
187+
}
126188
}

org.eclipse.wildwebdeveloper/META-INF/MANIFEST.MF

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ Require-Bundle: org.eclipse.ui,
3535
Bundle-RequiredExecutionEnvironment: JavaSE-21
3636
Bundle-ActivationPolicy: lazy
3737
Eclipse-BundleShape: dir
38-
Export-Package: org.eclipse.wildwebdeveloper.debug;x-internal:=true,
38+
Export-Package: org.eclipse.wildwebdeveloper;x-friends:="org.eclipse.wildwebdeveloper.tests",
39+
org.eclipse.wildwebdeveloper.debug;x-internal:=true,
3940
org.eclipse.wildwebdeveloper.debug.node;x-internal:=true,
4041
org.eclipse.wildwebdeveloper.debug.npm;x-internal:=true,
4142
org.eclipse.wildwebdeveloper.markdown;x-friends:="org.eclipse.wildwebdeveloper.tests"

org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/markdown/MarkdownLanguageClient.java

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818
import java.net.URISyntaxException;
1919
import java.net.URL;
2020
import java.nio.charset.StandardCharsets;
21+
import java.nio.file.FileSystems;
2122
import java.nio.file.Files;
2223
import java.nio.file.Path;
24+
import java.nio.file.PathMatcher;
2325
import java.nio.file.Paths;
2426
import java.util.ArrayList;
25-
import java.util.Collections;
2627
import java.util.HashMap;
2728
import java.util.List;
2829
import java.util.Map;
@@ -38,6 +39,7 @@
3839
import org.eclipse.core.resources.ResourcesPlugin;
3940
import org.eclipse.core.runtime.FileLocator;
4041
import org.eclipse.core.runtime.ILog;
42+
import org.eclipse.core.runtime.IPath;
4143
import org.eclipse.lsp4e.LSPEclipseUtils;
4244
import org.eclipse.lsp4e.client.DefaultLanguageClient;
4345
import org.eclipse.lsp4j.ConfigurationItem;
@@ -212,7 +214,7 @@ public CompletableFuture<List<Map<String, Object>>> parseMarkdown(final Map<Stri
212214
}
213215
}
214216
if (!hasText && uriPath == null) {
215-
return Collections.emptyList();
217+
return List.of();
216218
}
217219
final String argPath;
218220
if (hasText) {
@@ -229,7 +231,7 @@ public CompletableFuture<List<Map<String, Object>>> parseMarkdown(final Map<Stri
229231
if (exit != 0) {
230232
final var err = new String(proc.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);
231233
ILog.get().warn("markdown-it parser failed (" + exit + "): " + err, null);
232-
return Collections.emptyList();
234+
return List.of();
233235
}
234236
final var gson = new Gson();
235237
final List<Map<String, Object>> tokens = gson.fromJson(out, List.class);
@@ -242,7 +244,7 @@ public CompletableFuture<List<Map<String, Object>>> parseMarkdown(final Map<Stri
242244
}
243245
} catch (final Exception ignore) {
244246
}
245-
return tokens != null ? tokens : Collections.emptyList();
247+
return tokens != null ? tokens : List.of();
246248
} catch (final InterruptedException ex) {
247249
ILog.get().warn(ex.getMessage(), ex);
248250
/* Clean up whatever needs to be handled before interrupting */
@@ -256,7 +258,7 @@ public CompletableFuture<List<Map<String, Object>>> parseMarkdown(final Map<Stri
256258
} catch (final Exception ignore) {
257259
}
258260
}
259-
return Collections.emptyList();
261+
return List.of();
260262
});
261263
}
262264

@@ -270,6 +272,9 @@ public CompletableFuture<List<Map<String, Object>>> parseMarkdown(final Map<Stri
270272
public CompletableFuture<List<String>> findMarkdownFilesInWorkspace(final Object unused) {
271273
return CompletableFuture.supplyAsync(() -> {
272274
final var uris = new ArrayList<String>();
275+
// Compile exclude globs from preferences once per request
276+
final String[] excludeGlobs = MarkdownPreferences.getSuggestPathsExcludeGlobs();
277+
final List<PathMatcher> excludeMatchers = compileGlobMatchers(excludeGlobs);
273278
try {
274279
final var roots = MarkdownLanguageServer.getServerRoots();
275280
if (roots != null && !roots.isEmpty()) {
@@ -285,7 +290,8 @@ public CompletableFuture<List<String>> findMarkdownFilesInWorkspace(final Object
285290
return false;
286291
if (res.getType() == IResource.FILE) {
287292
final String name = res.getName().toLowerCase();
288-
if (name.endsWith(".md") || name.endsWith(".markdown") || name.endsWith(".mdown")) {
293+
if ((name.endsWith(".md") || name.endsWith(".markdown") || name.endsWith(".mdown"))
294+
&& !isExcludedByGlobs(res, excludeMatchers)) {
289295
uris.add(normalizeFileUriForLanguageServer(res.getLocationURI()));
290296
}
291297
return false; // no children
@@ -303,7 +309,8 @@ public CompletableFuture<List<String>> findMarkdownFilesInWorkspace(final Object
303309
return false;
304310
if (res.getType() == IResource.FILE) {
305311
final String name = res.getName().toLowerCase();
306-
if (name.endsWith(".md") || name.endsWith(".markdown") || name.endsWith(".mdown")) {
312+
if ((name.endsWith(".md") || name.endsWith(".markdown") || name.endsWith(".mdown"))
313+
&& !isExcludedByGlobs(res, excludeMatchers)) {
307314
uris.add(normalizeFileUriForLanguageServer(res.getLocationURI()));
308315
}
309316
return false; // no children
@@ -318,6 +325,48 @@ public CompletableFuture<List<String>> findMarkdownFilesInWorkspace(final Object
318325
});
319326
}
320327

328+
private static List<PathMatcher> compileGlobMatchers(String... globs) {
329+
if (globs == null || globs.length == 0)
330+
return List.of();
331+
332+
var fs = FileSystems.getDefault();
333+
var matchers = new ArrayList<PathMatcher>();
334+
335+
for (String glob : globs) {
336+
if (glob == null || (glob = glob.trim()).isEmpty())
337+
continue;
338+
339+
// If pattern starts with "**/", also add a root-level variant without it.
340+
// This makes "**/node_modules/**" also match "node_modules/**".
341+
List<String> patterns = glob.startsWith("**/") && glob.length() > 3
342+
? List.of(glob, glob.substring(3))
343+
: List.of(glob);
344+
345+
for (String pattern : patterns) {
346+
try {
347+
matchers.add(fs.getPathMatcher("glob:" + pattern));
348+
} catch (Exception ex) {
349+
ILog.get().warn(ex.getMessage(), ex);
350+
}
351+
}
352+
}
353+
return matchers;
354+
}
355+
356+
private static boolean isExcludedByGlobs(final IResource res, final List<PathMatcher> matchers) {
357+
if (matchers == null || matchers.isEmpty())
358+
return false;
359+
final IPath pr = res.getProjectRelativePath();
360+
if (pr == null)
361+
return false;
362+
final Path p = pr.toPath();
363+
for (final PathMatcher m : matchers) {
364+
if (m.matches(p))
365+
return true;
366+
}
367+
return false;
368+
}
369+
321370
/**
322371
* <pre>
323372
* Request: { uri: string }
@@ -415,10 +464,10 @@ public CompletableFuture<Void> fsWatcherCreate(final Map<String, Object> params)
415464
@SuppressWarnings("unchecked")
416465
final Map<String, Object> options = params.get("options") instanceof Map //
417466
? (Map<String, Object>) params.get("options")
418-
: Collections.emptyMap();
419-
final var watcher = new Watcher((MarkdownLanguageServerAPI) getLanguageServer(), id, path,
467+
: Map.of();
468+
final var watcher = new Watcher((MarkdownLanguageServerAPI) getLanguageServer(), id, path, //
420469
Boolean.TRUE.equals(options.get("ignoreCreate")), //
421-
Boolean.TRUE.equals(options.get("ignoreChange")),
470+
Boolean.TRUE.equals(options.get("ignoreChange")), //
422471
Boolean.TRUE.equals(options.get("ignoreDelete")));
423472
ResourcesPlugin.getWorkspace().addResourceChangeListener(watcher, IResourceChangeEvent.POST_CHANGE);
424473
watchersById.put(Integer.valueOf(id), watcher);

org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/markdown/ui/preferences/MarkdownPreferenceInitializer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public void initializeDefaultPreferences() {
4141
store.setDefault(MD_PREFERRED_MD_PATH_EXTENSION_STYLE, PreferredMdPathExtensionStyle.auto.value);
4242
store.setDefault(MD_SUGGEST_PATHS_ENABLED, true);
4343
store.setDefault(MD_SUGGEST_PATHS_INCLUDE_WKS_HEADER_COMPLETIONS, IncludeWorkspaceHeaderCompletions.onDoubleHash.value);
44+
store.setDefault(MD_SUGGEST_PATHS_EXCLUDE_GLOBS, "**/node_modules/**");
4445

4546
/*
4647
* Validation

org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/markdown/ui/preferences/MarkdownPreferencePage.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,22 @@ protected void createFieldEditors() {
108108
toLabelValueArray(IncludeWorkspaceHeaderCompletions.class),
109109
suggestionsGroup));
110110

111+
final var excludeGlobs = new StringFieldEditor(
112+
MD_SUGGEST_PATHS_EXCLUDE_GLOBS,
113+
"Excluded path suggestion globs (comma-separated)",
114+
suggestionsGroup);
115+
addField(excludeGlobs);
116+
final String excludeTooltip = """
117+
Glob patterns to exclude Markdown files from path suggestions.
118+
Matched against project-relative paths, for example:
119+
• **/node_modules/**
120+
• docs/generated/**
121+
• **/drafts/**
122+
• **/*.tmp.md
123+
""";
124+
excludeGlobs.getLabelControl(suggestionsGroup).setToolTipText(excludeTooltip);
125+
excludeGlobs.getTextControl(suggestionsGroup).setToolTipText(excludeTooltip);
126+
111127
// Validation
112128
validateEnabledEditor = new BooleanFieldEditor(MD_VALIDATE_ENABLED, "Enable validation", pageParent);
113129
addField(validateEnabledEditor);

org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/markdown/ui/preferences/MarkdownPreferences.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ private ValidateEnabledForFragmentLinks(final String label) {
113113
static final String MD_SUGGEST_PATHS_INCLUDE_WKS_HEADER_COMPLETIONS = MD_SECTION
114114
+ ".suggest.paths.includeWorkspaceHeaderCompletions"; // IncludeWorkspaceHeaderCompletions
115115

116+
117+
// Note: MD_SUGGEST_PATHS_EXCLUDE_GLOBS is a client-only preference used for
118+
// filtering path suggestions on the Eclipse side and is not sent to the server.
119+
static final String MD_SUGGEST_PATHS_EXCLUDE_GLOBS = MD_SECTION + ".suggest.paths.excludeGlobs"; // comma list
120+
116121
/*
117122
* Validation
118123
*/
@@ -166,6 +171,22 @@ public static Settings getGlobalSettings() {
166171
return settings;
167172
}
168173

174+
/**
175+
* Returns comma-separated glob patterns from preferences for excluding files from
176+
* Markdown path suggestions. Empty or blank entries are filtered out.
177+
*/
178+
public static String[] getSuggestPathsExcludeGlobs() {
179+
final IPreferenceStore store = Activator.getDefault().getPreferenceStore();
180+
final String raw = store.getString(MD_SUGGEST_PATHS_EXCLUDE_GLOBS);
181+
if (raw == null || raw.trim().isEmpty()) {
182+
return new String[0];
183+
}
184+
return Arrays.stream(raw.split(","))
185+
.map(String::trim)
186+
.filter(s -> !s.isEmpty())
187+
.toArray(String[]::new);
188+
}
189+
169190
public static boolean isMatchMarkdownSection(final String section) {
170191
return isMatchSection(section, MD_SECTION);
171192
}

0 commit comments

Comments
 (0)