@@ -4,6 +4,7 @@ import type { Settings } from '../Config/Settings';
44import { DateParser } from '../DateTime/DateParser' ;
55import { doAutocomplete } from '../DateTime/DateAbbreviations' ;
66import { Occurrence } from '../Task/Occurrence' ;
7+ import { Duration } from '../Task/Duration' ;
78import { Recurrence } from '../Task/Recurrence' ;
89import {
910 type DefaultTaskSerializerSymbols ,
@@ -80,6 +81,9 @@ export function makeDefaultSuggestionBuilder(
8081 // add date suggestions if relevant
8182 suggestions = suggestions . concat ( addDatesSuggestions ( datePrefixRegex , maxGenericSuggestions , parameters ) ) ;
8283
84+ // add duration suggestions if relevant
85+ suggestions = suggestions . concat ( addDurationValueSuggestions ( symbols . durationSymbol , parameters ) ) ;
86+
8387 // add recurrence suggestions if relevant
8488 suggestions = suggestions . concat ( addRecurrenceValueSuggestions ( symbols . recurrenceSymbol , parameters ) ) ;
8589
@@ -152,6 +156,7 @@ function addTaskPropertySuggestions(
152156 addField ( genericSuggestions , line , symbols . dueDateSymbol , 'due date' ) ;
153157 addField ( genericSuggestions , line , symbols . startDateSymbol , 'start date' ) ;
154158 addField ( genericSuggestions , line , symbols . scheduledDateSymbol , 'scheduled date' ) ;
159+ addField ( genericSuggestions , line , symbols . durationSymbol , 'duration' ) ;
155160
156161 addPrioritySuggestions ( genericSuggestions , symbols , parameters ) ;
157162 addField ( genericSuggestions , line , symbols . recurrenceSymbol , 'recurring (repeat)' ) ;
@@ -272,6 +277,95 @@ function dateExtractor(symbol: string, date: string) {
272277 return { displayText, appendText } ;
273278}
274279
280+ /*
281+ * If the cursor is located in a section that should be followed by a duration description, suggest options
282+ * for what to enter as a duration.
283+ * This has two parts: either generic predefined suggestions, or a single suggestion that is a parsed result
284+ * of what the user is typing.
285+ * Generic predefined suggestions, in turn, also have two options: either filtered (if the user started typing
286+ * something where a duration is expected) or unfiltered
287+ */
288+ function addDurationValueSuggestions ( durationSymbol : string , parameters : SuggestorParameters ) {
289+ let genericSuggestions = [ '5m' , '15m' , '1h' , '30m' , '45m' , '2h' , '3h' ] ;
290+ const hourSuggestions = [ '15m' , '30m' , '45m' ] ;
291+
292+ const results : SuggestInfo [ ] = [ ] ;
293+ const durationRegex = new RegExp ( `(${ durationSymbol } )\\s*([0-9hm]*)` , 'ug' ) ;
294+ const durationMatch = matchIfCursorInRegex ( durationRegex , parameters ) ;
295+ if ( durationMatch && durationMatch . length >= 1 ) {
296+ const durationPrefix = durationMatch [ 1 ] ;
297+ let durationString = '' ;
298+ if ( durationMatch [ 2 ] ) {
299+ durationString = durationMatch [ 2 ] ;
300+ }
301+ if ( durationString . length > 0 ) {
302+ // If the text matches a valid duration, suggest logical continuations
303+ let parsedduration = Duration . fromText ( durationString ) ;
304+ if ( ! parsedduration ) {
305+ //not a valid duration string => no h/m yet. Suggest finishing with minutes or complete hour!
306+ if ( parseInt ( durationString , 10 ) > 0 ) {
307+ results . push ( {
308+ suggestionType : 'match' ,
309+ displayText : `${ durationPrefix } ${ durationString } m` ,
310+ appendText : `${ durationPrefix } ${ durationString } m` + parameters . postfix ,
311+ insertAt : durationMatch . index ,
312+ insertSkip : calculateSkipValueForMatch ( durationMatch [ 0 ] , parameters ) ,
313+ } ) ;
314+ genericSuggestions = genericSuggestions . filter ( ( s ) => s != `${ durationString } m` ) ;
315+ results . push ( {
316+ suggestionType : 'match' ,
317+ displayText : `${ durationPrefix } ${ durationString } h` ,
318+ appendText : `${ durationPrefix } ${ durationString } h` + parameters . postfix ,
319+ insertAt : durationMatch . index ,
320+ insertSkip : calculateSkipValueForMatch ( durationMatch [ 0 ] , parameters ) ,
321+ } ) ;
322+ genericSuggestions = genericSuggestions . filter ( ( s ) => s != `${ durationString } h` ) ;
323+ }
324+ // also suggest that '2' implies '2h', thus also suggesting continuations like '2h30m'
325+ parsedduration = Duration . fromText ( durationString + 'h' ) ;
326+ }
327+ if ( parsedduration ) {
328+ // special suggestions on finished hour like '123h'
329+ const genText = ( sugg : string ) => `${ durationPrefix } ${ parsedduration ! . hours } h${ sugg } ` ;
330+ for ( const suggestion of hourSuggestions . filter (
331+ // suggestion is either all suggestions or the one with matching prefix
332+ ( s ) => parsedduration ?. minutes == 0 || s . startsWith ( parsedduration ! . minutes . toString ( 10 ) ) ,
333+ ) ) {
334+ results . push ( {
335+ suggestionType : 'match' ,
336+ displayText : `${ genText ( suggestion ) } ` ,
337+ appendText : genText ( suggestion ) + parameters . postfix ,
338+ insertAt : durationMatch . index ,
339+ insertSkip : calculateSkipValueForMatch ( durationMatch [ 0 ] , parameters ) ,
340+ } ) ;
341+ }
342+ }
343+ }
344+ // Now to generic predefined suggestions.
345+ // If we get a partial match with some of the suggestions (e.g. the user started typing "3"),
346+ // we use that for matches to the generic example-list above (i.e. "3h").
347+ // Otherwise, we just display the list of suggestions, and either way, truncate them eventually to
348+ // a max number.
349+ // In the case of duration rules, the max number should be small enough to allow users to "escape"
350+ // the mode of writing a duration rule, i.e. we should leave enough space for component suggestions
351+ const maxGenericDurationSuggestions = parameters . settings . autoSuggestMaxItems / 2 ;
352+ const genericMatches = filterGenericSuggestions (
353+ genericSuggestions ,
354+ durationString ,
355+ maxGenericDurationSuggestions ,
356+ true ,
357+ ) ;
358+
359+ const extractor = ( durationPrefix : string , match : string ) => {
360+ const displayText = `${ durationPrefix } ${ match } ` ;
361+ const appendText = `${ durationPrefix } ${ match } ` ;
362+ return { displayText, appendText } ;
363+ } ;
364+ constructSuggestions ( parameters , durationMatch , genericMatches , extractor , results ) ;
365+ }
366+
367+ return results ;
368+ }
275369/*
276370 * If the cursor is located in a section that should be followed by a date (due, start date or scheduled date),
277371 * suggest options for what to enter as a date.
0 commit comments