Skip to content
Merged
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
12 changes: 12 additions & 0 deletions src/Expressif.LanguageServer.Core.Tests/SyntaxServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ public void Parse_ValidExpression_ReturnsSyntaxTree()
});
}

[Test]
public void Parse_UnknownFunctionName_ReturnsNoSyntaxErrors()
{
var result = service.Parse("this-function-does-not-exist(1)");

Assert.Multiple(() =>
{
Assert.That(result.IsValid, Is.True);
Assert.That(result.Errors, Is.Empty);
});
}

[Test]
public void Parse_InvalidExpression_ReturnsSyntaxErrors()
{
Expand Down
49 changes: 49 additions & 0 deletions src/Expressif.LanguageServer.Tests/SyntaxDiagnosticMapperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Expressif.LanguageServer.Diagnostics;
using Expressif.Syntax;
using NUnit.Framework;
using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range;

namespace Expressif.LanguageServer.Tests;

[TestFixture]
public sealed class SyntaxDiagnosticMapperTests
{
[Test]
public void Map_ParserSpan_ProducesExactLspRange()
{
const string source = "add(1";
var diagnostic = SyntaxDiagnosticMapper.Map(
source, new SyntaxError("ERROR", new SourceSpan(4, 1), "1", false));

Assert.Multiple(() =>
{
Assert.That(diagnostic.Range, Is.EqualTo(new Range(0, 4, 0, 5)));
Assert.That(diagnostic.Message, Is.EqualTo("Unexpected syntax '1'."));
Assert.That(diagnostic.Source, Is.EqualTo("expressif"));
});
}

[Test]
public void Map_MultilineUtf8Span_UsesZeroBasedUtf16Position()
{
const string source = "é\nadd(";
var diagnostic = SyntaxDiagnosticMapper.Map(
source, new SyntaxError(")", new SourceSpan(7, 0), "", true));

Assert.Multiple(() =>
{
Assert.That(diagnostic.Range, Is.EqualTo(new Range(1, 4, 1, 4)));
Assert.That(diagnostic.Message, Is.EqualTo("Missing )."));
});
}

[Test]
public void Map_SpanPastEndOfDocument_ClampsToEnd()
{
const string source = "add(";
var diagnostic = SyntaxDiagnosticMapper.Map(
source, new SyntaxError(")", new SourceSpan(100, 2), "", true));

Assert.That(diagnostic.Range, Is.EqualTo(new Range(0, 4, 0, 4)));
}
}
49 changes: 49 additions & 0 deletions src/Expressif.LanguageServer/Diagnostics/SyntaxDiagnosticMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Text;
using Expressif.Syntax;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range;

namespace Expressif.LanguageServer.Diagnostics;

internal static class SyntaxDiagnosticMapper
{
public static Diagnostic Map(string source, SyntaxError error) => new()
{
Range = new Range(
ToPosition(source, error.Span.Start),
ToPosition(source, error.Span.End)),
Severity = DiagnosticSeverity.Error,
Source = "expressif",
Message = CreateMessage(error)
};

private static Position ToPosition(string source, int utf8Offset)
{
var bytes = Encoding.UTF8.GetBytes(source);
var clampedOffset = Math.Clamp(utf8Offset, 0, bytes.Length);
var textOffset = Encoding.UTF8.GetCharCount(bytes, 0, clampedOffset);
var line = 0;
var lineStart = 0;

for (var index = 0; index < textOffset; index++)
{
if (source[index] != '\n')
continue;

line++;
lineStart = index + 1;
}

return new(line, textOffset - lineStart);
}

private static string CreateMessage(SyntaxError error)
{
var node = error.NodeType.Replace('_', ' ');
if (error.IsMissing)
return $"Missing {node}.";
if (!string.IsNullOrWhiteSpace(error.Text))
return $"Unexpected syntax '{error.Text}'.";
return $"Invalid {node} syntax.";
}
}
31 changes: 28 additions & 3 deletions src/Expressif.LanguageServer/Handlers/TextDocumentSyncHandler.cs
Original file line number Diff line number Diff line change
@@ -1,34 +1,47 @@
using Expressif.LanguageServer.Core.Documents;
using Expressif.LanguageServer.Diagnostics;
using MediatR;
using OmniSharp.Extensions.LanguageServer.Protocol;
using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities;
using OmniSharp.Extensions.LanguageServer.Protocol.Document;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
using OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities;

namespace Expressif.LanguageServer.Handlers;

public sealed class TextDocumentSyncHandler(IDocumentStore documents) : TextDocumentSyncHandlerBase
public sealed class TextDocumentSyncHandler(IDocumentStore documents, ILanguageServerFacade server) : TextDocumentSyncHandlerBase
{
public override TextDocumentAttributes GetTextDocumentAttributes(DocumentUri uri) => new(uri, "expressif");

public override Task<Unit> Handle(DidOpenTextDocumentParams request, CancellationToken cancellationToken)
{
documents.Open(request.TextDocument.Uri.ToUri(), request.TextDocument.Text, request.TextDocument.Version);
var document = documents.Open(
request.TextDocument.Uri.ToUri(), request.TextDocument.Text, request.TextDocument.Version);
PublishDiagnostics(request.TextDocument.Uri, document);
return Unit.Task;
}

public override Task<Unit> Handle(DidChangeTextDocumentParams request, CancellationToken cancellationToken)
{
var text = request.ContentChanges.LastOrDefault()?.Text;
if (text is not null)
documents.Change(request.TextDocument.Uri.ToUri(), text, request.TextDocument.Version);
{
var document = documents.Change(
request.TextDocument.Uri.ToUri(), text, request.TextDocument.Version);
PublishDiagnostics(request.TextDocument.Uri, document);
}
return Unit.Task;
}

public override Task<Unit> Handle(DidCloseTextDocumentParams request, CancellationToken cancellationToken)
{
documents.Close(request.TextDocument.Uri.ToUri());
server.TextDocument.PublishDiagnostics(new()
{
Uri = request.TextDocument.Uri,
Diagnostics = []
});
return Unit.Task;
}

Expand All @@ -41,4 +54,16 @@ protected override TextDocumentSyncRegistrationOptions CreateRegistrationOptions
Change = TextDocumentSyncKind.Full,
Save = false
};

private void PublishDiagnostics(DocumentUri uri, DocumentSnapshot document)
{
server.TextDocument.PublishDiagnostics(new()
{
Uri = uri,
Version = document.Version,
Diagnostics = document.SyntaxErrors
.Select(error => SyntaxDiagnosticMapper.Map(document.Text, error))
.ToArray()
});
}
}
3 changes: 3 additions & 0 deletions src/Expressif.LanguageServer/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Expressif.LanguageServer.Tests")]
Loading