Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Positioned Progress Tasks - Before or After Other Tasks #1250

Merged
142 changes: 131 additions & 11 deletions src/Spectre.Console/Live/Progress/ProgressContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@ public sealed class ProgressContext
/// <summary>
/// Gets a value indicating whether or not all started tasks have completed.
/// </summary>
public bool IsFinished => _tasks.Where(x => x.IsStarted).All(task => task.IsFinished);
public bool IsFinished
{
get
{
lock (_taskLock)
{
return _tasks.Where(x => x.IsStarted).All(task => task.IsFinished);
}
}
}

internal ProgressContext(IAnsiConsole console, ProgressRenderer renderer)
{
Expand All @@ -33,11 +42,68 @@ internal ProgressContext(IAnsiConsole console, ProgressRenderer renderer)
/// <returns>The newly created task.</returns>
public ProgressTask AddTask(string description, bool autoStart = true, double maxValue = 100)
{
return AddTask(description, new ProgressTaskSettings
lock (_taskLock)
{
AutoStart = autoStart,
MaxValue = maxValue,
});
var settings = new ProgressTaskSettings { AutoStart = autoStart, MaxValue = maxValue, };

return AddTaskAtInternal(description, settings, _tasks.Count);
}
}

/// <summary>
/// Adds a task.
/// </summary>
/// <param name="description">The task description.</param>
/// <param name="index">The index at which the task should be inserted.</param>
/// <param name="autoStart">Whether or not the task should start immediately.</param>
/// <param name="maxValue">The task's max value.</param>
/// <returns>The newly created task.</returns>
public ProgressTask AddTaskAt(string description, int index, bool autoStart = true, double maxValue = 100)
{
lock (_taskLock)
{
var settings = new ProgressTaskSettings { AutoStart = autoStart, MaxValue = maxValue, };

return AddTaskAtInternal(description, settings, index);
}
}

/// <summary>
/// Adds a task before the reference task.
/// </summary>
/// <param name="description">The task description.</param>
/// <param name="referenceProgressTask">The reference task to add before.</param>
/// <param name="autoStart">Whether or not the task should start immediately.</param>
/// <param name="maxValue">The task's max value.</param>
/// <returns>The newly created task.</returns>
public ProgressTask AddTaskBefore(string description, ProgressTask referenceProgressTask, bool autoStart = true, double maxValue = 100)
{
lock (_taskLock)
{
var settings = new ProgressTaskSettings { AutoStart = autoStart, MaxValue = maxValue, };
var indexOfReference = _tasks.IndexOf(referenceProgressTask);

return AddTaskAtInternal(description, settings, indexOfReference);
}
}

/// <summary>
/// Adds a task after the reference task.
/// </summary>
/// <param name="description">The task description.</param>
/// <param name="referenceProgressTask">The reference task to add after.</param>
/// <param name="autoStart">Whether or not the task should start immediately.</param>
/// <param name="maxValue">The task's max value.</param>
/// <returns>The newly created task.</returns>
public ProgressTask AddTaskAfter(string description, ProgressTask referenceProgressTask, bool autoStart = true, double maxValue = 100)
{
lock (_taskLock)
{
var settings = new ProgressTaskSettings { AutoStart = autoStart, MaxValue = maxValue, };
var indexOfReference = _tasks.IndexOf(referenceProgressTask);

return AddTaskAtInternal(description, settings, indexOfReference + 1);
}
}

