Skip to content

Commit ad24c91

Browse files
authored
Merge pull request #196 from TylerLeonhardt/workaround-unicode-characters-uri-bug
Workaround Unicode characters in URIs .NET bug
2 parents 09fc4f3 + 025a8d0 commit ad24c91

19 files changed

+379
-10
lines changed

src/JsonRpc/RequestRouterBase.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,10 @@ public virtual async Task<ErrorResponse> RouteRequest(TDescriptor descriptor, Re
9999
// TODO: Try / catch for Internal Error
100100
try
101101
{
102-
if (descriptor == default)
102+
// To avoid boxing, the best way to compare generics for equality is with EqualityComparer<T>.Default.
103+
// This respects IEquatable<T> (without boxing) as well as object.Equals, and handles all the Nullable<T> "lifted" nuances.
104+
// https://stackoverflow.com/a/864860
105+
if (EqualityComparer<TDescriptor>.Default.Equals(descriptor, default))
103106
{
104107
_logger.LogDebug("descriptor not found for Request ({Id}) {Method}", request.Id, request.Method);
105108
return new MethodNotFound(request.Id, request.Method);

src/Protocol/Models/BooleanOr.cs

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Collections.Generic;
2+
13
namespace OmniSharp.Extensions.LanguageServer.Protocol.Models
24
{
35
public class BooleanOr<T>
@@ -15,7 +17,11 @@ public BooleanOr(bool value)
1517
_bool = value;
1618
}
1719

18-
public bool IsValue => this._value != default;
20+
// To avoid boxing, the best way to compare generics for equality is with EqualityComparer<T>.Default.
21+
// This respects IEquatable<T> (without boxing) as well as object.Equals, and handles all the Nullable<T> "lifted" nuances.
22+
// https://stackoverflow.com/a/864860
23+
public bool IsValue => !EqualityComparer<T>.Default.Equals(_value, default);
24+
1925
public T Value
2026
{
2127
get { return this._value; }

src/Protocol/Models/ConfigurationItem.cs

+3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
using System;
2+
using Newtonsoft.Json;
23
using OmniSharp.Extensions.LanguageServer.Protocol.Serialization;
4+
using OmniSharp.Extensions.LanguageServer.Protocol.Serialization.Converters;
35

46
namespace OmniSharp.Extensions.LanguageServer.Protocol.Models
57
{
68
public class ConfigurationItem
79
{
810
[Optional]
11+
[JsonConverter(typeof(AbsoluteUriConverter))]
912
public Uri ScopeUri { get; set; }
1013
[Optional]
1114
public string Section { get; set; }

src/Protocol/Models/DocumentLink.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
using MediatR;
33
using Newtonsoft.Json;
44
using Newtonsoft.Json.Linq;
5-
using Newtonsoft.Json.Serialization;
65
using OmniSharp.Extensions.LanguageServer.Protocol.Serialization;
6+
using OmniSharp.Extensions.LanguageServer.Protocol.Serialization.Converters;
77

88
namespace OmniSharp.Extensions.LanguageServer.Protocol.Models
99
{
@@ -23,6 +23,7 @@ public class DocumentLink : ICanBeResolved, IRequest<DocumentLink>
2323
/// The uri this link points to. If missing a resolve request is sent later.
2424
/// </summary>
2525
[Optional]
26+
[JsonConverter(typeof(AbsoluteUriConverter))]
2627
public Uri Target { get; set; }
2728

2829
/// </summary>

src/Protocol/Models/InitializeParams.cs

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Newtonsoft.Json.Serialization;
55
using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities;
66
using OmniSharp.Extensions.LanguageServer.Protocol.Serialization;
7+
using OmniSharp.Extensions.LanguageServer.Protocol.Serialization.Converters;
78

89
namespace OmniSharp.Extensions.LanguageServer.Protocol.Models
910
{
@@ -34,6 +35,7 @@ public string RootPath
3435
/// folder is open. If both `rootPath` and `rootUri` are set
3536
/// `rootUri` wins.
3637
/// </summary>
38+
[JsonConverter(typeof(AbsoluteUriConverter))]
3739
public Uri RootUri { get; set; }
3840

3941
/// <summary>

src/Protocol/Models/LocationLink.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
2+
using Newtonsoft.Json;
23
using OmniSharp.Extensions.LanguageServer.Protocol.Serialization;
4+
using OmniSharp.Extensions.LanguageServer.Protocol.Serialization.Converters;
35

46
namespace OmniSharp.Extensions.LanguageServer.Protocol.Models
57
{
@@ -17,6 +19,7 @@ public class LocationLink
1719
/// <summary>
1820
/// The target resource identifier of this link.
1921
/// </summary>
22+
[JsonConverter(typeof(AbsoluteUriConverter))]
2023
public Uri TargetUri { get; set; }
2124

2225
/// <summary>
@@ -32,4 +35,4 @@ public class LocationLink
3235
/// </summary>
3336
public Range TargetSelectionRange { get; set; }
3437
}
35-
}
38+
}

src/Protocol/Models/WorkspaceEdit.cs

+3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
22
using System.Collections.Generic;
3+
using Newtonsoft.Json;
34
using Newtonsoft.Json.Serialization;
45
using OmniSharp.Extensions.LanguageServer.Protocol.Serialization;
6+
using OmniSharp.Extensions.LanguageServer.Protocol.Serialization.Converters;
57

68
namespace OmniSharp.Extensions.LanguageServer.Protocol.Models
79
{
@@ -11,6 +13,7 @@ public class WorkspaceEdit
1113
/// Holds changes to existing resources.
1214
/// </summary>
1315
[Optional]
16+
[JsonConverter(typeof(AbsoluteUriKeyConverter<IEnumerable<TextEdit>>))]
1417
public IDictionary<Uri, IEnumerable<TextEdit>> Changes { get; set; }
1518
/// <summary>
1619
/// An array of `TextDocumentEdit`s to express changes to n different text documents

src/Protocol/Serialization/Converters/AbsoluteUriConverter.cs

+28-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33
// #see https://github.com/NuGet/NuGet.Server
44
using System;
5+
using System.Text;
56
using Newtonsoft.Json;
67

78
namespace OmniSharp.Extensions.LanguageServer.Protocol.Serialization.Converters
@@ -41,17 +42,38 @@ public override void WriteJson(JsonWriter writer, Uri value, JsonSerializer seri
4142
return;
4243
}
4344

44-
if (!(value is Uri uriValue))
45+
if (!value.IsAbsoluteUri)
4546
{
46-
throw new JsonSerializationException("The value must be a URI.");
47+
throw new JsonSerializationException("The URI value must be an absolute Uri. Relative URI instances are not allowed.");
4748
}
4849

49-
if (!uriValue.IsAbsoluteUri)
50+
if (value.IsFile)
5051
{
51-
throw new JsonSerializationException("The URI value must be an absolute Uri. Relative URI instances are not allowed.");
52-
}
52+
// First add the file scheme and ://
53+
var builder = new StringBuilder(value.Scheme)
54+
.Append("://");
55+
56+
// UNC file paths use the Host
57+
if (value.HostNameType != UriHostNameType.Basic)
58+
{
59+
builder.Append(value.Host);
60+
}
5361

54-
writer.WriteValue(uriValue.ToString());
62+
// Paths that start with a drive letter don't have a slash in the PathAndQuery
63+
// but they need it in the final result.
64+
if (value.PathAndQuery[0] != '/')
65+
{
66+
builder.Append('/');
67+
}
68+
69+
// Lastly add the remaining parts of the URL
70+
builder.Append(value.PathAndQuery);
71+
writer.WriteValue(builder.ToString());
72+
}
73+
else
74+
{
75+
writer.WriteValue(value.AbsoluteUri);
76+
}
5577
}
5678
}
5779
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using Newtonsoft.Json;
5+
6+
namespace OmniSharp.Extensions.LanguageServer.Protocol.Serialization.Converters
7+
{
8+
class AbsoluteUriKeyConverter<TValue> : JsonConverter<Dictionary<Uri, TValue>>
9+
{
10+
public override Dictionary<Uri, TValue> ReadJson(
11+
JsonReader reader,
12+
Type objectType,
13+
Dictionary<Uri, TValue> existingValue,
14+
bool hasExistingValue,
15+
JsonSerializer serializer)
16+
{
17+
if (reader.TokenType != JsonToken.StartObject)
18+
{
19+
throw new JsonException();
20+
}
21+
22+
var dictionary = new Dictionary<Uri, TValue>();
23+
24+
while (reader.Read())
25+
{
26+
if (reader.TokenType == JsonToken.EndObject)
27+
{
28+
return dictionary;
29+
}
30+
31+
// Get the key.
32+
if (reader.TokenType != JsonToken.PropertyName)
33+
{
34+
throw new JsonSerializationException($"The token type must be a property name. Given {reader.TokenType.ToString()}");
35+
}
36+
37+
// Get the stringified Uri.
38+
var key = new Uri((string)reader.Value, UriKind.RelativeOrAbsolute);
39+
if (!key.IsAbsoluteUri)
40+
{
41+
throw new JsonSerializationException($"The Uri must be absolute. Given: {reader.Value}");
42+
}
43+
44+
// Get the value.
45+
reader.Read();
46+
var value = serializer.Deserialize<TValue>(reader);
47+
48+
// Add to dictionary.
49+
dictionary.Add(key, value);
50+
}
51+
52+
throw new JsonException();
53+
}
54+
55+
public override void WriteJson(
56+
JsonWriter writer,
57+
Dictionary<Uri, TValue> value,
58+
JsonSerializer serializer)
59+
{
60+
writer.WriteStartObject();
61+
62+
foreach (var kvp in value)
63+
{
64+
var uri = kvp.Key;
65+
if (!uri.IsAbsoluteUri)
66+
{
67+
throw new JsonSerializationException("The URI value must be an absolute Uri. Relative URI instances are not allowed.");
68+
}
69+
70+
if (uri.IsFile)
71+
{
72+
// First add the file scheme and ://
73+
var builder = new StringBuilder(uri.Scheme)
74+
.Append("://");
75+
76+
// UNC file paths use the Host
77+
if (uri.HostNameType != UriHostNameType.Basic)
78+
{
79+
builder.Append(uri.Host);
80+
}
81+
82+
// Paths that start with a drive letter don't have a slash in the PathAndQuery
83+
// but they need it in the final result.
84+
if (uri.PathAndQuery[0] != '/')
85+
{
86+
builder.Append('/');
87+
}
88+
89+
// Lastly add the remaining parts of the URL
90+
builder.Append(uri.PathAndQuery);
91+
writer.WritePropertyName(builder.ToString());
92+
}
93+
else
94+
{
95+
writer.WritePropertyName(uri.AbsoluteUri);
96+
}
97+
98+
serializer.Serialize(writer, kvp.Value);
99+
}
100+
101+
writer.WriteEndObject();
102+
}
103+
}
104+
}

test/Lsp.Tests/Models/ApplyWorkspaceEditParamsTests.cs

+32
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,38 @@ public void SimpleTest(string expected)
4343
deresult.Should().BeEquivalentTo(model);
4444
}
4545

46+
[Theory, JsonFixture]
47+
public void NonStandardCharactersTest(string expected)
48+
{
49+
var model = new ApplyWorkspaceEditParams()
50+
{
51+
Edit = new WorkspaceEdit()
52+
{
53+
Changes = new Dictionary<Uri, IEnumerable<TextEdit>>() {
54+
{
55+
// Mörkö
56+
new Uri("file:///abc/123/M%C3%B6rk%C3%B6.cs"), new [] {
57+
new TextEdit() {
58+
NewText = "new text",
59+
Range = new Range(new Position(1, 1), new Position(2,2))
60+
},
61+
new TextEdit() {
62+
NewText = "new text2",
63+
Range = new Range(new Position(3, 3), new Position(4,4))
64+
}
65+
}
66+
}
67+
}
68+
}
69+
};
70+
var result = Fixture.SerializeObject(model);
71+
72+
result.Should().Be(expected);
73+
74+
var deresult = new Serializer(ClientVersion.Lsp3).DeserializeObject<ApplyWorkspaceEditParams>(expected);
75+
deresult.Should().BeEquivalentTo(model);
76+
}
77+
4678
[Theory, JsonFixture]
4779
public void DocumentChangesTest(string expected)
4880
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"edit": {
3+
"changes": {
4+
"file:///abc/123/M%C3%B6rk%C3%B6.cs": [
5+
{
6+
"range": {
7+
"start": {
8+
"line": 1,
9+
"character": 1
10+
},
11+
"end": {
12+
"line": 2,
13+
"character": 2
14+
}
15+
},
16+
"newText": "new text"
17+
},
18+
{
19+
"range": {
20+
"start": {
21+
"line": 3,
22+
"character": 3
23+
},
24+
"end": {
25+
"line": 4,
26+
"character": 4
27+
}
28+
},
29+
"newText": "new text2"
30+
}
31+
]
32+
}
33+
}
34+
}

test/Lsp.Tests/Models/CodeActionParamsTests.cs

+28
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,33 @@ public void SimpleTest(string expected)
3737
var deresult = new Serializer(ClientVersion.Lsp3).DeserializeObject<CodeActionParams>(expected);
3838
deresult.Should().BeEquivalentTo(model);
3939
}
40+
41+
[Theory, JsonFixture]
42+
public void NonStandardCharactersTest(string expected)
43+
{
44+
var model = new CodeActionParams() {
45+
Context = new CodeActionContext() {
46+
Diagnostics = new[] { new Diagnostic() {
47+
Code = new DiagnosticCode("abcd"),
48+
Message = "message",
49+
Range = new Range(new Position(1, 1), new Position(2,2)),
50+
Severity = DiagnosticSeverity.Error,
51+
Source = "csharp"
52+
} }
53+
54+
},
55+
Range = new Range(new Position(1, 1), new Position(2, 2)),
56+
TextDocument = new TextDocumentIdentifier() {
57+
// 树 - Chinese for tree
58+
Uri = new Uri("file:///test/123/%E6%A0%91.cs")
59+
}
60+
};
61+
var result = Fixture.SerializeObject(model);
62+
63+
result.Should().Be(expected);
64+
65+
var deresult = new Serializer(ClientVersion.Lsp3).DeserializeObject<CodeActionParams>(expected);
66+
deresult.Should().BeEquivalentTo(model);
67+
}
4068
}
4169
}

0 commit comments

Comments
 (0)