Skip to content

Commit

Permalink
Merge pull request #14 from thedemons/explain-problem
Browse files Browse the repository at this point in the history
Add Explain Problem feature
  • Loading branch information
fortenforge authored Dec 11, 2023
2 parents a39e39e + a584520 commit 5377559
Show file tree
Hide file tree
Showing 9 changed files with 443 additions and 16 deletions.
5 changes: 4 additions & 1 deletion CodeiumVS/CodeiumVS.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
Expand Down Expand Up @@ -60,7 +60,10 @@
<Compile Include="Proposal\CodeiumProposalSource.cs" />
<Compile Include="Proposal\CodeiumProposalSourceProvider.cs" />
<Compile Include="Proposal\Mapper.cs" />
<Compile Include="QuickInfo\IntellisenseController.cs" />
<Compile Include="QuickInfo\QuickInfoSource.cs" />
<Compile Include="Utilities\Extensions.cs" />
<Compile Include="Utilities\IntellisenseUtilities.cs" />
<Compile Include="Utilities\ProcessExtensions.cs" />
<Compile Include="Utilities\TextHighlighter.cs" />
<Compile Include="Utilities\CodeAnalyzer.cs" />
Expand Down
38 changes: 37 additions & 1 deletion CodeiumVS/LanguageServer/LanguageServerController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,42 @@ public async Task RefactorFunctionAsync(string prompt, string filePath, Function
await Package.ShowToolWindowAsync(typeof(ChatToolWindow), 0, create: true, Package.DisposalToken);
}

public async Task ExplainProblemAsync(string problemMessage, SnapshotSpan span)
{
ITextSnapshotLine problemLineStart = span.Snapshot.GetLineFromPosition(span.Start);
ITextSnapshotLine problemLineEnd = span.Snapshot.GetLineFromPosition(span.End);

int surroundingLineStart_no = Math.Max(problemLineStart.LineNumber - 10, 0);
int surroundingLineEnd_no = Math.Min(problemLineStart.LineNumber + 10, span.Snapshot.LineCount - 1);

ITextSnapshotLine surroundingLineStart = span.Snapshot.GetLineFromLineNumber(surroundingLineStart_no);
ITextSnapshotLine surroundingLineEnd = span.Snapshot.GetLineFromLineNumber(surroundingLineEnd_no);

var request = WebChatServer.NewRequest();
request.get_chat_message_request.chat_messages[0].intent = new()
{
problem_explain = new()
{
diagnostic_message = problemMessage,
problematic_code = new()
{
raw_source = span.GetText(),
start_line = problemLineStart.LineNumber + 1,
end_line = problemLineEnd.LineNumber + 1,
start_col = span.Start - problemLineStart.Start + 1,
end_col = span.End - problemLineEnd.Start + 1,
},
surrounding_code_snippet = span.Snapshot.GetText(surroundingLineStart.Start, surroundingLineEnd.End - surroundingLineStart.Start),
language = Languages.Mapper.GetLanguage(span.Snapshot.TextBuffer.ContentType).Type,
file_path = span.Snapshot.TextBuffer.GetFileName(),
line_number = problemLineStart.LineNumber + 1,
}
};

request.Send(ws);
await Package.ShowToolWindowAsync(typeof(ChatToolWindow), 0, create: true, Package.DisposalToken);
}

