@@ -69,10 +69,14 @@ export async function tagComplete(completeEvent: CompleteEvent) {
6969 return null ;
7070 }
7171
72- const match = / # [ ^ # \s [ \] ] + \w * $ / . exec ( completeEvent . linePrefix ) ;
72+ const match = / # [ ^ # \s [ \] ] * $ / . exec ( completeEvent . linePrefix ) ;
7373 if ( ! match ) {
7474 return null ;
7575 }
76+ // Don't trigger on markdown headers (# Heading, ## Heading, etc.)
77+ if ( match . index === 0 && / ^ # { 1 , 6 } ( \s | $ ) / . test ( completeEvent . linePrefix ) ) {
78+ return null ;
79+ }
7680
7781 // Query all tags with a matching parent
7882 const allTags : string [ ] = await index . queryLuaObjects < string > ( "tag" , {
@@ -89,6 +93,73 @@ export async function tagComplete(completeEvent: CompleteEvent) {
8993 } ;
9094}
9195
96+ export async function frontmatterTagComplete ( completeEvent : CompleteEvent ) {
97+ // Only trigger inside frontmatter
98+ const frontmatterNode = completeEvent . parentNodes . find ( ( n ) =>
99+ n . startsWith ( "FrontMatter:" )
100+ ) ;
101+ if ( ! frontmatterNode ) {
102+ return null ;
103+ }
104+
105+ const fmContent = frontmatterNode . substring ( "FrontMatter:" . length ) ;
106+
107+ // Determine if the cursor line is within a tags section
108+ // Pattern 1: tags: value or tags: [v1, v2, partial (comma or space separated)
109+ const tagsLineMatch = / t a g s : \s + \[ ? (?: .* [ , \s ] \s * ) ? ( [ ^ \s ! @ $ % ^ & * ( ) , . ? " : { } | < > \\ [ \] ] * ) $ / . exec (
110+ completeEvent . linePrefix ,
111+ ) ;
112+
113+ let prefix = "" ;
114+ if ( tagsLineMatch ) {
115+ prefix = tagsLineMatch [ 1 ] ;
116+ } else {
117+ // Pattern 2: list item under a tags: key (e.g. " - partial")
118+ const listItemMatch = / ^ \s + - \s + ( [ ^ \s ! @ $ % ^ & * ( ) , . ? " : { } | < > \\ [ \] ] * ) $ / . exec ( completeEvent . linePrefix ) ;
119+ if ( ! listItemMatch ) {
120+ return null ;
121+ }
122+
123+ // Check if this list item is under the tags: key using cheap YAML parsing
124+ const lines = fmContent . split ( "\n" ) ;
125+ const cursorLineText = completeEvent . linePrefix ;
126+ let inTagsSection = false ;
127+ let foundCursorInTags = false ;
128+
129+ for ( const line of lines ) {
130+ const kvMatch = / ^ \s * ( \w + ) : / . exec ( line ) ;
131+ if ( kvMatch ) {
132+ inTagsSection = kvMatch [ 1 ] === "tags" ;
133+ }
134+ if ( inTagsSection && line . trimEnd ( ) === cursorLineText . trimEnd ( ) ) {
135+ foundCursorInTags = true ;
136+ break ;
137+ }
138+ }
139+
140+ if ( ! foundCursorInTags ) {
141+ return null ;
142+ }
143+ prefix = listItemMatch [ 1 ] ;
144+ }
145+
146+ // Strip # prefix if present (frontmatter tags can use #tag syntax)
147+ const cleanPrefix = prefix . replace ( / ^ # / , "" ) ;
148+
149+ const allTags : string [ ] = await index . queryLuaObjects < string > ( "tag" , {
150+ distinct : true ,
151+ select : { type : "Variable" , name : "name" , ctx : { } as any } ,
152+ } ) ;
153+
154+ return {
155+ from : completeEvent . pos - prefix . length ,
156+ options : allTags . map ( ( tag ) => ( {
157+ label : tag ,
158+ type : "tag" ,
159+ } ) ) ,
160+ } ;
161+ }
162+
92163export function updateITags < T > ( obj : ObjectValue < T > , frontmatter : FrontMatter ) {
93164 const itags = new Set < string > ( [ obj . tag , ...( frontmatter . tags || [ ] ) ] ) ;
94165 for ( const tag of obj . tags || [ ] ) {
0 commit comments