/// <summary>
Expand All @@ -48,18 +114,58 @@ public ProgressTask AddTask(string description, bool autoStart = true, double ma
/// <returns>The newly created task.</returns>
public ProgressTask AddTask(string description, ProgressTaskSettings settings)
{
if (settings is null)
lock (_taskLock)
{
throw new ArgumentNullException(nameof(settings));
return AddTaskAtInternal(description, settings, _tasks.Count);
}
}

/// <summary>
/// Adds a task at the specified index.
/// </summary>
/// <param name="description">The task description.</param>
/// <param name="settings">The task settings.</param>
/// <param name="index">The index at which the task should be inserted.</param>
/// <returns>The newly created task.</returns>
public ProgressTask AddTaskAt(string description, ProgressTaskSettings settings, int index)
{
lock (_taskLock)
{
return AddTaskAtInternal(description, settings, index);
}
}

/// <summary>
/// Adds a task before the reference task.
/// </summary>
/// <param name="description">The task description.</param>
/// <param name="settings">The task settings.</param>
/// <param name="referenceProgressTask">The reference task to add before.</param>
/// <returns>The newly created task.</returns>
public ProgressTask AddTaskBefore(string description, ProgressTaskSettings settings, ProgressTask referenceProgressTask)
{
lock (_taskLock)
{
var task = new ProgressTask(_taskId++, description, settings.MaxValue, settings.AutoStart);
var indexOfReference = _tasks.IndexOf(referenceProgressTask);

return AddTaskAtInternal(description, settings, indexOfReference);
}
}

_tasks.Add(task);
/// <summary>
/// Adds a task after the reference task.
/// </summary>
/// <param name="description">The task description.</param>
/// <param name="settings">The task settings.</param>
/// <param name="referenceProgressTask">The reference task to add after.</param>
/// <returns>The newly created task.</returns>
public ProgressTask AddTaskAfter(string description, ProgressTaskSettings settings, ProgressTask referenceProgressTask)
{
lock (_taskLock)
{
var indexOfReference = _tasks.IndexOf(referenceProgressTask);

return task;
return AddTaskAtInternal(description, settings, indexOfReference + 1);
}
}

Expand All @@ -72,11 +178,25 @@ public void Refresh()
_console.Write(new ControlCode(string.Empty));
}

private ProgressTask AddTaskAtInternal(string description, ProgressTaskSettings settings, int position)
{
if (settings is null)
{
throw new ArgumentNullException(nameof(settings));
}

var task = new ProgressTask(_taskId++, description, settings.MaxValue, settings.AutoStart);

_tasks.Insert(position, task);

return task;
}

internal IReadOnlyList<ProgressTask> GetTasks()
{
lock (_taskLock)
{
return new List<ProgressTask>(_tasks);
}
}
}
}
78 changes: 78 additions & 0 deletions test/Spectre.Console.Tests/Unit/Live/Progress/ProgressTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,82 @@ public void Should_Report_Max_Remaining_Time_For_Extremely_Small_Progress()
// Then
task.RemainingTime.ShouldBe(TimeSpan.MaxValue);
}

[Fact]
public void Should_Render_Tasks_Added_Before_And_After_Correctly()
{
// Given
var console = new TestConsole()
.Width(10)
.Interactive()
.EmitAnsiSequences();

var progress = new Progress(console)
.Columns(new TaskDescriptionColumn())
.AutoRefresh(false)
.AutoClear(true);

// When
progress.Start(ctx =>
{
var foo1 = ctx.AddTask("foo1");
var foo2 = ctx.AddTask("foo2");
var foo3 = ctx.AddTask("foo3");

var afterFoo1 = ctx.AddTaskAfter("afterFoo1", foo1);
var beforeFoo3 = ctx.AddTaskBefore("beforeFoo3", foo3);
});

// Then
console.Output.SplitLines().Select(x => x.Trim()).ToArray()
.ShouldBeEquivalentTo(new[]
{
"[?25l",
"foo1",
"afterFoo1",
"foo2",
"beforeFoo3",
"foo3",
"[?25h",
});
}

[Fact]
public void Should_Render_Tasks_At_Specified_Indexes_Correctly()
{
// Given
var console = new TestConsole()
.Width(10)
.Interactive()
.EmitAnsiSequences();

var progress = new Progress(console)
.Columns(new TaskDescriptionColumn())
.AutoRefresh(false)
.AutoClear(true);

// When
progress.Start(ctx =>
{
var foo1 = ctx.AddTask("foo1");
var foo2 = ctx.AddTask("foo2");
var foo3 = ctx.AddTask("foo3");

var afterFoo1 = ctx.AddTaskAt("afterFoo1", 1);
var beforeFoo3 = ctx.AddTaskAt("beforeFoo3", 3);
});

// Then
console.Output.SplitLines().Select(x => x.Trim()).ToArray()
.ShouldBeEquivalentTo(new[]
{
"[?25l",
"foo1",
"afterFoo1",
"foo2",
"beforeFoo3",
"foo3",
"[?25h",
});
}
}