diff --git a/MultilineGreyText/InlineGreyTextTagger.cs b/MultilineGreyText/InlineGreyTextTagger.cs index d55d30a..a947baa 100644 --- a/MultilineGreyText/InlineGreyTextTagger.cs +++ b/MultilineGreyText/InlineGreyTextTagger.cs @@ -34,14 +34,20 @@ public InlineGreyTextTagger(IWpfTextView view){ /// Adornment corresponding to given data. May be null. public void UpdateAdornment(UIElement text){ ClearAdornment(); - stackPanel.Children.Add(text); + stackPanel.Children.Add(text); + stackPanel.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + stackPanel.UpdateLayout(); } public void ClearAdornment(){ stackPanel.Children.Clear(); + stackPanel = new StackPanel(); } public void FormatText(TextRunProperties props){ + if(props == null){ + return; + } foreach (TextBlock block in stackPanel.Children){ block.FontFamily = props.Typeface.FontFamily; block.FontSize = props.FontRenderingEmSize; @@ -68,7 +74,10 @@ public virtual IEnumerable> GetTags(NormalizedSn } ITextSnapshot requestedSnapshot = spans[0].Snapshot; - stackPanel.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + double width = view.FormattedLineSource.ColumnWidth * ((stackPanel.Children[0] as TextBlock).Inlines.First() as Run).Text.Length; + stackPanel.Measure(new Size(width, double.PositiveInfinity)); + stackPanel.MinWidth = width; + stackPanel.MaxWidth = width; var caretLine = view.Caret.ContainingTextViewLine; SnapshotPoint point = view.Caret.Position.BufferPosition.TranslateTo(requestedSnapshot, PointTrackingMode.Positive); var line = requestedSnapshot.GetLineFromPosition(point); diff --git a/MultilineGreyText/MultilineGreyTextTagger.cs b/MultilineGreyText/MultilineGreyTextTagger.cs index 43c6fba..fadb70f 100644 --- a/MultilineGreyText/MultilineGreyTextTagger.cs +++ b/MultilineGreyText/MultilineGreyTextTagger.cs @@ -70,39 +70,39 @@ private InlineGreyTextTagger GetTagger(){ } public void SetSuggestion(String newSuggestion, bool inline, int caretPoint){ - ClearSuggestion(); - inlineSuggestion = inline; - - int lineN = GetCurrentTextLine(); - - if (lineN < 0) return; - - String untrim = buffer.CurrentSnapshot.GetLineFromLineNumber(lineN).GetText(); - String line = untrim.TrimStart(); - int offset = untrim.Length - line.Length; - - caretPoint = Math.Max(0, caretPoint - offset); - - String combineSuggestion = line + newSuggestion; - if (line.Length - caretPoint > 0){ - String currentText = line.Substring(0, caretPoint); - combineSuggestion = currentText + newSuggestion; - userEndingText = line.TrimEnd().Substring(caretPoint); - var userIndex = newSuggestion.IndexOf(userEndingText); - if(userIndex < 0){ - return; - } - userIndex += currentText.Length; - - this.userIndex = userIndex; - isTextInsertion = true; - insertionPoint = line.Length - caretPoint; - }else{ - isTextInsertion = false; - } - - suggestion = new Tuple(combineSuggestion, combineSuggestion.Split('\n')); - Update(); + ClearSuggestion(); + inlineSuggestion = inline; + + int lineN = GetCurrentTextLine(); + + if (lineN < 0) return; + + String untrim = buffer.CurrentSnapshot.GetLineFromLineNumber(lineN).GetText(); + String line = untrim.TrimStart(); + int offset = untrim.Length - line.Length; + + caretPoint = Math.Max(0, caretPoint - offset); + + String combineSuggestion = line + newSuggestion; + if (line.Length - caretPoint > 0){ + String currentText = line.Substring(0, caretPoint); + combineSuggestion = currentText + newSuggestion; + userEndingText = line.Substring(caretPoint).TrimEnd(); + var userIndex = newSuggestion.IndexOf(userEndingText); + if (userIndex < 0){ + return; + } + userIndex += currentText.Length; + + this.userIndex = userIndex; + isTextInsertion = true; + insertionPoint = line.Length - caretPoint; + }else{ + isTextInsertion = false; + } + + suggestion = new Tuple(combineSuggestion, combineSuggestion.Split('\n')); + Update(); } private void CaretUpdate(object sender, CaretPositionChangedEventArgs e){ @@ -163,7 +163,7 @@ public IEnumerable> GetTags(NormalizedSnapshotSpanCollection s var snapshotLine = currentSnapshot.GetLineFromLineNumber(currentTextLineN); var height = view.LineHeight * (currentSuggestion.Item2.Length - 1); - + if(currentTextLineN == 0 && currentSnapshot.Lines.Count() == 1 && String.IsNullOrEmpty(currentSnapshot.GetText())){ height += view.LineHeight; } @@ -241,7 +241,6 @@ void AddInsertionTextBlock(int start, int end, string line){ GetTagger().UpdateAdornment(CreateTextBox(remainder, greyBrush)); } - //Updates the grey text public void UpdateAdornment(IWpfTextView view, string userText, int suggestionStart){ stackPanel.Children.Clear(); @@ -456,12 +455,16 @@ public bool CompleteText(){ //replaces text in the editor void ReplaceText(string text, int lineN){ ClearSuggestion(); - SnapshotSpan span = this.snapshot.GetLineFromLineNumber(lineN).Extent; - ITextEdit edit = view.TextBuffer.CreateEdit(); - - edit.Replace(span, text); - edit.Apply(); + ITextEdit edit = view.BufferGraph.TopBuffer.CreateEdit(); + var spanLength = span.Length; + edit.Replace(span, text); + var newSnapshot = edit.Apply(); + + if(spanLength == 0 && text.Length > 0){ + view.Caret.MoveToPreviousCaretPosition(); + view.Caret.MoveToNextCaretPosition(); + } } //sets up the suggestion for display diff --git a/MultilineGreyText/RefactCompletionCommandHandler.cs b/MultilineGreyText/RefactCompletionCommandHandler.cs index 4ee9e3e..e066990 100644 --- a/MultilineGreyText/RefactCompletionCommandHandler.cs +++ b/MultilineGreyText/RefactCompletionCommandHandler.cs @@ -45,6 +45,7 @@ public LanguageClientMetadata(string[] contentTypes, string clientName = null){ private int version = 0; private bool hasCompletionUpdated = false; + private Task completionTask = null; //The command Handler processes keyboard input. internal RefactCompletionCommandHandler(IVsTextView textViewAdapter, ITextView textView, RefactCompletionHandlerProvider provider){ @@ -79,12 +80,22 @@ void LoadLsp(String file, ITextDocument doc){ } //Adds file to LSP - void ConnectFileToLSP(){ + async Task ConnectFileToLSP(){ if (!client.ContainsFile(filePath)){ - client.AddFile(filePath, doc.TextBuffer.CurrentSnapshot.GetText()); - - //listen for changes - ((ITextBuffer2)doc.TextBuffer).ChangedHighPriority += ChangeEvent; + await client.AddFile(filePath, doc.TextBuffer.CurrentSnapshot.GetText()); + }else{ + version++; + TextDocumentContentChangeEvent[] contentChanges = new TextDocumentContentChangeEvent[1]; + var snapshot = doc.TextBuffer.CurrentSnapshot; + contentChanges[0] = new TextDocumentContentChangeEvent { + Text = snapshot.GetText(), + Range = new Range { + Start = new Position(0, 0), + End = new Position(snapshot.Lines.Count(), 0) + }, + RangeLength = snapshot.Lines.Count() + }; + await this.client.InvokeTextDocumentDidChangeAsync(fileURI, version, contentChanges); } } @@ -98,34 +109,6 @@ private MultilineGreyTextTagger GetTagger(){ } } - //Send changes to LSP - private void ChangeEvent(object sender, TextContentChangedEventArgs args){ - version++; - - //converts the changelist to be readable by LSP - TextDocumentContentChangeEvent[] contentChanges = args.Changes.Reverse().Select(change => { - int startLine, startColumn; - textViewAdapter.GetLineAndColumn(change.OldSpan.Start, out startLine, out startColumn); - int endLine, endColumn; - textViewAdapter.GetLineAndColumn(change.OldSpan.End, out endLine, out endColumn); - - return new TextDocumentContentChangeEvent{ - Text = change.NewText, - Range = new Range{ - Start = new Position(startLine, startColumn), - End = new Position(endLine, endColumn) - }, - RangeLength = change.OldSpan.Length - }; - }).ToArray(); - - //sends changes to LSP - if (contentChanges.Length > 0){ - contentChanges[0].Text = m_textView.TextBuffer.CurrentSnapshot.GetText(); - this.client.InvokeTextDocumentDidChangeAsync(fileURI, version, contentChanges); - } - } - //required by interface just boiler plate public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText){ return m_nextCommandHandler.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText); @@ -137,7 +120,7 @@ public bool IsInline(int lineN){ } //gets recommendations from LSP - public void GetLSPCompletions(){ + public async void GetLSPCompletions(){ if (!General.Instance.PauseCompletion){ SnapshotPoint? caretPoint = m_textView.Caret.Position.Point.GetPoint(textBuffer => (!textBuffer.ContentType.IsOfType("projection")), PositionAffinity.Predecessor); @@ -159,27 +142,27 @@ public void GetLSPCompletions(){ return; } - if (!client.ContainsFile(filePath)){ - ConnectFileToLSP(); - } + await ConnectFileToLSP(); hasCompletionUpdated = false; bool multiline = !IsInline(lineN); - var refactRes = client.RefactCompletion(m_textView.TextBuffer.Properties, filePath, lineN, multiline ? 0 : characterN, multiline); - ShowRefactSuggestion(refactRes, new Tuple(lineN, characterN)); + if(completionTask == null || completionTask.IsCompleted){ + completionTask = client.RefactCompletion(m_textView.TextBuffer.Properties, filePath, lineN, multiline ? 0 : characterN, multiline); + var s = await completionTask; + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + if (completionTask == null || completionTask.IsCompleted){ + ShowRefactSuggestion(s, lineN, characterN); + } + } } } } } //sends lsp reccomendations to grey text tagger to be dispalyed - public async void ShowRefactSuggestion(Task res, Object position){ - var p = position as Tuple; - int lineN = p.Item1; - int characterN = p.Item2; + public void ShowRefactSuggestion(String s, int lineN, int characterN){ - String s = await res; - if (res != null){ + if (!string.IsNullOrEmpty(s)){ //the caret must be in a non-projection location SnapshotPoint? caretPoint = m_textView.Caret.Position.Point.GetPoint(textBuffer => (!textBuffer.ContentType.IsOfType("projection")), PositionAffinity.Predecessor); if (!caretPoint.HasValue){ @@ -265,10 +248,10 @@ public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pv //gets lsp completions on added character or deletions if (!typedChar.Equals(char.MinValue) || commandID == (uint)VSConstants.VSStd2KCmdID.RETURN){ - GetLSPCompletions(); + _ = Task.Run(() => GetLSPCompletions()); handled = true; }else if (commandID == (uint)VSConstants.VSStd2KCmdID.BACKSPACE || commandID == (uint)VSConstants.VSStd2KCmdID.DELETE){ - GetLSPCompletions(); + _ = Task.Run(()=>GetLSPCompletions()); handled = true; } diff --git a/MultilineGreyText/RefactLanguageClient.cs b/MultilineGreyText/RefactLanguageClient.cs index fd42eef..852a093 100644 --- a/MultilineGreyText/RefactLanguageClient.cs +++ b/MultilineGreyText/RefactLanguageClient.cs @@ -1,342 +1,364 @@ -using Microsoft.VisualStudio.LanguageServer.Client; -using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Utilities; -using StreamJsonRpc; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Task = System.Threading.Tasks.Task; -using Microsoft.VisualStudio.Threading; -using Newtonsoft.Json.Linq; -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.LanguageServer.Protocol; -using Microsoft.VisualStudio.Shell.Interop; -using static System.Net.Mime.MediaTypeNames; -using System.Windows.Forms; -using System.Windows.Media; -using Microsoft.VisualStudio; -using Community.VisualStudio.Toolkit; -using System.Windows.Controls; -using System.Windows; - -// VS uses LSP in the background but doesn't play nicely with custom LSP servers for -// languages that already have a default LSP. - - -namespace RefactAI{ - - //the lsp client for refact - //any means the lsp should start up for any file extension - [ContentType("any")] - [Export(typeof(ILanguageClient))] - [RunOnContext(RunningContext.RunOnHost)] - - public class RefactLanguageClient : ILanguageClient, ILanguageClientCustomMessage2, IDisposable{ - //service provider is used to get the IVsServiceProvider which is needed for the status bar - [Import] - internal SVsServiceProvider ServiceProvider { get; set; } - - private Connection c; - private Process serverProcess = null; - private StatusBar statusBar; - - //lsp instance - internal static RefactLanguageClient Instance{ - get; - set; - } - - //rpc for sending requests to the lsp - internal JsonRpc Rpc{ - get; - set; - } - - //checks if lsp has started to load used to detect presence of lsp - public bool loaded = false; - - //StartAsync used to start the lsp - public event AsyncEventHandler StartAsync; - - //StopAsync used to stop the lsp - public event AsyncEventHandler StopAsync; - - //name of lsp - public string Name => "Refact Language Extension"; - - //intialization options - public object InitializationOptions => null; - - //files to watch - public IEnumerable FilesToWatch => null; - - //middle layer used to intercep messages to/from lsp - public object MiddleLayer => RefactMiddleLayer.Instance; - - //custom message target - public object CustomMessageTarget => null; - - //show notification on initialize failed setting - public bool ShowNotificationOnInitializeFailed => true; - - //files lsp is aware of - internal HashSet files; - - //constructor - public RefactLanguageClient(){ - Instance = this; - files = new HashSet(); - statusBar = new StatusBar(); - } - - //gets/sets lsp configuration sections - public IEnumerable ConfigurationSections{ - get{ - yield return ""; - } - } - - //sends file to lsp and adds it to known file set - public async void AddFile(String filePath, String text){ - - //wait for the rpc - while (Rpc == null) await Task.Delay(1); - - //dont send the file to the lsp if the lsp already knows about it - if (ContainsFile(filePath)){ - return; - } - - //add file to known file set - files.Add(filePath); - - //message to send to lsp - var openParam = new DidOpenTextDocumentParams{ - TextDocument = new TextDocumentItem{ - Uri = new Uri(filePath), - LanguageId = filePath.Substring(filePath.LastIndexOf(".") + 1), - Version = 0, - Text = text - } - }; - - //send message to lsp catch any communication errors - try{ - await Rpc.NotifyWithParameterObjectAsync("textDocument/didOpen", openParam); - }catch (Exception e){ - Debug.Write("InvokeTextDocumentDidChangeAsync Server Exception " + e.ToString()); - ShowStatusBarError("Server Exception: \n" + e.Message); - } - } - - //does lsp know about the file? - public bool ContainsFile(String file){ - return files.Contains(file); - } - - //activates the lsp using stdin/stdout to communicate with it - public async Task ActivateAsync(CancellationToken token){ - files.Clear(); - ProcessStartInfo info = new ProcessStartInfo(); - - info.FileName = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Resources", @"refact-lsp.exe"); - - info.Arguments = GetArgs(); - info.RedirectStandardInput = true; - info.RedirectStandardOutput = true; - info.UseShellExecute = false; - - //tells the lsp not to show the window - //turning this off can be useful for debugging - info.CreateNoWindow = true; - - //starts the lsp process - Process process = new Process(); - process.StartInfo = info; - - if (process.Start()){ - //returns the connection for future use - this.c = new Connection(process.StandardOutput.BaseStream, process.StandardInput.BaseStream); - return c; - } - - return null; - } - - //get command line args for the lsp - String GetArgs(){ - String args = ""; - args += "--basic-telemetry "; - - if (General.Instance.TelemetryCodeSnippets){ - args += "--snippet-telemetry "; - } - - args += "--address-url " + (String.IsNullOrWhiteSpace(General.Instance.AddressURL) ? "Refact" : General.Instance.AddressURL) + " "; - args += "--api-key " + (String.IsNullOrWhiteSpace(General.Instance.APIKey) ? "vs-classic-no-key" : General.Instance.APIKey) + " "; - args += "--lsp-stdin-stdout 1"; - - return args; - } - - //used to start loading lsp - public async Task OnLoadedAsync(){ - if (StartAsync != null){ - loaded = true; - await StartAsync.InvokeAsync(this, EventArgs.Empty); - statusBar = new StatusBar(); - } - } - - //stops the lsp - public async Task StopServerAsync(){ - if (StopAsync != null){ - await StopAsync.InvokeAsync(this, EventArgs.Empty); - } - } - - //returns the completed task when the lsp has finished loading - public Task OnServerInitializedAsync(){ - return Task.CompletedTask; - } - - //used to set up custom messages - public Task AttachForCustomMessageAsync(JsonRpc rpc){ - this.Rpc = rpc; - return Task.CompletedTask; - } - - //server initialize failed - public Task OnServerInitializeFailedAsync(ILanguageClientInitializationInfo initializationState){ - string message = "Oh no! Refact Language Client failed to activate, now we can't test LSP! :("; - string exception = initializationState.InitializationException?.ToString() ?? string.Empty; - message = $"{message}\n {exception}"; - - var failureContext = new InitializationFailureContext(){ - FailureMessage = message, - }; - - ShowStatusBarError(message); - - return Task.FromResult(failureContext); - } - - //manually sends change message to lsp - public async void InvokeTextDocumentDidChangeAsync(Uri fileURI, int version, TextDocumentContentChangeEvent[] contentChanges){ - if (Rpc != null && ContainsFile(fileURI.ToString())){ - var changesParam = new DidChangeTextDocumentParams{ - ContentChanges = contentChanges, - TextDocument = new VersionedTextDocumentIdentifier{ - Version = version, - Uri = fileURI, - } - }; - - try{ - await Rpc.NotifyWithParameterObjectAsync("textDocument/didChange", changesParam); - }catch(Exception e){ - Debug.Write("InvokeTextDocumentDidChangeAsync Server Exception " + e.ToString()); - ShowStatusBarError("Server Exception: \n" + e.Message); - } - } - } - - public async Task RefactCompletion(PropertyCollection props, String fileUri, int lineN, int character, bool multiline){ - //Make sure lsp has finished loading - if(this.Rpc == null){ - return null; - } - if (!ContainsFile(fileUri)){ - return null; - } - //catching server errors - try{ - //args to send for refact/getCompletions - var argObj2 = new{ - text_document_position = new { - textDocument = new { uri = fileUri }, - position = new { line = lineN, character = character }, - }, - parameters = new { max_new_tokens = 50, temperature = 0.2f }, - multiline = multiline, - textDocument = new { uri = fileUri }, - position = new{ line = lineN, character = character } - }; - ShowLoadingStatusBar(); - - var res = await this.Rpc.InvokeWithParameterObjectAsync("refact/getCompletions", argObj2); - - //process results - List suggestions = new List(); - foreach (var s in res["choices"]){ - suggestions.Add(s["code_completion"].ToString()); - } - - ShowDefaultStatusBar(); - - return suggestions[0]; - }catch (Exception e){ - Debug.Write("Error " + e.ToString()); - ShowStatusBarError("Error: \n" + e.Message); - return null; - } - } - - async void ShowDefaultStatusBar(){ - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - statusBar.ShowDefaultStatusBar(); - } - - async void ShowStatusBarError(String error){ - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - statusBar.ShowStatusBarError(error); - } - - async void ShowLoadingStatusBar(){ - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - statusBar.ShowLoadingSymbol(); - } - - public void Dispose(){ - if(serverProcess != null){ - serverProcess.Kill(); - serverProcess.WaitForExit(); - serverProcess.Dispose(); - } - } - - //ilanguage client middle layer - internal class RefactMiddleLayer : ILanguageClientMiddleLayer{ - internal readonly static RefactMiddleLayer Instance = new RefactMiddleLayer(); - - //returns true if the method should be handled by the middle layer - public bool CanHandle(string methodName){ - return true; - } - - //intercepts new files and adds them to the knonw file set - public Task HandleNotificationAsync(string methodName, JToken methodParam, Func sendNotification){ - Task t = sendNotification(methodParam); - if (methodName == "textDocument/didOpen"){ - RefactLanguageClient.Instance.files.Add(methodParam["textDocument"]["uri"].ToString()); - } - return t; - } - - //intercepts requests for completions sent to the lsp - //returns an empty list to avoid showing default completions - public async Task HandleRequestAsync(string methodName, JToken methodParam, Func> sendRequest){ - var result = await sendRequest(methodParam); - if(methodName == "textDocument/completion"){ - return JToken.Parse("[]"); - }else{ - return result; - } - } - } - } -} +using Microsoft.VisualStudio.LanguageServer.Client; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Utilities; +using StreamJsonRpc; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Task = System.Threading.Tasks.Task; +using Microsoft.VisualStudio.Threading; +using Newtonsoft.Json.Linq; +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Shell.Interop; +using static System.Net.Mime.MediaTypeNames; +using System.Windows.Forms; +using System.Windows.Media; +using Microsoft.VisualStudio; +using Community.VisualStudio.Toolkit; +using System.Windows.Controls; +using System.Windows; +using System.Linq; +using System.Management; + +namespace RefactAI{ + + //the lsp client for refact + //any means the lsp should start up for any file extension + [ContentType("any")] + [Export(typeof(ILanguageClient))] + [RunOnContext(RunningContext.RunOnHost)] + + public class RefactLanguageClient : ILanguageClient, ILanguageClientCustomMessage2, IDisposable{ + //service provider is used to get the IVsServiceProvider which is needed for the status bar + [Import] + internal SVsServiceProvider ServiceProvider { get; set; } + + private Connection c; + private Process serverProcess = null; + private StatusBar statusBar; + + //lsp instance + internal static RefactLanguageClient Instance{ + get; + set; + } + + //rpc for sending requests to the lsp + internal JsonRpc Rpc{ + get; + set; + } + + //checks if lsp has started to load used to detect presence of lsp + public bool loaded = false; + + //StartAsync used to start the lsp + public event AsyncEventHandler StartAsync; + + //StopAsync used to stop the lsp + public event AsyncEventHandler StopAsync; + + //name of lsp + public string Name => "Refact Language Extension"; + + //intialization options + public object InitializationOptions => null; + + //files to watch + public IEnumerable FilesToWatch => null; + + //middle layer used to intercep messages to/from lsp + public object MiddleLayer => RefactMiddleLayer.Instance; + + //custom message target + public object CustomMessageTarget => null; + + //show notification on initialize failed setting + public bool ShowNotificationOnInitializeFailed => true; + + //files lsp is aware of + internal HashSet files; + + //constructor + public RefactLanguageClient(){ + Instance = this; + files = new HashSet(); + statusBar = new StatusBar(); + } + + //gets/sets lsp configuration sections + public IEnumerable ConfigurationSections{ + get{ + yield return ""; + } + } + + //sends file to lsp and adds it to known file set + public async Task AddFile(String filePath, String text){ + + //wait for the rpc + while (Rpc == null) await Task.Delay(1); + + //dont send the file to the lsp if the lsp already knows about it + if (ContainsFile(filePath)){ + return; + } + + //message to send to lsp + var openParam = new DidOpenTextDocumentParams{ + TextDocument = new TextDocumentItem{ + Uri = new Uri(filePath), + LanguageId = filePath.Substring(filePath.LastIndexOf(".") + 1), + Version = 0, + Text = text + } + }; + + //send message to lsp catch any communication errors + try{ + await Rpc.NotifyWithParameterObjectAsync("textDocument/didOpen", openParam); + //add file to known file set + files.Add(filePath); + }catch (Exception e){ + Debug.Write("AddFile Server Exception " + e.ToString()); + ShowStatusBarError("Server Exception: \n" + e.Message); + } + } + + //does lsp know about the file? + public bool ContainsFile(String file){ + return files.Contains(file); + } + + //activates the lsp using stdin/stdout to communicate with it + public async Task ActivateAsync(CancellationToken token){ + files.Clear(); + ProcessStartInfo info = new ProcessStartInfo(); + + info.FileName = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Resources", @"refact-lsp.exe"); + + info.Arguments = GetArgs(); + info.RedirectStandardInput = true; + info.RedirectStandardOutput = true; + info.UseShellExecute = false; + + //tells the lsp not to show the window + //turning this off can be useful for debugging + info.CreateNoWindow = true; + + //starts the lsp process + serverProcess = new Process(); + serverProcess.StartInfo = info; + + if (serverProcess.Start()){ + //returns the connection for future use + this.c = new Connection(serverProcess.StandardOutput.BaseStream, serverProcess.StandardInput.BaseStream); + return c; + } + + return null; + } + + //get command line args for the lsp + String GetArgs(){ + String args = ""; + args += "--basic-telemetry "; + + if (General.Instance.TelemetryCodeSnippets){ + args += "--snippet-telemetry "; + } + + args += "--address-url " + (String.IsNullOrWhiteSpace(General.Instance.AddressURL) ? "Refact" : General.Instance.AddressURL) + " "; + args += "--api-key " + (String.IsNullOrWhiteSpace(General.Instance.APIKey) ? "vs-classic-no-key" : General.Instance.APIKey) + " "; + args += "--lsp-stdin-stdout 1"; + + return args; + } + + //used to start loading lsp + public async Task OnLoadedAsync(){ + if (StartAsync != null){ + loaded = true; + await StartAsync.InvokeAsync(this, EventArgs.Empty); + } + } + + //stops the lsp + public async Task StopServerAsync(){ + if (StopAsync != null){ + await StopAsync.InvokeAsync(this, EventArgs.Empty); + } + } + + //returns the completed task when the lsp has finished loading + public Task OnServerInitializedAsync(){ + return Task.CompletedTask; + } + + //used to set up custom messages + public Task AttachForCustomMessageAsync(JsonRpc rpc){ + this.Rpc = rpc; + return Task.CompletedTask; + } + + //server initialize failed + public Task OnServerInitializeFailedAsync(ILanguageClientInitializationInfo initializationState){ + string message = "Oh no! Refact Language Client failed to activate, now we can't test LSP! :("; + string exception = initializationState.InitializationException?.ToString() ?? string.Empty; + message = $"{message}\n {exception}"; + + var failureContext = new InitializationFailureContext(){ + FailureMessage = message, + }; + + ShowStatusBarError(message); + + return Task.FromResult(failureContext); + } + + //manually sends change message to lsp + public async Task InvokeTextDocumentDidChangeAsync(Uri fileURI, int version, TextDocumentContentChangeEvent[] contentChanges){ + if (Rpc != null && ContainsFile(fileURI.ToString())){ + var changesParam = new DidChangeTextDocumentParams{ + ContentChanges = contentChanges, + TextDocument = new VersionedTextDocumentIdentifier{ + Version = version, + Uri = fileURI, + } + }; + + try{ + await Rpc.NotifyWithParameterObjectAsync("textDocument/didChange", changesParam); + }catch(Exception e){ + Debug.Write("InvokeTextDocumentDidChangeAsync Server Exception " + e.ToString()); + ShowStatusBarError("Server Exception: \n" + e.Message); + } + } + } + + public async Task RefactCompletion(PropertyCollection props, String fileUri, int lineN, int character, bool multiline){ + //Make sure lsp has finished loading + if(this.Rpc == null){ + return null; + } + if (!ContainsFile(fileUri)){ + return null; + } + //catching server errors + try{ + //args to send for refact/getCompletions + var argObj2 = new{ + text_document_position = new { + textDocument = new { uri = fileUri }, + position = new { line = lineN, character = character }, + }, + parameters = new { max_new_tokens = 50, temperature = 0.2f }, + multiline = multiline, + textDocument = new { uri = fileUri }, + position = new{ line = lineN, character = character } + }; + await this.Rpc.DispatchCompletion; + ShowLoadingStatusBar(); + + var res = await this.Rpc.InvokeWithParameterObjectAsync("refact/getCompletions", argObj2); + ShowDefaultStatusBar(); + + var choices = res["choices"]; + + if (!(choices != null && choices.Count() > 0)){ + return null; + } + //process results + List suggestions = new List(); + foreach (var s in res["choices"]){ + var code_completion = s["code_completion"]; + if (code_completion != null){ + suggestions.Add(code_completion.ToString()); + } + } + + if (suggestions.Count > 0){ + return suggestions[0]; + }else{ + return null; + } + } + catch (Exception e){ + Debug.Write("Error " + e.ToString()); + ShowStatusBarError("Error: \n" + e.Message); + return null; + } + } + + async void ShowDefaultStatusBar(){ + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + if (!statusBar.IsInitialized()){ + statusBar.InitStatusBar(); + } + statusBar.ShowDefaultStatusBar(); + } + + async void ShowStatusBarError(String error){ + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + if (!statusBar.IsInitialized()){ + statusBar.InitStatusBar(); + } + statusBar.ShowStatusBarError(error); + } + + async void ShowLoadingStatusBar(){ + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + if (!statusBar.IsInitialized()){ + statusBar.InitStatusBar(); + } + statusBar.ShowLoadingSymbol(); + } + + public void Dispose(){ + if(serverProcess != null){ + try{ + serverProcess.Kill(); + serverProcess.WaitForExit(); + serverProcess.Dispose(); + }catch(Exception e){ + Debug.Write("Dispose" + e.ToString()); + } + } + } + + //ilanguage client middle layer + internal class RefactMiddleLayer : ILanguageClientMiddleLayer{ + internal readonly static RefactMiddleLayer Instance = new RefactMiddleLayer(); + + //returns true if the method should be handled by the middle layer + public bool CanHandle(string methodName){ + return true; + } + + //intercepts new files and adds them to the knonw file set + public Task HandleNotificationAsync(string methodName, JToken methodParam, Func sendNotification){ + Task t = sendNotification(methodParam); + if (methodName == "textDocument/didOpen"){ + RefactLanguageClient.Instance.files.Add(methodParam["textDocument"]["uri"].ToString()); + } + return t; + } + + //intercepts requests for completions sent to the lsp + //returns an empty list to avoid showing default completions + public async Task HandleRequestAsync(string methodName, JToken methodParam, Func> sendRequest){ + var result = await sendRequest(methodParam); + if(methodName == "textDocument/completion"){ + return JToken.Parse("[]"); + }else{ + return result; + } + } + } + } +} diff --git a/MultilineGreyText/StatusBar.cs b/MultilineGreyText/StatusBar.cs index 38ad2ff..053f6fb 100644 --- a/MultilineGreyText/StatusBar.cs +++ b/MultilineGreyText/StatusBar.cs @@ -15,19 +15,27 @@ internal class StatusBar{ Brush whiteBrush; Brush errorBrush; Brush transparentBrush; + bool isInitialized = false; public StatusBar(){ stack = new StackPanel(); stack.Width = 75.0; stack.Orientation = Orientation.Horizontal; + ShowDefaultStatusBar(); + } + + public bool IsInitialized(){ + return isInitialized; + } + + public void InitStatusBar(){ + isInitialized = true; panel = VisualTreeUtils.FindChild(Application.Current.MainWindow, childName: "StatusBarPanel") as Panel; whiteBrush = new SolidColorBrush(Colors.White); errorBrush = new SolidColorBrush(Colors.Red); transparentBrush = new SolidColorBrush(Colors.Transparent); - panel.Children.Add(stack); - ShowDefaultStatusBar(); + panel.Children.Insert(3, stack); } - public void ShowDefaultStatusBar(){ stack.Children.Clear(); stack.Background = transparentBrush;