Skip to content

Commit

Permalink
Add code analysis for .NET projects
Browse files Browse the repository at this point in the history
  • Loading branch information
Aminator committed Mar 16, 2020
1 parent e9f67b8 commit 708353a
Show file tree
Hide file tree
Showing 105 changed files with 2,041 additions and 357 deletions.
12 changes: 12 additions & 0 deletions DirectX12GameEngine.Core/Matrix4x4Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Numerics;

namespace DirectX12GameEngine.Core
{
public static class Matrix4x4Extensions
{
public static void Deconstruct(this in Matrix4x4 matrix, out Vector3 scale, out Quaternion rotation, out Vector3 translation)
{
Matrix4x4.Decompose(matrix, out scale, out rotation, out translation);
}
}
}
302 changes: 287 additions & 15 deletions DirectX12GameEngine.Editor.ViewModels/CodeEditorViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,48 +1,320 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DirectX12GameEngine.Mvvm;
using DirectX12GameEngine.Mvvm.Commanding;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Text;
using Nito.AsyncEx;
using Windows.Storage;
using Windows.UI;

namespace DirectX12GameEngine.Editor.ViewModels
{
public class CodeEditorViewModel : ViewModelBase, IClosable
{
private string? text;
private readonly AsyncLock changeLock = new AsyncLock();

public CodeEditorViewModel()
private readonly Stack<Document> documentStack = new Stack<Document>();

private int tabSize = 4;
private Encoding? encoding;
private NewLineMode newLineMode = NewLineMode.Crlf;
private string? currentText;

public CodeEditorViewModel(IStorageFile file, SolutionLoaderViewModel solutionLoader)
{
SaveCommand = new RelayCommand(async () => await SaveAsync());
File = file;
SolutionLoader = solutionLoader;
SolutionLoader.Workspace.WorkspaceChanged += OnWorkspaceChanged;

Solution solution = SolutionLoader.Workspace.CurrentSolution;
Document? document = solution.GetDocument(GetDocumentId(solution));

if (document != null)
{
documentStack.Push(document);
}
}

public CodeEditorViewModel(StorageFileViewModel file) : this()
private async void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
{
File = file;
using (await changeLock.LockAsync())
{
DocumentId? id = GetDocumentId(e.NewSolution);
Document? document = e.NewSolution.GetDocument(id);

if (document != null)
{
bool isClassificationChanged = e.DocumentId != id || e.Kind != WorkspaceChangeKind.DocumentChanged;
bool isTextChanged = e.DocumentId != id && e.Kind != WorkspaceChangeKind.DocumentChanged;

if (isClassificationChanged || isTextChanged)
{
documentStack.Push(document);

if (isTextChanged)
{
SourceText sourceText = await document.GetTextAsync();
Encoding = sourceText.Encoding;
}

DocumentChanged?.Invoke(sender, new DocumentChangedEventArgs(isClassificationChanged, isTextChanged));
}
}
}
}

public StorageFileViewModel? File { get; set; }
public event EventHandler<DocumentChangedEventArgs>? DocumentChanged;

public SolutionLoaderViewModel SolutionLoader { get; }

public string? Text
public bool CanClose => false;

public IStorageFile File { get; set; }

public ObservableCollection<Diagnostic> CurrentDiagnostics { get; } = new ObservableCollection<Diagnostic>();

public Document? CurrentDocument => documentStack.Count > 0 ? documentStack.Peek() : null;

public string? CurrentText
{
get => text;
set => Set(ref text, value);
get => currentText;
set => Set(ref currentText, value);
}

public bool CanClose => false;
public int TabSize
{
get => tabSize;
set => Set(ref tabSize, value);
}

public RelayCommand SaveCommand { get; }
public Encoding? Encoding
{
get => encoding;
set => Set(ref encoding, value);
}

public NewLineMode NewLineMode
{
get => newLineMode;
set => Set(ref newLineMode, value);
}

public async Task SaveAsync()
{
if (File != null)
if (documentStack.Count == 0)
{
await FileIO.WriteTextAsync(File.Model, Text);
using Stream stream = await File.OpenStreamForWriteAsync();
using StreamWriter writer = new StreamWriter(stream, Encoding);
await writer.WriteAsync(CurrentText);
}
}

public Task<bool> TryCloseAsync()
{
return Task.FromResult(false);
return Task.FromResult(true);
}

public async Task<string> LoadTextAsync()
{
if (documentStack.Count > 0)
{
Document document = documentStack.Peek();
SourceText sourceText = await document.GetTextAsync();

CurrentText = sourceText.ToString();
Encoding = sourceText.Encoding;
}
else
{
using Stream stream = await File.OpenStreamForReadAsync();
using StreamReader reader = new StreamReader(stream, Encoding.Default);

CurrentText = await reader.ReadToEndAsync();
Encoding = reader.CurrentEncoding;
}

return CurrentText;
}

public async Task ApplyChangesAsync(TextChange change)
{
if (!(change.Span.IsEmpty && string.IsNullOrEmpty(change.NewText)))
{
using (await changeLock.LockAsync())
{
if (documentStack.Count > 0)
{
Document document = documentStack.Peek();

SourceText text = await document.GetTextAsync();
SourceText newText = text.WithChanges(change);

Document newDocument = document.WithText(newText);

if (SolutionLoader.Workspace.TryApplyChanges(newDocument.Project.Solution))
{
Solution solution = SolutionLoader.Workspace.CurrentSolution;

DocumentId? id = GetDocumentId(solution);
documentStack.Push(solution.GetDocument(id)!);

DocumentChanged?.Invoke(SolutionLoader.Workspace, new DocumentChangedEventArgs(true, false));
}
}
else
{
using Stream stream = await File.OpenStreamForWriteAsync();
using StreamWriter writer = new StreamWriter(stream, Encoding);
await writer.WriteAsync(CurrentText);
}
}
}
}

public async Task GetCompletionListAsync()
{

await Task.CompletedTask;
}

public async Task<IEnumerable<Diagnostic>> GetDiagnosticsAsync(int position)
{
var diagnostics = await GetDiagnosticsAsync();

return diagnostics.Where(d => d.Location.SourceSpan.Contains(position));
}

public async Task<IEnumerable<Diagnostic>> GetDiagnosticsAsync()
{
if (documentStack.Count > 0)
{
Document document = documentStack.Peek();

SyntaxTree? syntaxTree = await document.GetSyntaxTreeAsync();

if (syntaxTree != null)
{
var diagnostics = syntaxTree.GetDiagnostics();

CurrentDiagnostics.Clear();

foreach (Diagnostic diagnostic in diagnostics)
{
CurrentDiagnostics.Add(diagnostic);
}

return diagnostics;
}
}

return Enumerable.Empty<Diagnostic>();
}

public async Task<IEnumerable<ClassifiedSpan>> GetChangedClassifiedSpansAsync()
{
if (documentStack.Count >= 2)
{
Document[] documents = documentStack.ToArray();

Document newDocument = documents[0];
Document oldDocument = documents[1];

SourceText newText = await newDocument.GetTextAsync();
var newClassifiedSpans = await Classifier.GetClassifiedSpansAsync(newDocument, new TextSpan(0, newText.Length));

SourceText oldText = await oldDocument.GetTextAsync();
var oldClassifiedSpans = await Classifier.GetClassifiedSpansAsync(oldDocument, new TextSpan(0, oldText.Length));

var textChanges = newText.GetChangeRanges(oldText);

return GetChangedClassifiedSpans(textChanges.FirstOrDefault(), oldClassifiedSpans, newClassifiedSpans);
}

return await GetClassifiedSpansAsync();
}

public async Task<IEnumerable<ClassifiedSpan>> GetClassifiedSpansAsync()
{
if (documentStack.Count > 0)
{
Document document = documentStack.Peek();
SourceText text = await document.GetTextAsync();

return await Classifier.GetClassifiedSpansAsync(document, new TextSpan(0, text.Length));
}

return Enumerable.Empty<ClassifiedSpan>();
}

private static IEnumerable<ClassifiedSpan> GetChangedClassifiedSpans(TextChangeRange textChange, IEnumerable<ClassifiedSpan> oldClassifiedSpans, IEnumerable<ClassifiedSpan> newClassifiedSpans)
{
int offset = textChange.NewLength - textChange.Span.Length;

foreach (ClassifiedSpan span in newClassifiedSpans)
{
ClassifiedSpan offsettedSpan = span;

if (span.TextSpan.Start > textChange.Span.End)
{
offsettedSpan = new ClassifiedSpan(span.ClassificationType, new TextSpan(span.TextSpan.Start - offset, span.TextSpan.Length));
}

if (!oldClassifiedSpans.Contains(offsettedSpan))
{
yield return span;
}
}
}

private DocumentId? GetDocumentId(Solution solution)
{
if (solution.FilePath is null) return null;

string relativeDocumentFilePath = StorageExtensions.GetRelativePath(SolutionLoader.RootFolder!.Path, File.Path);
string documentFilePath = Path.Combine(Path.GetDirectoryName(solution.FilePath), relativeDocumentFilePath);

return solution.GetDocumentIdsWithFilePath(documentFilePath).FirstOrDefault();
}
}

public class CodeTextFormat
{
public CodeTextFormat()
{
}

public CodeTextFormat(Color foregroundColor)
{
ForegroundColor = foregroundColor;
}

public Color ForegroundColor { get; set; }
}

public class DocumentChangedEventArgs : EventArgs
{
public DocumentChangedEventArgs(bool isClassificationChanged, bool isTextChanged)
{
IsClassificationChanged = isClassificationChanged;
IsTextChanged = isTextChanged;
}

public bool IsClassificationChanged { get; }

public bool IsTextChanged { get; }
}

public enum NewLineMode
{
Cr,
Lf,
Crlf
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,29 @@
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NoWarn>NU1701</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Build" Version="16.4.0" ExcludeAssets="Compile" PrivateAssets="all" />
<PackageReference Include="Microsoft.Build.Framework" Version="16.4.0" />
<PackageReference Include="Microsoft.CodeAnalysis" Version="3.4.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="3.4.0" ExcludeAssets="Compile" PrivateAssets="all" />
<PackageReference Include="NuGet.Build.Tasks" Version="5.4.0" />
</ItemGroup>

<ItemGroup>
<Reference Include="Microsoft.Build">
<HintPath>$(NuGetPackageRoot)microsoft.build\16.4.0\lib\netcoreapp2.1\Microsoft.Build.dll</HintPath>
</Reference>
<Reference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild">
<HintPath>$(NuGetPackageRoot)microsoft.codeanalysis.workspaces.msbuild\3.4.0\lib\net472\Microsoft.CodeAnalysis.Workspaces.MSBuild.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Loader">
<HintPath>..\Libs\System.Runtime.Loader.dll</HintPath>
</Reference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DirectX12GameEngine.Engine\DirectX12GameEngine.Engine.csproj" />
<ProjectReference Include="..\DirectX12GameEngine.Mvvm\DirectX12GameEngine.Mvvm.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Windows.Storage;

#nullable enable

namespace DirectX12GameEngine.Editor.ViewModels.Factories
{
public class CodeEditorViewFactory : IEditorViewFactory
{
public Task<object?> CreateAsync(StorageFileViewModel item)
public Task<object?> CreateAsync(IStorageFile item, IServiceProvider services)
{
return Task.FromResult<object?>(new CodeEditorViewModel(item));
return Task.FromResult<object?>(new CodeEditorViewModel(item, services.GetRequiredService<SolutionLoaderViewModel>()));
}
}
}
Loading

0 comments on commit 708353a

Please sign in to comment.