From 325206d3994eb0d67dc485bc60e50fabaca5f9df Mon Sep 17 00:00:00 2001 From: James Date: Fri, 7 Jul 2023 21:19:25 +1200 Subject: [PATCH] perf: :zap: prefer `TryParse` to avoid exceptions (#100) --- src/TogglTrack.cs | 765 +++++++++++++++++++++++----------------------- 1 file changed, 384 insertions(+), 381 deletions(-) diff --git a/src/TogglTrack.cs b/src/TogglTrack.cs index d61cd9d..e9ca559 100644 --- a/src/TogglTrack.cs +++ b/src/TogglTrack.cs @@ -836,27 +836,23 @@ private async ValueTask> _GetStartResults(CancellationToken token, TimeSpan startTimeSpan = TimeSpan.Zero; if (query.SearchTerms.Contains(Settings.TimeSpanFlag)) { - try + bool success = TimeSpanParser.TryParse( + new TransformedQuery(query) + .After(Settings.TimeSpanFlag) + .ToString(), + new TimeSpanParserOptions + { + UncolonedDefault = Units.Minutes, + ColonedDefault = Units.Minutes, + }, + out startTimeSpan + ); + if (success) { - startTimeSpan = TimeSpanParser.Parse( - new TransformedQuery(query) - .After(Settings.TimeSpanFlag) - .ToString(), - new TimeSpanParserOptions - { - UncolonedDefault = Units.Minutes, - ColonedDefault = Units.Minutes, - } - ); - description = new TransformedQuery(query) .To(Settings.TimeSpanFlag) .ReplaceProject(string.Empty, unescape: true); } - catch (ArgumentException) - { - // Invalid time span; so continue to create the time entry now - } } var startTime = DateTimeOffset.UtcNow + startTimeSpan; @@ -961,27 +957,23 @@ private async ValueTask> _GetStartResults(CancellationToken token, TimeSpan startTimeSpan = TimeSpan.Zero; if (query.SearchTerms.Contains(Settings.TimeSpanFlag)) { - try + bool success = TimeSpanParser.TryParse( + new TransformedQuery(query) + .After(Settings.TimeSpanFlag) + .ToString(), + new TimeSpanParserOptions + { + UncolonedDefault = Units.Minutes, + ColonedDefault = Units.Minutes, + }, + out startTimeSpan + ); + if (success) { - startTimeSpan = TimeSpanParser.Parse( - new TransformedQuery(query) - .After(Settings.TimeSpanFlag) - .ToString(), - new TimeSpanParserOptions - { - UncolonedDefault = Units.Minutes, - ColonedDefault = Units.Minutes, - } - ); - description = new TransformedQuery(query) .To(Settings.TimeSpanFlag) .ReplaceProject(string.Empty, unescape: true); } - catch (ArgumentException) - { - // Invalid time span; so continue to create the time entry now - } } var startTime = DateTimeOffset.UtcNow + startTimeSpan; @@ -1202,96 +1194,18 @@ private async ValueTask> _GetStartResults(CancellationToken token, } else { - try - { - var startTimeSpan = TimeSpanParser.Parse( - new TransformedQuery(query) - .After(Settings.TimeSpanFlag) - .ToString(), - new TimeSpanParserOptions - { - UncolonedDefault = Units.Minutes, - ColonedDefault = Units.Minutes, - } - ); - // If we get here, there will have been a valid time span - // An exception will be thrown if a time span was not able to be parsed - var startTime = DateTimeOffset.UtcNow + startTimeSpan; - - // Remove -t flag from description - string sanitisedDescription = new TransformedQuery(query) - .To(Settings.TimeSpanFlag) - .ToString(TransformedQuery.Escaping.Unescaped); - - results.Add(new Result + bool success = TimeSpanParser.TryParse( + new TransformedQuery(query) + .After(Settings.TimeSpanFlag) + .ToString(), + new TimeSpanParserOptions { - Title = $"Start {((string.IsNullOrEmpty(sanitisedDescription) ? Settings.EmptyTimeEntry : sanitisedDescription))} {startTime.Humanize()} at {startTime.ToLocalTime().ToString("t")}", - SubTitle = projectName, - IcoPath = this._colourIconProvider.GetColourIcon(project?.Colour, "start.png"), - AutoCompleteText = $"{query.ActionKeyword} {query.Search}", - Score = int.MaxValue - 1000, - Action = _ => - { - Task.Run(async delegate - { - try - { - this._context.API.LogInfo("TogglTrack", $"{projectId}, {workspaceId}, {sanitisedDescription}, {startTimeSpan.ToString()}, time span flag"); - - var runningTimeEntry = (await this._GetRunningTimeEntry(CancellationToken.None, force: true))?.ToTimeEntry(me); - if (runningTimeEntry is not null) - { - var stoppedTimeEntry = (await this._client.EditTimeEntry( - workspaceId: runningTimeEntry.WorkspaceId, - projectId: runningTimeEntry.ProjectId, - id: runningTimeEntry.Id, - stop: startTime, - duration: runningTimeEntry.Duration, - tags: runningTimeEntry.Tags, - billable: runningTimeEntry.Billable - ))?.ToTimeEntry(me); - - if (stoppedTimeEntry?.Id is null) - { - throw new Exception("An API error was encountered."); - } - } - - var createdTimeEntry = (await this._client.CreateTimeEntry( - workspaceId: workspaceId, - projectId: projectId, - description: sanitisedDescription, - start: startTime - ))?.ToTimeEntry(me); - - if (createdTimeEntry?.Id is null) - { - throw new Exception("An API error was encountered."); - } - - this.ShowSuccessMessage($"Started {createdTimeEntry.GetRawDescription(withTrailingSpace: true)}{startTime.Humanize()}", projectName, "start.png"); - - // Update cached running time entry state - this.RefreshCache(); - } - catch (Exception exception) - { - this._context.API.LogException("TogglTrack", "Failed to start time entry", exception); - this.ShowErrorMessage("Failed to start time entry.", exception.Message); - } - finally - { - this._state.SelectedIds.Project = -1; - } - }); - - return true; - }, - }); - - return results; - } - catch + UncolonedDefault = Units.Minutes, + ColonedDefault = Units.Minutes, + }, + out var startTimeSpan + ); + if (!success) { if (this._settings.ShowUsageExamples) { @@ -1316,6 +1230,81 @@ private async ValueTask> _GetStartResults(CancellationToken token, return results; } + + var startTime = DateTimeOffset.UtcNow + startTimeSpan; + + // Remove -t flag from description + string sanitisedDescription = new TransformedQuery(query) + .To(Settings.TimeSpanFlag) + .ToString(TransformedQuery.Escaping.Unescaped); + + results.Add(new Result + { + Title = $"Start {((string.IsNullOrEmpty(sanitisedDescription) ? Settings.EmptyTimeEntry : sanitisedDescription))} {startTime.Humanize()} at {startTime.ToLocalTime().ToString("t")}", + SubTitle = projectName, + IcoPath = this._colourIconProvider.GetColourIcon(project?.Colour, "start.png"), + AutoCompleteText = $"{query.ActionKeyword} {query.Search}", + Score = int.MaxValue - 1000, + Action = _ => + { + Task.Run(async delegate + { + try + { + this._context.API.LogInfo("TogglTrack", $"{projectId}, {workspaceId}, {sanitisedDescription}, {startTimeSpan.ToString()}, time span flag"); + + var runningTimeEntry = (await this._GetRunningTimeEntry(CancellationToken.None, force: true))?.ToTimeEntry(me); + if (runningTimeEntry is not null) + { + var stoppedTimeEntry = (await this._client.EditTimeEntry( + workspaceId: runningTimeEntry.WorkspaceId, + projectId: runningTimeEntry.ProjectId, + id: runningTimeEntry.Id, + stop: startTime, + duration: runningTimeEntry.Duration, + tags: runningTimeEntry.Tags, + billable: runningTimeEntry.Billable + ))?.ToTimeEntry(me); + + if (stoppedTimeEntry?.Id is null) + { + throw new Exception("An API error was encountered."); + } + } + + var createdTimeEntry = (await this._client.CreateTimeEntry( + workspaceId: workspaceId, + projectId: projectId, + description: sanitisedDescription, + start: startTime + ))?.ToTimeEntry(me); + + if (createdTimeEntry?.Id is null) + { + throw new Exception("An API error was encountered."); + } + + this.ShowSuccessMessage($"Started {createdTimeEntry.GetRawDescription(withTrailingSpace: true)}{startTime.Humanize()}", projectName, "start.png"); + + // Update cached running time entry state + this.RefreshCache(); + } + catch (Exception exception) + { + this._context.API.LogException("TogglTrack", "Failed to start time entry", exception); + this.ShowErrorMessage("Failed to start time entry.", exception.Message); + } + finally + { + this._state.SelectedIds.Project = -1; + } + }); + + return true; + }, + }); + + return results; } // Use cached time entries here to ensure responsiveness @@ -1482,76 +1471,18 @@ private async ValueTask> _GetStopResults(CancellationToken token, Q return results; } - try - { - var stopTimeSpan = TimeSpanParser.Parse( - new TransformedQuery(query) - .After(Settings.TimeSpanEndFlag) - .ToString(), - new TimeSpanParserOptions - { - UncolonedDefault = Units.Minutes, - ColonedDefault = Units.Minutes, - } - ); - // An exception will be thrown if a time span was not able to be parsed - // If we get here, there will have been a valid time span - var stopTime = DateTimeOffset.UtcNow + stopTimeSpan; - if (stopTime < runningTimeEntry.StartDate) - { - // Ensure stop is not before start - stopTime = runningTimeEntry.StartDate; - } - - var newElapsed = stopTime.Subtract(runningTimeEntry.StartDate); - - results.Add(new Result + bool success = TimeSpanParser.TryParse( + new TransformedQuery(query) + .After(Settings.TimeSpanEndFlag) + .ToString(), + new TimeSpanParserOptions { - Title = $"Stop {runningTimeEntry.GetDescription()} {stopTime.Humanize()} at {stopTime.ToLocalTime().ToString("t")}", - SubTitle = $"{projectName} | {newElapsed.Humanize(minUnit: Humanizer.Localisation.TimeUnit.Second, maxUnit: Humanizer.Localisation.TimeUnit.Hour)} ({(int)newElapsed.TotalHours}:{newElapsed.ToString(@"mm\:ss")})", - IcoPath = this._colourIconProvider.GetColourIcon(runningTimeEntry.Project?.Colour, "stop.png"), - AutoCompleteText = $"{query.ActionKeyword} {query.Search}", - Score = 100000, - Action = _ => - { - Task.Run(async delegate - { - try - { - this._context.API.LogInfo("TogglTrack", $"{this._state.SelectedIds.Project}, {runningTimeEntry.Id}, {runningTimeEntry.WorkspaceId}, {runningTimeEntry.StartDate}, {(int)newElapsed.TotalHours}:{newElapsed.ToString(@"mm\:ss")}, {stopTime}, time span flag"); - - var stoppedTimeEntry = (await this._client.EditTimeEntry( - workspaceId: runningTimeEntry.WorkspaceId, - projectId: runningTimeEntry.ProjectId, - id: runningTimeEntry.Id, - stop: stopTime, - duration: runningTimeEntry.Duration, - tags: runningTimeEntry.Tags, - billable: runningTimeEntry.Billable - ))?.ToTimeEntry(me); - - if (stoppedTimeEntry?.Id is null) - { - throw new Exception("An API error was encountered."); - } - - this.ShowSuccessMessage($"Stopped {stoppedTimeEntry.GetRawDescription()}", $"{(int)newElapsed.TotalHours}:{newElapsed.ToString(@"mm\:ss")} elapsed", "stop.png"); - - // Update cached running time entry state - this.RefreshCache(); - } - catch (Exception exception) - { - this._context.API.LogException("TogglTrack", "Failed to stop time entry", exception); - this.ShowErrorMessage("Failed to stop time entry.", exception.Message); - } - }); - - return true; - }, - }); - } - catch + UncolonedDefault = Units.Minutes, + ColonedDefault = Units.Minutes, + }, + out var stopTimeSpan + ); + if (!success) { if (this._settings.ShowUsageExamples) { @@ -1569,8 +1500,65 @@ private async ValueTask> _GetStopResults(CancellationToken token, Q } }); } + + return results; } + var stopTime = DateTimeOffset.UtcNow + stopTimeSpan; + if (stopTime < runningTimeEntry.StartDate) + { + // Ensure stop is not before start + stopTime = runningTimeEntry.StartDate; + } + + var newElapsed = stopTime.Subtract(runningTimeEntry.StartDate); + + results.Add(new Result + { + Title = $"Stop {runningTimeEntry.GetDescription()} {stopTime.Humanize()} at {stopTime.ToLocalTime().ToString("t")}", + SubTitle = $"{projectName} | {newElapsed.Humanize(minUnit: Humanizer.Localisation.TimeUnit.Second, maxUnit: Humanizer.Localisation.TimeUnit.Hour)} ({(int)newElapsed.TotalHours}:{newElapsed.ToString(@"mm\:ss")})", + IcoPath = this._colourIconProvider.GetColourIcon(runningTimeEntry.Project?.Colour, "stop.png"), + AutoCompleteText = $"{query.ActionKeyword} {query.Search}", + Score = 100000, + Action = _ => + { + Task.Run(async delegate + { + try + { + this._context.API.LogInfo("TogglTrack", $"{this._state.SelectedIds.Project}, {runningTimeEntry.Id}, {runningTimeEntry.WorkspaceId}, {runningTimeEntry.StartDate}, {(int)newElapsed.TotalHours}:{newElapsed.ToString(@"mm\:ss")}, {stopTime}, time span flag"); + + var stoppedTimeEntry = (await this._client.EditTimeEntry( + workspaceId: runningTimeEntry.WorkspaceId, + projectId: runningTimeEntry.ProjectId, + id: runningTimeEntry.Id, + stop: stopTime, + duration: runningTimeEntry.Duration, + tags: runningTimeEntry.Tags, + billable: runningTimeEntry.Billable + ))?.ToTimeEntry(me); + + if (stoppedTimeEntry?.Id is null) + { + throw new Exception("An API error was encountered."); + } + + this.ShowSuccessMessage($"Stopped {stoppedTimeEntry.GetRawDescription()}", $"{(int)newElapsed.TotalHours}:{newElapsed.ToString(@"mm\:ss")} elapsed", "stop.png"); + + // Update cached running time entry state + this.RefreshCache(); + } + catch (Exception exception) + { + this._context.API.LogException("TogglTrack", "Failed to stop time entry", exception); + this.ShowErrorMessage("Failed to stop time entry.", exception.Message); + } + }); + + return true; + }, + }); + return results; } @@ -2060,238 +2048,253 @@ private async ValueTask> _GetEditResults(CancellationToken token, Q break; } - try - { - TimeSpan? startTimeSpan = null; - TimeSpan? endTimeSpan = null; - TimeSpan newElapsed = timeEntry.Elapsed; + TimeSpan? startTimeSpan = null; + TimeSpan? endTimeSpan = null; + TimeSpan newElapsed = timeEntry.Elapsed; - if (hasTimeSpanFlag) + if (hasTimeSpanFlag) + { + bool success = TimeSpanParser.TryParse( + new TransformedQuery(query) + .After(Settings.TimeSpanFlag) + .ToString(), + new TimeSpanParserOptions + { + UncolonedDefault = Units.Minutes, + ColonedDefault = Units.Minutes, + }, + out var parsedStartTimeSpan + ); + if (success) { - try + startTimeSpan = parsedStartTimeSpan; + newElapsed -= parsedStartTimeSpan; + } + else + { + if (this._settings.ShowUsageExamples) { - startTimeSpan = TimeSpanParser.Parse( - new TransformedQuery(query) - .After(Settings.TimeSpanFlag) - .ToString(), - new TimeSpanParserOptions + string queryToFlag = new TransformedQuery(query) + .To(Settings.TimeSpanFlag) + .ToString(); + + results.Add(new Result + { + Title = Settings.UsageExampleTitle, + SubTitle = $"{query.ActionKeyword} {queryToFlag} {Settings.TimeSpanFlag} 5 mins", + IcoPath = "tip.png", + AutoCompleteText = $"{query.ActionKeyword} {queryToFlag} {Settings.TimeSpanFlag} 5 mins", + Score = 100000, + Action = _ => { - UncolonedDefault = Units.Minutes, - ColonedDefault = Units.Minutes, + this._context.API.ChangeQuery($"{query.ActionKeyword} {queryToFlag} {Settings.TimeSpanFlag} 5 mins"); + return false; } - ); - // An exception will be thrown if a time span was not able to be parsed - // If we get here, there will have been a valid time span - newElapsed = newElapsed.Subtract((TimeSpan)startTimeSpan); + }); } - catch + + return results; + } + } + if (hasTimeSpanEndFlag) + { + bool success = TimeSpanParser.TryParse( + new TransformedQuery(query) + .After(Settings.TimeSpanEndFlag) + .ToString(), + new TimeSpanParserOptions { - throw new ArgumentException(Settings.TimeSpanFlag); - } + UncolonedDefault = Units.Minutes, + ColonedDefault = Units.Minutes, + }, + out var parsedEndTimeSpan + ); + if (success) + { + endTimeSpan = parsedEndTimeSpan; + newElapsed += parsedEndTimeSpan; } - if (hasTimeSpanEndFlag) + else { - try + if (this._settings.ShowUsageExamples) { - endTimeSpan = TimeSpanParser.Parse( - new TransformedQuery(query) - .After(Settings.TimeSpanEndFlag) - .ToString(), - new TimeSpanParserOptions + string queryToFlag = new TransformedQuery(query) + .To(Settings.TimeSpanEndFlag) + .ToString(); + + results.Add(new Result + { + Title = Settings.UsageExampleTitle, + SubTitle = $"{query.ActionKeyword} {queryToFlag} {Settings.TimeSpanEndFlag} 5 mins", + IcoPath = "tip.png", + AutoCompleteText = $"{query.ActionKeyword} {queryToFlag} {Settings.TimeSpanEndFlag} 5 mins", + Score = 100000, + Action = _ => { - UncolonedDefault = Units.Minutes, - ColonedDefault = Units.Minutes, + this._context.API.ChangeQuery($"{query.ActionKeyword} {queryToFlag} {Settings.TimeSpanEndFlag} 5 mins"); + return false; } - ); - // An exception will be thrown if a time span was not able to be parsed - // If we get here, there will have been a valid time span - newElapsed = newElapsed.Add((TimeSpan)endTimeSpan); - } - catch - { - throw new ArgumentException(Settings.TimeSpanEndFlag); + }); } - } - // Remove flags from description - string sanitisedDescription = new TransformedQuery(query) - .Between(ArgumentIndices.Description, firstFlag) - .ToString(TransformedQuery.Escaping.Unescaped); - - if (this._settings.ShowUsageWarnings && string.IsNullOrEmpty(sanitisedDescription) && !string.IsNullOrEmpty(timeEntry.GetRawDescription())) - { - results.Add(new Result - { - Title = Settings.UsageWarningTitle, - SubTitle = $"Time entry description will be cleared if nothing is entered!", - IcoPath = "tip-warning.png", - AutoCompleteText = $"{query.ActionKeyword} {query.Search} {timeEntry.GetRawDescription(withTrailingSpace: true, escapePotentialSymbols: true)}", - Score = 1000, - Action = _ => - { - this._context.API.ChangeQuery($"{query.ActionKeyword} {query.Search} {timeEntry.GetRawDescription(withTrailingSpace: true, escapePotentialSymbols: true)}"); - return false; - } - }); + return results; } - var startTime = (timeEntry.StartDate + startTimeSpan) ?? timeEntry.StartDate; - var stopTime = ((timeEntry.StopDate ?? DateTimeOffset.UtcNow) + endTimeSpan) ?? timeEntry.StopDate; + } + + // Remove flags from description + string sanitisedDescription = new TransformedQuery(query) + .Between(ArgumentIndices.Description, firstFlag) + .ToString(TransformedQuery.Escaping.Unescaped); + if (this._settings.ShowUsageWarnings && string.IsNullOrEmpty(sanitisedDescription) && !string.IsNullOrEmpty(timeEntry.GetRawDescription())) + { results.Add(new Result { - Title = (string.IsNullOrEmpty(sanitisedDescription)) - ? timeEntry.GetDescription() - : sanitisedDescription, - SubTitle = $"{projectName} | {newElapsed.Humanize(minUnit: Humanizer.Localisation.TimeUnit.Second, maxUnit: Humanizer.Localisation.TimeUnit.Hour)} ({(int)newElapsed.TotalHours}:{newElapsed.ToString(@"mm\:ss")})", - IcoPath = this._colourIconProvider.GetColourIcon(project?.Colour, "edit.png"), - AutoCompleteText = $"{query.ActionKeyword} {query.Search}", - Score = int.MaxValue - 1000, + Title = Settings.UsageWarningTitle, + SubTitle = $"Time entry description will be cleared if nothing is entered!", + IcoPath = "tip-warning.png", + AutoCompleteText = $"{query.ActionKeyword} {query.Search} {timeEntry.GetRawDescription(withTrailingSpace: true, escapePotentialSymbols: true)}", + Score = 1000, Action = _ => { - Task.Run(async delegate - { - try - { - this._context.API.LogInfo("TogglTrack", $"{projectId}, {timeEntry.Id}, {timeEntry.Duration}, {timeEntry.Start}, {timeEntry.WorkspaceId}, {sanitisedDescription}, {startTime.ToString("yyyy-MM-ddTHH:mm:ssZ")}, {startTimeSpan.ToString()}, {stopTime?.ToString("yyyy-MM-ddTHH:mm:ssZ")}, {endTimeSpan.ToString()}, edit start time"); + this._context.API.ChangeQuery($"{query.ActionKeyword} {query.Search} {timeEntry.GetRawDescription(withTrailingSpace: true, escapePotentialSymbols: true)}"); + return false; + } + }); + } - var editedTimeEntry = (await this._client.EditTimeEntry( - workspaceId: timeEntry.WorkspaceId, - projectId: projectId, - id: timeEntry.Id, - description: sanitisedDescription, - start: startTime, - stop: stopTime, - duration: timeEntry.Duration, - tags: timeEntry.Tags, - billable: timeEntry.Billable - ))?.ToTimeEntry(me); + var startTime = (timeEntry.StartDate + startTimeSpan) ?? timeEntry.StartDate; + var stopTime = ((timeEntry.StopDate ?? DateTimeOffset.UtcNow) + endTimeSpan) ?? timeEntry.StopDate; - if (editedTimeEntry?.Id is null) - { - throw new Exception("An API error was encountered."); - } + results.Add(new Result + { + Title = (string.IsNullOrEmpty(sanitisedDescription)) + ? timeEntry.GetDescription() + : sanitisedDescription, + SubTitle = $"{projectName} | {newElapsed.Humanize(minUnit: Humanizer.Localisation.TimeUnit.Second, maxUnit: Humanizer.Localisation.TimeUnit.Hour)} ({(int)newElapsed.TotalHours}:{newElapsed.ToString(@"mm\:ss")})", + IcoPath = this._colourIconProvider.GetColourIcon(project?.Colour, "edit.png"), + AutoCompleteText = $"{query.ActionKeyword} {query.Search}", + Score = int.MaxValue - 1000, + Action = _ => + { + Task.Run(async delegate + { + try + { + this._context.API.LogInfo("TogglTrack", $"{projectId}, {timeEntry.Id}, {timeEntry.Duration}, {timeEntry.Start}, {timeEntry.WorkspaceId}, {sanitisedDescription}, {startTime.ToString("yyyy-MM-ddTHH:mm:ssZ")}, {startTimeSpan.ToString()}, {stopTime?.ToString("yyyy-MM-ddTHH:mm:ssZ")}, {endTimeSpan.ToString()}, edit start time"); - this.ShowSuccessMessage($"Edited {editedTimeEntry.GetRawDescription()}", $"{projectName} | {(int)newElapsed.TotalHours}:{newElapsed.ToString(@"mm\:ss")}", "edit.png"); + var editedTimeEntry = (await this._client.EditTimeEntry( + workspaceId: timeEntry.WorkspaceId, + projectId: projectId, + id: timeEntry.Id, + description: sanitisedDescription, + start: startTime, + stop: stopTime, + duration: timeEntry.Duration, + tags: timeEntry.Tags, + billable: timeEntry.Billable + ))?.ToTimeEntry(me); - // Update cached running time entry state - this.RefreshCache(); - } - catch (Exception exception) - { - this._context.API.LogException("TogglTrack", "Failed to edit time entry", exception); - this.ShowErrorMessage("Failed to edit time entry.", exception.Message); - } - finally + if (editedTimeEntry?.Id is null) { - this._state.SelectedIds.TimeEntry = -1; - this._state.SelectedIds.Project = -1; + throw new Exception("An API error was encountered."); } - }); - return true; - }, - }); + this.ShowSuccessMessage($"Edited {editedTimeEntry.GetRawDescription()}", $"{projectName} | {(int)newElapsed.TotalHours}:{newElapsed.ToString(@"mm\:ss")}", "edit.png"); - if (!string.IsNullOrEmpty(sanitisedDescription)) - { - results.AddRange(maxTimeEntries.Groups.Values.SelectMany(pastProject => - { - if (pastProject.SubGroups is null) + // Update cached running time entry state + this.RefreshCache(); + } + catch (Exception exception) { - return Enumerable.Empty(); + this._context.API.LogException("TogglTrack", "Failed to edit time entry", exception); + this.ShowErrorMessage("Failed to edit time entry.", exception.Message); + } + finally + { + this._state.SelectedIds.TimeEntry = -1; + this._state.SelectedIds.Project = -1; } + }); + + return true; + }, + }); - return pastProject.SubGroups.Values - .Where(pastTimeEntry => ( - ( - pastProject.Project?.Id != timeEntry.Project?.Id || - pastTimeEntry.GetRawTitle() != sanitisedDescription - ) && - ( - this._context.API.FuzzySearch(sanitisedDescription, pastTimeEntry.GetTitle()).Score > 0 - ) + if (!string.IsNullOrEmpty(sanitisedDescription)) + { + results.AddRange(maxTimeEntries.Groups.Values.SelectMany(pastProject => + { + if (pastProject.SubGroups is null) + { + return Enumerable.Empty(); + } + + return pastProject.SubGroups.Values + .Where(pastTimeEntry => ( + ( + pastProject.Project?.Id != timeEntry.Project?.Id || + pastTimeEntry.GetRawTitle() != sanitisedDescription + ) && + ( + this._context.API.FuzzySearch(sanitisedDescription, pastTimeEntry.GetTitle()).Score > 0 ) ) - .Select(pastTimeEntry => new Result + ) + .Select(pastTimeEntry => new Result + { + Title = pastTimeEntry.GetTitle(), + SubTitle = $"{pastProject.Project?.WithClientName ?? Settings.NoProjectName} | {newElapsed.Humanize(minUnit: Humanizer.Localisation.TimeUnit.Second, maxUnit: Humanizer.Localisation.TimeUnit.Hour)} ({(int)newElapsed.TotalHours}:{newElapsed.ToString(@"mm\:ss")})", + IcoPath = this._colourIconProvider.GetColourIcon(pastProject.Project?.Colour, "edit.png"), + AutoCompleteText = $"{query.ActionKeyword} {pastTimeEntry.GetTitle(escapePotentialSymbols: true)}", + Score = pastTimeEntry.GetScoreByStart(), + Action = _ => { - Title = pastTimeEntry.GetTitle(), - SubTitle = $"{pastProject.Project?.WithClientName ?? Settings.NoProjectName} | {newElapsed.Humanize(minUnit: Humanizer.Localisation.TimeUnit.Second, maxUnit: Humanizer.Localisation.TimeUnit.Hour)} ({(int)newElapsed.TotalHours}:{newElapsed.ToString(@"mm\:ss")})", - IcoPath = this._colourIconProvider.GetColourIcon(pastProject.Project?.Colour, "edit.png"), - AutoCompleteText = $"{query.ActionKeyword} {pastTimeEntry.GetTitle(escapePotentialSymbols: true)}", - Score = pastTimeEntry.GetScoreByStart(), - Action = _ => + Task.Run(async delegate { - Task.Run(async delegate + try { - try - { - this._context.API.LogInfo("TogglTrack", $"past time entry {pastProject.Project?.Id}, {pastTimeEntry.Id}, {timeEntry.Duration}, {timeEntry.Start}, {projectId}, {timeEntry.WorkspaceId}, {sanitisedDescription}, {pastTimeEntry.GetTitle()}"); - - var editedTimeEntry = (await this._client.EditTimeEntry( - workspaceId: timeEntry.WorkspaceId, - projectId: pastProject.Project?.Id, - id: timeEntry.Id, - description: pastTimeEntry.GetRawTitle(), - start: startTime, - stop: stopTime, - duration: timeEntry.Duration, - tags: timeEntry.Tags, - billable: timeEntry.Billable - ))?.ToTimeEntry(me); - - if (editedTimeEntry?.Id is null) - { - throw new Exception("An API error was encountered."); - } - - this.ShowSuccessMessage($"Edited {editedTimeEntry.GetRawDescription()}", $"{pastProject.Project?.WithClientName ?? Settings.NoProjectName} | {(int)newElapsed.TotalHours}:{newElapsed.ToString(@"mm\:ss")}", "edit.png"); - - // Update cached running time entry state - this.RefreshCache(); - } - catch (Exception exception) - { - this._context.API.LogException("TogglTrack", "Failed to edit time entry", exception); - this.ShowErrorMessage("Failed to edit time entry.", exception.Message); - } - finally + this._context.API.LogInfo("TogglTrack", $"past time entry {pastProject.Project?.Id}, {pastTimeEntry.Id}, {timeEntry.Duration}, {timeEntry.Start}, {projectId}, {timeEntry.WorkspaceId}, {sanitisedDescription}, {pastTimeEntry.GetTitle()}"); + + var editedTimeEntry = (await this._client.EditTimeEntry( + workspaceId: timeEntry.WorkspaceId, + projectId: pastProject.Project?.Id, + id: timeEntry.Id, + description: pastTimeEntry.GetRawTitle(), + start: startTime, + stop: stopTime, + duration: timeEntry.Duration, + tags: timeEntry.Tags, + billable: timeEntry.Billable + ))?.ToTimeEntry(me); + + if (editedTimeEntry?.Id is null) { - this._state.SelectedIds.TimeEntry = -1; - this._state.SelectedIds.Project = -1; + throw new Exception("An API error was encountered."); } - }); - return true; - }, - }); - })); - } - } - catch (ArgumentException exception) - { - if (this._settings.ShowUsageExamples) - { - string flag = exception.Message; + this.ShowSuccessMessage($"Edited {editedTimeEntry.GetRawDescription()}", $"{pastProject.Project?.WithClientName ?? Settings.NoProjectName} | {(int)newElapsed.TotalHours}:{newElapsed.ToString(@"mm\:ss")}", "edit.png"); - string queryToFlag = new TransformedQuery(query) - .To(flag) - .ToString(); + // Update cached running time entry state + this.RefreshCache(); + } + catch (Exception exception) + { + this._context.API.LogException("TogglTrack", "Failed to edit time entry", exception); + this.ShowErrorMessage("Failed to edit time entry.", exception.Message); + } + finally + { + this._state.SelectedIds.TimeEntry = -1; + this._state.SelectedIds.Project = -1; + } + }); - results.Add(new Result - { - Title = Settings.UsageExampleTitle, - SubTitle = $"{query.ActionKeyword} {queryToFlag} {flag} 5 mins", - IcoPath = "tip.png", - AutoCompleteText = $"{query.ActionKeyword} {queryToFlag} {flag} 5 mins", - Score = 100000, - Action = _ => - { - this._context.API.ChangeQuery($"{query.ActionKeyword} {queryToFlag} {flag} 5 mins"); - return false; - } - }); - } + return true; + }, + }); + })); } } @@ -2518,7 +2521,7 @@ private async ValueTask> _GetReportsResults(CancellationToken token /* NO_SPAN | NO_VALID_ARG | NO_VALID_DATE | SELECTING_SPAN ------------------------------------------------------- - 0 0 0 X — Not possible + 0 0 0 X — Not possible 0 0 1 0 — Valid argument 0 1 0 0 — Valid date 0 1 1 1 — No valid span