Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 236 additions & 0 deletions Cast.Tool.Tests/LspTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
using Xunit;
using Cast.Tool.Core;
using System.IO;
using System.Threading.Tasks;

namespace Cast.Tool.Tests;

public class LspTests : IDisposable
{
private readonly LspHelper _helper;
private readonly string _testFilePath;

public LspTests()
{
_helper = new LspHelper();

// Create a temporary C# test file
_testFilePath = Path.GetTempFileName();
File.WriteAllText(_testFilePath, @"
using System;

namespace TestNamespace
{
public class TestClass
{
public string TestProperty { get; set; }

public void TestMethod()
{
Console.WriteLine(""Hello World"");
}
}
}");

// Rename to .cs extension
var csFilePath = Path.ChangeExtension(_testFilePath, ".cs");
File.Move(_testFilePath, csFilePath);
_testFilePath = csFilePath;
}

public void Dispose()
{
if (File.Exists(_testFilePath))
{
File.Delete(_testFilePath);
}
}

[Fact]
public void LspHelper_ValidateInputs_ValidFile_ShouldNotThrow()
{
// Arrange
var settings = new LspSettings { FilePath = _testFilePath };

// Act & Assert - Should not throw
_helper.ValidateInputs(settings);
}

[Fact]
public void LspHelper_ValidateInputs_InvalidFile_ShouldThrow()
{
// Arrange
var settings = new LspSettings { FilePath = "nonexistent.cs" };

// Act & Assert
Assert.Throws<FileNotFoundException>(() => _helper.ValidateInputs(settings));
}

[Fact]
public void LspHelper_ValidateInputs_NegativeLineNumber_ShouldThrow()
{
// Arrange
var settings = new LspSettings { FilePath = _testFilePath, LineNumber = -1 };

// Act & Assert
Assert.Throws<ArgumentException>(() => _helper.ValidateInputs(settings));
}

[Fact]
public void LspHelper_ValidateInputs_NegativeColumnNumber_ShouldThrow()
{
// Arrange
var settings = new LspSettings { FilePath = _testFilePath, ColumnNumber = -1 };

// Act & Assert
Assert.Throws<ArgumentException>(() => _helper.ValidateInputs(settings));
}

[Fact]
public void LspHelper_CreatePosition_ShouldReturnCorrectPosition()
{
// Arrange
var settings = new LspSettings { LineNumber = 5, ColumnNumber = 10 };

// Act
var position = _helper.CreatePosition(settings);

// Assert
Assert.Equal(5, position.Line);
Assert.Equal(10, position.Character);
}

[Fact]
public void LspHelper_CreateDocumentUri_ShouldReturnValidUri()
{
// Act
var uri = _helper.CreateDocumentUri(_testFilePath);

// Assert
Assert.StartsWith("file://", uri);
Assert.Contains(Path.GetFileName(_testFilePath), uri);
}

[Fact]
public void LspHelper_GetLanguageId_CSharpFile_ShouldReturnCSharp()
{
// Act
var languageId = _helper.GetLanguageId(_testFilePath);

// Assert
Assert.Equal("csharp", languageId);
}

[Fact]
public void LspHelper_GetLanguageId_TypeScriptFile_ShouldReturnTypeScript()
{
// Act
var languageId = _helper.GetLanguageId("test.ts");

// Assert
Assert.Equal("typescript", languageId);
}

[Fact]
public void LspHelper_GetLanguageId_JavaScriptFile_ShouldReturnJavaScript()
{
// Act
var languageId = _helper.GetLanguageId("test.js");

// Assert
Assert.Equal("javascript", languageId);
}

[Fact]
public void LspHelper_GetLanguageId_PythonFile_ShouldReturnPython()
{
// Act
var languageId = _helper.GetLanguageId("test.py");

// Assert
Assert.Equal("python", languageId);
}

[Fact]
public void LspHelper_GetLanguageId_UnknownFile_ShouldReturnPlainText()
{
// Act
var languageId = _helper.GetLanguageId("test.unknown");

// Assert
Assert.Equal("plaintext", languageId);
}

[Fact]
public void LspHelper_GetDefaultServerPath_CSharpFile_ShouldReturnCSharpServerOrNull()
{
// Act
var serverPath = _helper.GetDefaultServerPath(_testFilePath);

// Assert
// Should return null if no server is installed, or a path if found
// This test just verifies the method doesn't throw
Assert.True(serverPath == null || serverPath.Length > 0);
}

[Fact]
public void LspPosition_Constructor_ShouldSetCorrectValues()
{
// Act
var position = new LspPosition(10, 5);

// Assert
Assert.Equal(10, position.Line);
Assert.Equal(5, position.Character);
}

[Fact]
public void LspRange_Constructor_ShouldSetCorrectValues()
{
// Arrange
var start = new LspPosition(1, 0);
var end = new LspPosition(2, 10);

// Act
var range = new LspRange(start, end);

// Assert
Assert.Equal(start, range.Start);
Assert.Equal(end, range.End);
Assert.Equal(1, range.Start.Line);
Assert.Equal(0, range.Start.Character);
Assert.Equal(2, range.End.Line);
Assert.Equal(10, range.End.Character);
}

[Fact]
public async Task LspHelper_CreateLspClientAsync_WithInvalidServer_ShouldReturnNull()
{
// Arrange
var settings = new LspSettings
{
FilePath = _testFilePath,
ServerPath = "nonexistent-server"
};

// Act
var client = await _helper.CreateLspClientAsync(settings);

// Assert
Assert.Null(client);
}

[Fact]
public void LspSettings_DefaultValues_ShouldBeCorrect()
{
// Act
var settings = new LspSettings();

// Assert
Assert.Equal(0, settings.LineNumber);
Assert.Equal(0, settings.ColumnNumber);
Assert.Null(settings.ServerPath);
Assert.Null(settings.ServerArgs);
Assert.Null(settings.Query);
}
}
3 changes: 3 additions & 0 deletions Cast.Tool/Cast.Tool.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.9.2" />
<PackageReference Include="Spectre.Console.Cli" Version="0.50.0" />
<PackageReference Include="StreamJsonRpc" Version="2.18.48" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>

</Project>
126 changes: 126 additions & 0 deletions Cast.Tool/Commands/LspCodeActionsCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using Cast.Tool.Core;
using Spectre.Console;
using Spectre.Console.Cli;
using System.ComponentModel;
using Newtonsoft.Json.Linq;

namespace Cast.Tool.Commands;

public class LspCodeActionsCommand : Command<LspCodeActionsCommand.Settings>
{
public class Settings : LspSettings
{
[CommandOption("--end-line")]
[Description("End line number (0-based) for range selection")]
public int? EndLineNumber { get; init; }

[CommandOption("--end-column")]
[Description("End column number (0-based) for range selection")]
public int? EndColumnNumber { get; init; }
}

public override int Execute(CommandContext context, Settings settings)
{
return ExecuteAsync(context, settings).GetAwaiter().GetResult();
}

public async Task<int> ExecuteAsync(CommandContext context, Settings settings)
{
var helper = new LspHelper();
try
{
helper.ValidateInputs(settings);

using var client = await helper.CreateLspClientAsync(settings);
if (client == null) return 1;

var documentUri = helper.CreateDocumentUri(settings.FilePath);
var languageId = helper.GetLanguageId(settings.FilePath);
var fileContent = await File.ReadAllTextAsync(settings.FilePath);

// Open the document
await client.DidOpenAsync(documentUri, languageId, fileContent);

// Give the server time to analyze the document
await Task.Delay(1000);

// Create range for code actions
var startPosition = helper.CreatePosition(settings);
var endPosition = settings.EndLineNumber.HasValue || settings.EndColumnNumber.HasValue
? new LspPosition(settings.EndLineNumber ?? settings.LineNumber, settings.EndColumnNumber ?? settings.ColumnNumber)
: startPosition;

var range = new LspRange(startPosition, endPosition);

// Request code actions
var result = await client.GetCodeActionsAsync(documentUri, range);

if (result == null)
{
AnsiConsole.WriteLine("[yellow]No code actions available for the specified range.[/]");
return 0;
}

if (result is JArray actionsArray)
{
AnsiConsole.WriteLine($"[green]Available code actions ({actionsArray.Count}):[/]");

var actionIndex = 1;
foreach (var action in actionsArray)
{
var title = action["title"]?.ToString();
var kind = action["kind"]?.ToString();
var command = action["command"];
var edit = action["edit"];
var diagnostics = action["diagnostics"] as JArray;

if (!string.IsNullOrEmpty(title))
{
AnsiConsole.WriteLine($"{actionIndex}. [cyan]{title}[/]");

if (!string.IsNullOrEmpty(kind))
{
AnsiConsole.WriteLine($" Kind: {kind}");
}

if (diagnostics != null && diagnostics.Count > 0)
{
AnsiConsole.WriteLine($" Fixes {diagnostics.Count} diagnostic(s)");
}

if (edit?["changes"] != null)
{
var changes = edit["changes"] as JObject;
if (changes != null)
{
AnsiConsole.WriteLine($" Affects {changes.Count} file(s)");
}
}

if (command != null)
{
var commandName = command["command"]?.ToString();
if (!string.IsNullOrEmpty(commandName))
{
AnsiConsole.WriteLine($" Command: {commandName}");
}
}
}

actionIndex++;
AnsiConsole.WriteLine();
}
}

// Close the document
await client.DidCloseAsync(documentUri);

return 0;
}
catch (Exception ex)
{
AnsiConsole.WriteLine($"[red]Error: {ex.Message}[/]");
return 1;
}
}
}
Loading