public void Disconnect()
{
if (ws == null) return;
Expand All @@ -248,7 +284,7 @@ static string RandomString(int length)
{
get_chat_message_request = new()
{
context_inclusion_type = Packets.ContextInclusionType.CONTEXT_INCLUSION_TYPE_UNSPECIFIED,
context_inclusion_type = ContextInclusionType.CONTEXT_INCLUSION_TYPE_UNSPECIFIED,
metadata = CodeiumVSPackage.Instance?.LanguageServer.GetMetadata(),
prompt = ""
}
Expand Down
80 changes: 80 additions & 0 deletions CodeiumVS/QuickInfo/IntellisenseController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using CodeiumVS.Utilities;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
using System.Collections.Generic;
using System.ComponentModel.Composition;

namespace CodeiumVS.QuickInfo;

internal class IntellisenseController : TextViewExtension<ITextView, IntellisenseController>, IIntellisenseController
{
private readonly IList<ITextBuffer> _subjectBuffer;

internal IntellisenseController(ITextView hostView, IList<ITextBuffer> subjectBuffers) : base(hostView)
{
_subjectBuffer = subjectBuffers;
_hostView.MouseHover += OnTextViewMouseHover;
}

private void OnTextViewMouseHover(object sender, MouseHoverEventArgs e)
{
// already active
if (MefProvider.Instance.AsyncQuickInfoBroker.IsQuickInfoActive(_hostView)) return;

// find the mouse position by mapping down to the subject buffer
SnapshotPoint? point = _hostView.BufferGraph.MapDownToFirstMatch(
new SnapshotPoint(_hostView.TextSnapshot, e.Position),
PointTrackingMode.Positive,
snapshot => _subjectBuffer.Contains(snapshot.TextBuffer),
PositionAffinity.Predecessor
);

if (!point.HasValue) return;

// make a tracking point of the source
ITrackingPoint triggerPoint = point.Value.Snapshot.CreateTrackingPoint(
point.Value.Position, PointTrackingMode.Positive
);

ThreadHelper.JoinableTaskFactory.Run(async delegate
{
await MefProvider.Instance.AsyncQuickInfoBroker.TriggerQuickInfoAsync(
_hostView, triggerPoint, QuickInfoSessionOptions.TrackMouse
);
});
}

public void Detach(ITextView textView)
{
if (_hostView == textView)
{
_hostView.MouseHover -= OnTextViewMouseHover;
base.Dispose();
}
}

public void ConnectSubjectBuffer(ITextBuffer subjectBuffer)
{
}

public void DisconnectSubjectBuffer(ITextBuffer subjectBuffer)
{
}
}

// This causes it to call `TriggerQuickInfoAsync` too early and dead lock
// the UI, I'm not sure what happened and how should we check if quickinfo
// is ready yet, but we don't need this right now, leave here for the future
//
//[Export(typeof(IIntellisenseControllerProvider))]
//[Name("Codeium Intellisense Controller Provider")]
//[ContentType("any")]
//internal sealed class IntellisenseControllerProvider : IIntellisenseControllerProvider
//{
// public IIntellisenseController TryCreateIntellisenseController(ITextView textView, IList<ITextBuffer> subjectBuffers)
// {
// return new IntellisenseController(textView, subjectBuffers);
// }
//}
150 changes: 150 additions & 0 deletions CodeiumVS/QuickInfo/QuickInfoSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
using CodeiumVS.Utilities;
using Microsoft;
using Microsoft.VisualStudio.Core.Imaging;
using Microsoft.VisualStudio.Imaging;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Adornments;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Utilities;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CodeiumVS.QuickInfo;


internal sealed class CodeiumAsyncQuickInfoSource(ITextBuffer textBuffer) :
PropertyOwnerExtension<ITextBuffer, CodeiumAsyncQuickInfoSource>(textBuffer), IAsyncQuickInfoSource, IDisposable
{
private ITextView _tagAggregatorTextView;
private ITagAggregator<IErrorTag>? _tagAggregator;

private static readonly ImageId _icon = KnownMonikers.StatusInformation.ToImageId();

private static string GetQuickInfoItemText(object content)
{
if (content is string str) return str;
if (content is ClassifiedTextRun textRun) return textRun.Text;

if (content is ContainerElement containter)
{
string text = string.Empty;
foreach (var element in containter.Elements)
text += GetQuickInfoItemText(element);
return text;
}

if (content is ClassifiedTextElement textElement)
{
string text = string.Empty;
foreach (var element in textElement.Runs)
text += GetQuickInfoItemText(element);
return text;
}

return string.Empty;
}

public async Task<QuickInfoItem> GetQuickInfoItemAsync(IAsyncQuickInfoSession session,CancellationToken cancellationToken)
{
if (_disposed || session.TextView.TextBuffer != _owner)
return null;

await GetTagAggregatorAsync(session.TextView);

Assumes.True(_tagAggregator != null,
"Codeium Quick Info Source couldn't create a tag aggregator for error tags");

// Put together the span over which tags are to be discovered.
// This will be the zero-length span at the trigger point of the session.
SnapshotPoint? subjectTriggerPoint = session.GetTriggerPoint(_owner.CurrentSnapshot);
if (!subjectTriggerPoint.HasValue)
{
Debug.Fail("The Codeium Quick Info Source is being called when it shouldn't be.");
return null;
}

ITextSnapshot currentSnapshot = subjectTriggerPoint.Value.Snapshot;
var querySpan = new SnapshotSpan(subjectTriggerPoint.Value, 0);

// Must be on main thread when dealing with tag aggregator
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

// Ask for all of the error tags that intersect our query span. We'll get back a list of mapping tag spans.
// The first of these is what we'll use for our quick info.
IEnumerable<IMappingTagSpan<IErrorTag>> tags = _tagAggregator.GetTags(querySpan);
ITrackingSpan appToSpan = null;

string problemMessage = string.Empty;

foreach (var tag in tags.Cast<MappingTagSpan<IErrorTag>>())
{
NormalizedSnapshotSpanCollection applicableToSpans = tag.Span.GetSpans(currentSnapshot);
if ((applicableToSpans.Count == 0) || (tag.Tag.ToolTipContent == null)) continue;

// We've found a error tag at the right location with a tag span that maps to our subject buffer.
appToSpan = currentSnapshot.CreateTrackingSpan(
applicableToSpans[0].Span, SpanTrackingMode.EdgeInclusive
);

appToSpan = IntellisenseUtilities.GetEncapsulatingSpan(
session.TextView, appToSpan, appToSpan
);

problemMessage += GetQuickInfoItemText(tag.Tag.ToolTipContent) + " and ";
}

if (appToSpan != null && problemMessage.Length > 0)
{
var hyperLink = ClassifiedTextElement.CreateHyperlink(
"Codeium: Explain Problem",
"Ask codeium to explain the problem",
() => {
ThreadHelper.JoinableTaskFactory.RunAsync(async delegate
{
// TODO: Has the package been loaded at this point?
await CodeiumVSPackage.Instance.LanguageServer.Controller.ExplainProblemAsync(
problemMessage.Substring(0, problemMessage.Length - 5), appToSpan.GetSpan(currentSnapshot)
);
}).FireAndForget(true);
}
);

ContainerElement container = new(ContainerElementStyle.Wrapped, new ImageElement(_icon), hyperLink);
return new QuickInfoItem(appToSpan, container);
}

return null;
}

private async Task GetTagAggregatorAsync(ITextView textView)
{
if (_tagAggregator == null)
{
_tagAggregatorTextView = textView;
_tagAggregator = await IntellisenseUtilities.GetTagAggregatorAsync<IErrorTag>(textView);
}
else if (_tagAggregatorTextView != textView)
{
throw new ArgumentException("The Codeium Quick Info Source cannot be shared between TextViews.");
}
}
}


[Export(typeof(IAsyncQuickInfoSourceProvider))]
[Name("Codeium Quick Info Provider")]
[ContentType("any")]
[Order(After = "Default Quick Info Presenter")]
internal sealed class AsyncQuickInfoSourceProvider : IAsyncQuickInfoSourceProvider
{
public IAsyncQuickInfoSource TryCreateQuickInfoSource(ITextBuffer textBuffer)
{
return CodeiumAsyncQuickInfoSource.GetOrCreate(textBuffer, () => new CodeiumAsyncQuickInfoSource(textBuffer));
}
}
3 changes: 1 addition & 2 deletions CodeiumVS/Utilities/CodeAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ public static Span GetBlockSpan(ITextView view, int position, out IStructureTag?
else if (length < 0) return new Span(0, 0);

// get the tag aggregator
ITagAggregator<IStructureTag> tagAggregator =
MefProvider.Instance.TagAggregatorFactoryService.CreateTagAggregator<IStructureTag>(view);
ITagAggregator<IStructureTag> tagAggregator = IntellisenseUtilities.GetTagAggregator<IStructureTag>(view);

// not sure if this could happen
if (tagAggregator == null) return new Span(0, 0);
Expand Down
61 changes: 51 additions & 10 deletions CodeiumVS/Utilities/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,64 @@
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Threading;
using Microsoft.VisualStudio.Utilities;
using System.Collections.Generic;
using System.Diagnostics;

namespace CodeiumVS.Utilities;

internal abstract class TextViewExtension<T> where T : class
internal abstract class PropertyOwnerExtension<OwnerType, ExtensionType> : IDisposable
where OwnerType : IPropertyOwner
where ExtensionType : class
{
protected ITextView _hostView { get; set; }
protected bool _disposed = false;
protected readonly OwnerType _owner;

private static readonly Dictionary<ITextView, T> Instances = [];
public PropertyOwnerExtension(OwnerType owner)
{
_owner = owner;
_owner.Properties.AddProperty(typeof(ExtensionType), this as ExtensionType);
}

public static ExtensionType? GetInstance(OwnerType owner)
{
return owner.Properties.TryGetProperty(typeof(ExtensionType), out ExtensionType instance) ? instance : null;
}

public TextViewExtension(ITextView hostView)
public static ExtensionType GetOrCreate(OwnerType owner, Func<ExtensionType> creator)
{
_hostView = hostView;
Instances.Add(_hostView, this as T);
return GetInstance(owner) ?? creator();
}

public static T? GetInstance(ITextView hostView)
public virtual void Dispose()
{
return Instances.TryGetValue(hostView, out var instance) ? instance : null;
if (_disposed) return;
_disposed = true;
_owner.Properties.RemoveProperty(typeof(ExtensionType));
}
}

internal abstract class TextViewExtension<ViewType, ExtensionType> : PropertyOwnerExtension<ViewType, ExtensionType>
where ViewType : ITextView
where ExtensionType : class
{
protected ViewType _hostView => _owner;

public TextViewExtension(ViewType hostView) : base(hostView)
{
_hostView.Closed += HostView_Closed;
}

private void HostView_Closed(object sender, EventArgs e)
{
Dispose();
}

public override void Dispose()
{
base.Dispose();
_hostView.Closed -= HostView_Closed;
}
}

Expand All @@ -27,4 +67,5 @@ internal class FunctionBlock(string name, string @params, TextSpan span)
public readonly string Name = name;
public readonly string Params = @params;
public readonly TextSpan Span = span;
}
}

Loading

0 comments on commit 5377559

Please sign in to comment.