@@ -26,6 +26,23 @@ const MATCH_OPTIONS: micromatch.Options = { dot: true, nocase: true };
2626
2727const asPrefix = ( path : string ) => ( path . endsWith ( '/' ) ? path : path + '/' ) ;
2828
29+ /** With nested roots, the innermost one owns the path. */
30+ function findOwner < R extends { prefix : string } > (
31+ roots : R [ ] ,
32+ path : string
33+ ) : R | undefined {
34+ let owner : R | undefined ;
35+ for ( const root of roots ) {
36+ if (
37+ path . startsWith ( root . prefix ) &&
38+ ( ! owner || root . prefix . length > owner . prefix . length )
39+ ) {
40+ owner = root ;
41+ }
42+ }
43+ return owner ;
44+ }
45+
2946/**
3047 * An {@link IMatcher} that answers from the include/exclude globs directly.
3148 *
@@ -55,16 +72,7 @@ export class GlobMatcher implements IMatcher {
5572 }
5673
5774 isMatch ( uri : URI ) : boolean {
58- // With nested roots, the innermost one owns the file.
59- let owner : ( typeof this . roots ) [ number ] | undefined ;
60- for ( const root of this . roots ) {
61- if (
62- uri . path . startsWith ( root . prefix ) &&
63- ( ! owner || root . prefix . length > owner . prefix . length )
64- ) {
65- owner = root ;
66- }
67- }
75+ const owner = findOwner ( this . roots , uri . path ) ;
6876 if ( ! owner || owner . include . length === 0 ) {
6977 return false ;
7078 }
@@ -79,3 +87,53 @@ export class GlobMatcher implements IMatcher {
7987 return Promise . resolve ( ) ;
8088 }
8189}
90+
91+ /**
92+ * An {@link IMatcher} for placeholders, which are link targets rather than
93+ * files.
94+ *
95+ * Only exclude globs apply: the include globs already selected the notes the
96+ * links come from, and say nothing about where a missing note would live. A
97+ * placeholder from a path link lies under a workspace root and is tested
98+ * relative to it, like a file. One from a wikilink keeps the link text as its
99+ * path (`missing-note`, `journal/2024-01-01`) and lies under no root, so that
100+ * path is tested as is — a leading slash meaning the workspace root — against
101+ * every root's excludes.
102+ */
103+ export class PlaceholderMatcher implements IMatcher {
104+ public readonly include = [ '**/*' ] ;
105+ public readonly exclude : string [ ] ;
106+
107+ private readonly roots : { prefix : string ; exclude : string [ ] } [ ] ;
108+
109+ constructor ( roots : Omit < GlobMatcherRoot , 'include' > [ ] ) {
110+ this . roots = roots . map ( r => ( {
111+ prefix : asPrefix ( r . uri . path ) ,
112+ exclude : r . exclude ,
113+ } ) ) ;
114+ this . exclude = roots . flatMap ( r => r . exclude ) ;
115+ }
116+
117+ match ( uris : URI [ ] ) : URI [ ] {
118+ return uris . filter ( u => this . isMatch ( u ) ) ;
119+ }
120+
121+ isMatch ( uri : URI ) : boolean {
122+ const owner = findOwner ( this . roots , uri . path ) ;
123+ return owner
124+ ? ! micromatch . isMatch (
125+ uri . path . slice ( owner . prefix . length ) ,
126+ owner . exclude ,
127+ MATCH_OPTIONS
128+ )
129+ : ! micromatch . isMatch (
130+ uri . path . replace ( / ^ \/ / , '' ) ,
131+ this . exclude ,
132+ MATCH_OPTIONS
133+ ) ;
134+ }
135+
136+ refresh ( ) : Promise < void > {
137+ return Promise . resolve ( ) ;
138+ }
139+ }
0 commit comments