Skip to content

Commit

Permalink
Various improvements around completion and debugging.
Browse files Browse the repository at this point in the history
  • Loading branch information
ashmind committed Sep 11, 2016
1 parent e5875f0 commit 54854c5
Show file tree
Hide file tree
Showing 18 changed files with 450 additions and 165 deletions.
1 change: 1 addition & 0 deletions MirrorSharp.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/Browsers/Browsers/@EntryValue">C27+,E12+,FF21+,IE11+,S8+</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ACCESSOR_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ACCESSOR_OWNER_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ANONYMOUS_METHOD_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
Expand Down
8 changes: 2 additions & 6 deletions src/MirrorSharp.Demo/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
Expand All @@ -23,7 +19,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF
app.UseDefaultFiles()
.UseStaticFiles()
.UseWebSockets()
.UseMirrorSharp();
.UseMirrorSharp(new MirrorSharpOptions { SendDebugCompareMessages = true });
}
}
}
1 change: 1 addition & 0 deletions src/MirrorSharp.Demo/wwwroot/css/icons/method.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 13 additions & 20 deletions src/MirrorSharp.Demo/wwwroot/css/mirrorsharp.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,22 @@
font-size: inherit;
}

.mirrorsharp-hint-class::before,
.mirrorsharp-hint-field::before,
.mirrorsharp-hint-keyword::before,
.mirrorsharp-hint-parameter::before {
.mirrorsharp-hint {
display: flex;
padding-left: 0;
}

.mirrorsharp-hint::before {
content: '';
flex-shrink: 0;
margin-right: 5px;
display: inline-block;
width: 16px;
height: 16px;
background-color: #fff;
}

.mirrorsharp-hint-class::before {
background-image: url('icons/class.svg');
}

.mirrorsharp-hint-field::before {
background-image: url('icons/field.svg');
}

.mirrorsharp-hint-keyword::before {
background-image: url('icons/keyword.svg');
}

.mirrorsharp-hint-parameter::before {
background-image: url('icons/variable.svg');
}
.mirrorsharp-hint-class::before { background-image: url('icons/class.svg'); }
.mirrorsharp-hint-field::before { background-image: url('icons/field.svg'); }
.mirrorsharp-hint-keyword::before { background-image: url('icons/keyword.svg'); }
.mirrorsharp-hint-method::before { background-image: url('icons/method.svg'); }
.mirrorsharp-hint-parameter::before { background-image: url('icons/variable.svg'); }
152 changes: 84 additions & 68 deletions src/MirrorSharp.Demo/wwwroot/js/mirrorsharp.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@
function Connection(socket) {
const openPromise = new Promise(function(resolve) {
socket.addEventListener('open', function (e) {
//console.debug("[open]");
resolve();
});
});

function sendWhenOpen(command) {
openPromise.then(function() { socket.send(command); });
openPromise.then(function () {
//console.debug("[=>]", command);
socket.send(command);
});
}

this.sendReplaceText = function (start, length, newText, cursorIndexAfter) {
return sendWhenOpen('R' + start + ':' + length + ':' + cursorIndexAfter + ':' + newText);
this.sendReplaceText = function (isLastOrOnly, start, length, newText, cursorIndexAfter) {
const command = isLastOrOnly ? 'R' : 'P';
return sendWhenOpen(command + start + ':' + length + ':' + cursorIndexAfter + ':' + newText);
}

this.sendMoveCursor = function(cursorIndex) {
Expand All @@ -36,115 +41,126 @@

this.onMessage = function(handler) {
socket.addEventListener('message', function (e) {
//console.debug("[<=]", e.data);
const message = JSON.parse(e.data);
handler(message);
});
}
}

function getCursorIndex(cm) {
return cm.indexFromPos(cm.getCursor());
}

function showCompletions(cm, completions, connection) {
const indexInListKey = '$mirrorsharp-indexInList';
var commit = function(cm, data, item) {
connection.sendCommitCompletion(item[indexInListKey]);
}

var hintResult = {
from: cm.posFromIndex(completions.span.start),
list: completions.list.map(function (c, index) {
const item = {
displayText: c.displayText,
className: c.tags.map(function (t) { return 'mirrorsharp-hint-' + t.toLowerCase(); }).join(' '),
hint: commit
};
item[indexInListKey] = index;
if (c.span)
item.from = cm.posFromIndex(c.span.start);
return item;
})
}
cm.showHint({
hint: function () { return hintResult; },
completeSingle: false
});
}

return function (textarea, options) {
const connection = new Connection(new WebSocket(options.serviceUrl));

function Editor(textarea, connection, options) {
const cmOptions = options.forCodeMirror || { gutters: [] };
//cmOptions.lint = { async: true, getAnnotations: lint };
cmOptions.gutters.push('CodeMirror-lint-markers');

const cm = CodeMirror.fromTextArea(textarea, cmOptions);

var resetting = false;
function reset(newText, newCursorIndex) {
resetting = true;
if (newCursorIndex == null)
newCursorIndex = cm.indexFromPos(cm.getCursor());

cm.setValue(newText);
cm.setCursor(cm.posFromIndex(newCursorIndex));
resetting = false;
}

(function() {
const value = cm.getValue();
if (value !== '' && value != null)
connection.sendReplaceText(0, 0, value, 0);
(function () {
const text = cm.getValue();
if (text !== '' && text != null)
connection.sendReplaceText(true, 0, 0, text, 0);
})();

const indexKey = '$mirrorsharp-index';
var changePending = false;
cm.on('beforeChange', function (s, change) {
if (resetting)
return;

change.from[indexKey] = cm.indexFromPos(change.from);
change.to[indexKey] = cm.indexFromPos(change.to);
changePending = true;
});

cm.on('cursorActivity', function() {
if (resetting || changePending)
cm.on('cursorActivity', function () {
if (changePending)
return;
const cursorIndex = getCursorIndex(cm);
connection.sendMoveCursor(cursorIndex);
connection.sendMoveCursor(getCursorIndex(cm));
});

cm.on('changes', function (s, changes) {
if (resetting)
return;

const cursorIndex = getCursorIndex(cm);
changePending = false;
for (var change of changes) {
for (var i = 0; i < changes.length; i++) {
const change = changes[i];
const start = change.from[indexKey];
const length = change.to[indexKey] - start;
const text = change.text;
const text = change.text.join('\n');
if (cursorIndex === start + 1 && text.length === 1) {
connection.sendTypeChar(text);
}
else {
connection.sendReplaceText(start, length, text, cursorIndex);
const lastOrOnly = (i === changes.length - 1);
connection.sendReplaceText(lastOrOnly, start, length, text, cursorIndex);
}
}
});

connection.onMessage(function (message) {
switch (message.type) {
case 'reset':
reset(message.text, message.cursor);
case 'changes':
applyChanges(message.changes, message.cursor);
break;

case 'completions':
showCompletions(cm, message.completions, connection);
showCompletions(message.completions);
break;

case 'debug:compare':
debugCompare(message.text, message.cursor);
break;
}
});

function getCursorIndex() {
return cm.indexFromPos(cm.getCursor());
}

function applyChanges(changes) {
for (var change of changes) {
const from = cm.posFromIndex(change.start);
const to = change.length > 0 ? cm.posFromIndex(change.start + change.length) : from;
cm.replaceRange(change.text, from, to);
}
}

function showCompletions(completions) {
const indexInListKey = '$mirrorsharp-indexInList';
var commit = function (cm, data, item) {
connection.sendCommitCompletion(item[indexInListKey]);
}

var hintResult = {
from: cm.posFromIndex(completions.span.start),
list: completions.list.map(function (c, index) {
const item = {
displayText: c.displayText,
className: 'mirrorsharp-hint ' + c.tags.map(function (t) { return 'mirrorsharp-hint-' + t.toLowerCase(); }).join(' '),
hint: commit
};
item[indexInListKey] = index;
if (c.span)
item.from = cm.posFromIndex(c.span.start);
return item;
})
}
cm.showHint({
hint: function () { return hintResult; },
completeSingle: false
});
}

function debugCompare(serverText, serverCursorIndex) {
if (serverText !== undefined) {
const clientText = cm.getValue();
if (clientText !== serverText)
console.error('Client text does not match server text:', { clientText: clientText, serverText: serverText });
}

const clientCursorIndex = getCursorIndex();
if (clientCursorIndex !== serverCursorIndex)
console.error('Client cursor position does not match server position:', { clientPosition: clientCursorIndex, serverPosition: serverCursorIndex });
}
}

return function(textarea, options) {
const connection = new Connection(new WebSocket(options.serviceUrl));
return new Editor(textarea, connection, options);
}
}));
22 changes: 20 additions & 2 deletions src/MirrorSharp.Tests/ConnectionTests.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.Immutable;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Text;
using MirrorSharp.Internal;
using Moq;
using Xunit;

namespace MirrorSharp.Tests {
public class ConnectionTests {
private static readonly CompletionChange NoCompletionChange = CompletionChange.Create(ImmutableArray<TextChange>.Empty);

[Theory]
[InlineData("C1", 1)]
[InlineData("C79", 79)]
Expand Down Expand Up @@ -51,6 +54,21 @@ public async void ReceiveAndProcessAsync_CallsReplaceTextOnSession_AfterReceivin
Mock.Get(sessionMock).Verify(s => s.ReplaceText(expectedStart, expectedLength, expectedText, expectedPosition));
}

[Theory]
[InlineData("S1", 1)]
[InlineData("S79", 79)]
[InlineData("S1234567890", 1234567890)]
public async void ReceiveAndProcessAsync_CallsGetCompletionChangeAsyncOnSession_AfterReceivingCommitCompletionCommand(string command, int expectedItemIndex) {
var socketMock = Mock.Of<WebSocket>();
SetupReceive(socketMock, command);
var sessionMock = Mock.Of<IWorkSession>(
s => s.GetCompletionChangeAsync(It.IsAny<int>()) == Task.FromResult(NoCompletionChange)
);

await new Connection(socketMock, sessionMock).ReceiveAndProcessAsync();
Mock.Get(sessionMock).Verify(s => s.GetCompletionChangeAsync(expectedItemIndex));
}

private static void SetupReceive(WebSocket socket, string command) {
Mock.Get(socket)
.Setup(m => m.ReceiveAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<CancellationToken>()))
Expand Down
19 changes: 19 additions & 0 deletions src/MirrorSharp.Tests/SessionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@

namespace MirrorSharp.Tests {
public class SessionTests {
[Fact]
public async Task TypeChar_InsertsSingleChar() {
var session = SessionFromTextWithCursor("class A| {}");

await session.TypeCharAsync('1');

Assert.Equal("class A1 {}", session.SourceText.ToString());
}

[Fact]
public async Task TypeChar_MovesCursorBySingleChar() {
var session = SessionFromTextWithCursor("class A| {}");
var cursorPosition = session.CursorPosition;

await session.TypeCharAsync('1');

Assert.Equal(cursorPosition + 1, session.CursorPosition);
}

[Fact]
public async Task TypeChar_ProducesExpectedCompletion() {
var session = SessionFromTextWithCursor(@"
Expand Down
6 changes: 4 additions & 2 deletions src/MirrorSharp/AppBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Builder;
using MirrorSharp.Internal;

namespace MirrorSharp {
public static class AppBuilderExtensions {
public static void UseMirrorSharp(this IApplicationBuilder app) {
app.UseMiddleware<Middleware>();
public static void UseMirrorSharp([NotNull] this IApplicationBuilder app, [CanBeNull] MirrorSharpOptions options = null) {
Argument.NotNull(nameof(app), app);
app.UseMiddleware<Middleware>(options);
}
}
}
Loading

0 comments on commit 54854c5

Please sign in to comment.