diff --git a/MultilineGreyText/Options/General.cs b/MultilineGreyText/Options/General.cs index 3d56d54..eea0a54 100644 --- a/MultilineGreyText/Options/General.cs +++ b/MultilineGreyText/Options/General.cs @@ -5,6 +5,7 @@ using Community.VisualStudio.Toolkit; using Microsoft.VisualStudio.Shell; using System.Runtime.InteropServices; +using OutlineRegionTest.Properties; namespace RefactAI{ @@ -63,9 +64,22 @@ public class General : BaseOptionModel{ [DefaultValue(false)] public bool TelemetryCodeSnippets{ get; set; } = false; - //enters a message into the log when the options are saved + [Category("Refact Assistant")] + [DisplayName("Insecure SSL")] + [Description("An informative description.")] + [DefaultValue(false)] + public bool InsecureSSL { get; set; } = false; + + // Event handler to be invoked when settings are saved + private void OnSettingsSaved(General options) + { + OnSettingsChanged?.Invoke(this, EventArgs.Empty); + } + + // Event to notify when settings are changed + public static event EventHandler OnSettingsChanged; public General() : base(){ - Saved += delegate { VS.StatusBar.ShowMessageAsync("Options Saved").FireAndForget(); }; + Saved += OnSettingsSaved; } } } \ No newline at end of file diff --git a/MultilineGreyText/Options/GeneralOptionPage.cs b/MultilineGreyText/Options/GeneralOptionPage.cs index 5cd683e..c49dff8 100644 --- a/MultilineGreyText/Options/GeneralOptionPage.cs +++ b/MultilineGreyText/Options/GeneralOptionPage.cs @@ -3,15 +3,20 @@ using System.Runtime.InteropServices; using System.Windows; -namespace RefactAI{ +namespace RefactAI +{ [ComVisible(true)] [Guid("D8B47497-8AC9-4E2E-9D62-D8E8E7A47AA4")] - public class GeneralOptionPage : UIElementDialogPage{ - protected override UIElement Child{ - get{ - GeneralOptions page = new GeneralOptions{ + public class GeneralOptionPage : UIElementDialogPage + { + protected override UIElement Child + { + get + { + GeneralOptions page = new GeneralOptions + { generalOptionsPage = this }; page.Initialize(); @@ -20,4 +25,3 @@ protected override UIElement Child{ } } } - diff --git a/MultilineGreyText/Options/GeneralOptions.xaml b/MultilineGreyText/Options/GeneralOptions.xaml index 274a6b1..495eba9 100644 --- a/MultilineGreyText/Options/GeneralOptions.xaml +++ b/MultilineGreyText/Options/GeneralOptions.xaml @@ -51,6 +51,11 @@ + + Refactai: Insecure SSL + diff --git a/MultilineGreyText/Options/GeneralOptions.xaml.cs b/MultilineGreyText/Options/GeneralOptions.xaml.cs index 31e9941..a9f85ce 100644 --- a/MultilineGreyText/Options/GeneralOptions.xaml.cs +++ b/MultilineGreyText/Options/GeneralOptions.xaml.cs @@ -1,20 +1,24 @@ using System.Windows.Controls; -namespace RefactAI{ +namespace RefactAI +{ /// /// Interaction logic for GeneralOptions.xaml /// - public partial class GeneralOptions : UserControl { + public partial class GeneralOptions : UserControl + { internal GeneralOptionPage generalOptionsPage; - public GeneralOptions() { + public GeneralOptions() + { InitializeComponent(); } //Sets up all of the variables to display the current settings - public void Initialize(){ + public void Initialize() + { pPauseCompletion.IsChecked = General.Instance.PauseCompletion; pTelemetryCodeSnippets.IsChecked = General.Instance.TelemetryCodeSnippets; @@ -28,57 +32,80 @@ public void Initialize(){ } //pause completion checkbox checked - private void pPauseCompletion_Checked(object sender, System.Windows.RoutedEventArgs e){ + private void pPauseCompletion_Checked(object sender, System.Windows.RoutedEventArgs e) + { General.Instance.PauseCompletion = (bool)pPauseCompletion.IsChecked; General.Instance.Save(); } //pause completion checkbox unchecked - private void pPauseCompletion_Unchecked(object sender, System.Windows.RoutedEventArgs e){ + private void pPauseCompletion_Unchecked(object sender, System.Windows.RoutedEventArgs e) + { General.Instance.PauseCompletion = (bool)pPauseCompletion.IsChecked; General.Instance.Save(); } //code snippets checked - private void pTelemetryCodeSnippets_Checked(object sender, System.Windows.RoutedEventArgs e){ + private void pTelemetryCodeSnippets_Checked(object sender, System.Windows.RoutedEventArgs e) + { General.Instance.TelemetryCodeSnippets = (bool)pTelemetryCodeSnippets.IsChecked; General.Instance.Save(); } //code snippets unchecked - private void pTelemetryCodeSnippets_Unchecked(object sender, System.Windows.RoutedEventArgs e){ + private void pTelemetryCodeSnippets_Unchecked(object sender, System.Windows.RoutedEventArgs e) + { General.Instance.TelemetryCodeSnippets = (bool)pTelemetryCodeSnippets.IsChecked; General.Instance.Save(); } + //insecureSSL snippets checked + private void pInsecureSSL_Checked(object sender, System.Windows.RoutedEventArgs e) + { + General.Instance.InsecureSSL = (bool)pInsecureSSL.IsChecked; + General.Instance.Save(); + } + + //insecureSSL unchecked + private void pInsecureSSL_Unchecked(object sender, System.Windows.RoutedEventArgs e) + { + General.Instance.InsecureSSL = (bool)pInsecureSSL.IsChecked; + General.Instance.Save(); + } + //address url text handler - private void AddressURL_textChanged(object sender, TextChangedEventArgs args){ + private void AddressURL_textChanged(object sender, TextChangedEventArgs args) + { General.Instance.AddressURL = AddressURL.Text; General.Instance.Save(); } //api key text handler - private void APIKey_textChanged(object sender, TextChangedEventArgs args){ + private void APIKey_textChanged(object sender, TextChangedEventArgs args) + { General.Instance.APIKey = APIKey.Text; General.Instance.Save(); } //code completion model text handler - private void CodeCompletionModel_textChanged(object sender, TextChangedEventArgs args){ + private void CodeCompletionModel_textChanged(object sender, TextChangedEventArgs args) + { General.Instance.CodeCompletionModel = CodeCompletionModel.Text; General.Instance.Save(); } //code completion other text handler - private void CodeCompletionModelOther_textChanged(object sender, TextChangedEventArgs args){ + private void CodeCompletionModelOther_textChanged(object sender, TextChangedEventArgs args) + { General.Instance.CodeCompletionModelOther = CodeCompletionModelOther.Text; General.Instance.Save(); } //code completion scratchpad text handler - private void CodeCompletionScratchpad_textChanged(object sender, TextChangedEventArgs args){ + private void CodeCompletionScratchpad_textChanged(object sender, TextChangedEventArgs args) + { General.Instance.CodeCompletionScratchpad = CodeCompletionScratchpad.Text; General.Instance.Save(); } - } -} + } +} \ No newline at end of file diff --git a/MultilineGreyText/RefactCompletionCommandHandler.cs b/MultilineGreyText/RefactCompletionCommandHandler.cs index e066990..82441bd 100644 --- a/MultilineGreyText/RefactCompletionCommandHandler.cs +++ b/MultilineGreyText/RefactCompletionCommandHandler.cs @@ -15,6 +15,7 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.LanguageServer.Protocol; using System.Text.RegularExpressions; +using System.Diagnostics; namespace RefactAI{ @@ -48,23 +49,33 @@ public LanguageClientMetadata(string[] contentTypes, string clientName = null){ private Task completionTask = null; //The command Handler processes keyboard input. - internal RefactCompletionCommandHandler(IVsTextView textViewAdapter, ITextView textView, RefactCompletionHandlerProvider provider){ + internal RefactCompletionCommandHandler(IVsTextView textViewAdapter, ITextView textView, RefactCompletionHandlerProvider provider) + { this.m_textView = textView; this.m_provider = provider; this.textViewAdapter = textViewAdapter; - + var topBuffer = textView.BufferGraph.TopBuffer; var projectionBuffer = topBuffer as IProjectionBufferBase; var typeName = topBuffer.GetType(); ITextBuffer textBuffer = projectionBuffer != null ? projectionBuffer.SourceBuffers[0] : topBuffer; provider.documentFactory.TryGetTextDocument(textBuffer, out doc); - this.fileURI = new Uri(doc.FilePath); - this.filePath = this.fileURI.ToString(); - + + if (doc != null && !string.IsNullOrEmpty(doc.FilePath)) + { + this.fileURI = new Uri(doc.FilePath ?? throw new InvalidOperationException("Document file path is null.")); + this.filePath = this.fileURI.ToString(); + } + else + { + Debug.WriteLine("doc.FilePath is null or empty."); + this.filePath = string.Empty; + } + LoadLsp(this.filePath, doc); - //add the command to the command chain - textViewAdapter.AddCommandFilter(this, out m_nextCommandHandler); + // Add the command to the command chain + textViewAdapter.AddCommandFilter(this, out m_nextCommandHandler); } //Starts the refactlsp manually @@ -81,6 +92,12 @@ void LoadLsp(String file, ITextDocument doc){ //Adds file to LSP async Task ConnectFileToLSP(){ + if (fileURI == null) + { + // Handle the case where fileURI is not initialized + return; + } + if (!client.ContainsFile(filePath)){ await client.AddFile(filePath, doc.TextBuffer.CurrentSnapshot.GetText()); }else{ @@ -120,8 +137,9 @@ public bool IsInline(int lineN){ } //gets recommendations from LSP - public async void GetLSPCompletions(){ - if (!General.Instance.PauseCompletion){ + public async void GetLSPCompletions() + { + if (!General.Instance.PauseCompletion){ SnapshotPoint? caretPoint = m_textView.Caret.Position.Point.GetPoint(textBuffer => (!textBuffer.ContentType.IsOfType("projection")), PositionAffinity.Predecessor); if (caretPoint.HasValue){ @@ -146,12 +164,12 @@ public async void GetLSPCompletions(){ hasCompletionUpdated = false; bool multiline = !IsInline(lineN); - 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); + 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); } } } @@ -160,12 +178,14 @@ public async void GetLSPCompletions(){ } //sends lsp reccomendations to grey text tagger to be dispalyed - public void ShowRefactSuggestion(String s, int lineN, int characterN){ - - if (!string.IsNullOrEmpty(s)){ + public void ShowRefactSuggestion(String s, int lineN, int characterN) + { + 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){ + if (!caretPoint.HasValue) + { return; } @@ -174,17 +194,28 @@ public void ShowRefactSuggestion(String s, int lineN, int characterN){ int resCaretPos = textViewAdapter.GetCaretPos(out newLineN, out newCharacterN); //double checks the cursor is still on the line the recommendation is for - if(resCaretPos != VSConstants.S_OK || (lineN != newLineN) || (characterN != newCharacterN)){ + if (resCaretPos != VSConstants.S_OK || (lineN != newLineN) || (characterN != newCharacterN)) + { return; } var tagger = GetTagger(); - if(tagger != null && s != null){ + if (tagger != null && s != null) + { tagger.SetSuggestion(s, IsInline(lineN), characterN); + + // Ensure cursor is positioned correctly for multiline completions + if (s.Contains("\n")) + { + m_textView.Caret.MoveTo(new SnapshotPoint(m_textView.TextSnapshot, m_textView.TextSnapshot.Length)); + m_textView.Caret.EnsureVisible(); + } } } } + + //Used to detect when the user interacts with the intellisense popup void CheckSuggestionUpdate(uint nCmdID){ switch (nCmdID){ @@ -206,7 +237,7 @@ void CheckSuggestionUpdate(uint nCmdID){ //Key input handler public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut){ - //let the other handlers handle automation functions + //let the other handlers handle automation if (VsShellUtilities.IsInAutomationFunction(m_provider.ServiceProvider)){ return m_nextCommandHandler.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut); } @@ -222,6 +253,9 @@ public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pv return VSConstants.S_OK; }else{ tagger.ClearSuggestion(); + + // start the suggestions process again to see if suggestion left in between due to token limit + _ = Task.Run(() => GetLSPCompletions()); } } diff --git a/MultilineGreyText/RefactExtension.csproj b/MultilineGreyText/RefactExtension.csproj index a241786..5ac1f14 100644 --- a/MultilineGreyText/RefactExtension.csproj +++ b/MultilineGreyText/RefactExtension.csproj @@ -1,191 +1,193 @@ - - - - 17.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - true - - - - - - - - Debug - AnyCPU - 2.0 - {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - {EBB916BC-0F79-4EAB-82D7-C6B17817424E} - Library - Properties - OutlineRegionTest - OutlineRegionTest - v4.8 - true - true - true - false - false - true - true - Program - $(DevEnvDir)devenv.exe - /rootsuffix Exp - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - False - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - GeneralOptions.xaml - - - Component - - - - - - - - True - True - Resources.resx - - - True - True - Settings.settings - - - - - - - - - - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - Designer - VsixManifestGenerator - - - - - - - - - - - - - - - - - - - - PreserveNewest - true - - - true - PreserveNewest - - - - - 17.0.507 - - - 1.0.507 - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - 16.0.29.6 - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - 17.2.8 - - - compile; build; native; contentfiles; analyzers; buildtransitive - - - - - - Menus.ctmenu - - - true - - - Always - true - - - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - - true - VSPackage - Designer - - - - - Designer - MSBuild:Compile - - - - - - - - + + + + 17.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + true + + + + + + + + Debug + AnyCPU + 2.0 + {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {EBB916BC-0F79-4EAB-82D7-C6B17817424E} + Library + Properties + OutlineRegionTest + OutlineRegionTest + v4.8 + true + true + true + false + false + true + true + Program + $(DevEnvDir)devenv.exe + /rootsuffix Exp + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + False + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + GeneralOptions.xaml + + + Component + + + + + + + + True + True + Resources.resx + + + True + True + Settings.settings + + + + + + + + + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + Designer + VsixManifestGenerator + + + + + + + + + + + + + + + + + + + + PreserveNewest + true + + + true + PreserveNewest + + + + + 17.0.507 + + + 1.0.507 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + 16.0.29.6 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + 17.2.8 + + + compile; build; native; contentfiles; analyzers; buildtransitive + + + + + + Menus.ctmenu + + + true + + + Always + true + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + true + VSPackage + Designer + + + + + Designer + MSBuild:Compile + + + + + + + + + --> \ No newline at end of file diff --git a/MultilineGreyText/RefactLanguageClient.cs b/MultilineGreyText/RefactLanguageClient.cs index 0b1d12c..461cce2 100644 --- a/MultilineGreyText/RefactLanguageClient.cs +++ b/MultilineGreyText/RefactLanguageClient.cs @@ -20,16 +20,15 @@ using System.Windows.Media; using Microsoft.VisualStudio; using Community.VisualStudio.Toolkit; -using System.Windows.Controls; -using System.Windows; using System.Linq; -using System.Management; +using System.Runtime.InteropServices; -// 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{ +// 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 @@ -37,7 +36,8 @@ namespace RefactAI{ [Export(typeof(ILanguageClient))] [RunOnContext(RunningContext.RunOnHost)] - public class RefactLanguageClient : ILanguageClient, ILanguageClientCustomMessage2, IDisposable{ + 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; } @@ -47,13 +47,15 @@ public class RefactLanguageClient : ILanguageClient, ILanguageClientCustomMessag private StatusBar statusBar; //lsp instance - internal static RefactLanguageClient Instance{ + internal static RefactLanguageClient Instance + { get; set; } //rpc for sending requests to the lsp - internal JsonRpc Rpc{ + internal JsonRpc Rpc + { get; set; } @@ -89,33 +91,62 @@ internal JsonRpc Rpc{ internal HashSet files; //constructor - public RefactLanguageClient(){ + public RefactLanguageClient() + { Instance = this; files = new HashSet(); statusBar = new StatusBar(); + ShowDefaultStatusBar(); + + General.OnSettingsChanged += ReloadLanguageClient; + } + + async void ReloadLanguageClient(object sender, EventArgs e) + { + try + { + await StopServerAsync(); + var connection = await ActivateAsync(CancellationToken.None); + // Rpc = new JsonRpc(connection.Input, connection.Output); + + VS.StatusBar.ShowMessageAsync("Options Updated").FireAndForget(); + await OnLoadedAsync(); + + VS.StatusBar.ShowMessageAsync("Ready").FireAndForget(); + } + catch (Exception ex) + { + ShowStatusBarError("Failed to reload language client: " + ex.Message); + } } //gets/sets lsp configuration sections - public IEnumerable ConfigurationSections{ - get{ + 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){ + 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)){ + if (ContainsFile(filePath)) + { return; } //message to send to lsp - var openParam = new DidOpenTextDocumentParams{ - TextDocument = new TextDocumentItem{ + var openParam = new DidOpenTextDocumentParams + { + TextDocument = new TextDocumentItem + { Uri = new Uri(filePath), LanguageId = filePath.Substring(filePath.LastIndexOf(".") + 1), Version = 0, @@ -124,24 +155,31 @@ public async Task AddFile(String filePath, String text){ }; //send message to lsp catch any communication errors - try{ + try + { await Rpc.NotifyWithParameterObjectAsync("textDocument/didOpen", openParam); //add file to known file set files.Add(filePath); - }catch (Exception e){ + } + 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){ + 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){ + public async Task ActivateAsync(CancellationToken token) + { files.Clear(); + await StopServerAsync(); // Ensure any existing server is stopped before starting a new one + ProcessStartInfo info = new ProcessStartInfo(); info.FileName = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Resources", @"refact-lsp.exe"); @@ -159,64 +197,105 @@ public async Task ActivateAsync(CancellationToken token){ serverProcess = new Process(); serverProcess.StartInfo = info; - if (serverProcess.Start()){ + if (serverProcess.Start()) + { //returns the connection for future use this.c = new Connection(serverProcess.StandardOutput.BaseStream, serverProcess.StandardInput.BaseStream); return c; } - return null; + throw new InvalidOperationException("Failed to start language server process."); } + // get extension version + public static string GetPluginVersion() + { + var assembly = Assembly.GetExecutingAssembly(); + var versionAttribute = assembly.GetCustomAttribute(); + return versionAttribute?.Version ?? "unknown"; + } //get command line args for the lsp - String GetArgs(){ + String GetArgs() + { String args = ""; args += "--basic-telemetry "; - if (General.Instance.TelemetryCodeSnippets){ + if (General.Instance.TelemetryCodeSnippets) + { args += "--snippet-telemetry "; } + if (General.Instance.InsecureSSL) { + args += "--insecure "; + } 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 += "--enduser-client-version refact-" + GetPluginVersion() +" /vscode-2022 "; args += "--lsp-stdin-stdout 1"; return args; } //used to start loading lsp - public async Task OnLoadedAsync(){ - if (StartAsync != null){ + 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){ + public async Task StopServerAsync() + { + if (StopAsync != null) + { await StopAsync.InvokeAsync(this, EventArgs.Empty); } + + if (serverProcess != null) + { + try + { + serverProcess.Kill(); + serverProcess.WaitForExit(); + } + catch (Exception e) + { + Debug.Write("StopServerAsync Exception " + e); + } + finally + { + serverProcess.Dispose(); + serverProcess = null; + } + } } + //returns the completed task when the lsp has finished loading - public Task OnServerInitializedAsync(){ + public Task OnServerInitializedAsync() + { return Task.CompletedTask; } //used to set up custom messages - public Task AttachForCustomMessageAsync(JsonRpc rpc){ + public Task AttachForCustomMessageAsync(JsonRpc rpc) + { this.Rpc = rpc; return Task.CompletedTask; } //server initialize failed - public Task OnServerInitializeFailedAsync(ILanguageClientInitializationInfo initializationState){ + 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(){ + var failureContext = new InitializationFailureContext() + { FailureMessage = message, }; @@ -226,128 +305,170 @@ public Task OnServerInitializeFailedAsync(ILanguag } //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{ + 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{ + TextDocument = new VersionedTextDocumentIdentifier + { Version = version, Uri = fileURI, } }; - try{ + try + { await Rpc.NotifyWithParameterObjectAsync("textDocument/didChange", changesParam); - }catch(Exception e){ + } + 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){ + public async Task RefactCompletion(PropertyCollection props, String fileUri, int lineN, int character, bool multiline) + { //Make sure lsp has finished loading - if(this.Rpc == null){ + if (this.Rpc == null) + { return null; } - if (!ContainsFile(fileUri)){ + if (!ContainsFile(fileUri)) + { return null; } //catching server errors - try{ + try + { //args to send for refact/getCompletions - var argObj2 = new{ - text_document_position = new { + 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 } + 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)){ + if (!(choices != null && choices.Count() > 0)) + { return null; } //process results List suggestions = new List(); - foreach (var s in res["choices"]){ + foreach (var s in res["choices"]) + { var code_completion = s["code_completion"]; - if (code_completion != null){ + if (code_completion != null) + { suggestions.Add(code_completion.ToString()); } } - if (suggestions.Count > 0){ + if (suggestions.Count > 0) + { return suggestions[0]; - }else{ + } + else + { return null; } } - catch (Exception e){ + catch (Exception e) + { Debug.Write("Error " + e.ToString()); ShowStatusBarError("Error: \n" + e.Message); return null; } } - async void ShowDefaultStatusBar(){ + async void ShowDefaultStatusBar() + { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - if (!statusBar.IsInitialized()){ + if (!statusBar.IsInitialized()) + { statusBar.InitStatusBar(); } statusBar.ShowDefaultStatusBar(); } - async void ShowStatusBarError(String error){ + async void ShowStatusBarError(String error) + { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - if (!statusBar.IsInitialized()){ + if (!statusBar.IsInitialized()) + { statusBar.InitStatusBar(); } statusBar.ShowStatusBarError(error); } - async void ShowLoadingStatusBar(){ + async void ShowLoadingStatusBar() + { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - if (!statusBar.IsInitialized()){ + if (!statusBar.IsInitialized()) + { statusBar.InitStatusBar(); } statusBar.ShowLoadingSymbol(); } - public void Dispose(){ - if(serverProcess != null){ - try{ + public void Dispose() + { + General.OnSettingsChanged -= ReloadLanguageClient; + + if (serverProcess != null) + { + try + { serverProcess.Kill(); serverProcess.WaitForExit(); + } + catch (Exception e) + { + Debug.Write("Dispose " + e); + } + finally + { serverProcess.Dispose(); - }catch(Exception e){ - Debug.Write("Dispose" + e.ToString()); + serverProcess = null; } } } + //ilanguage client middle layer - internal class RefactMiddleLayer : ILanguageClientMiddleLayer{ + 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){ + 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){ + public Task HandleNotificationAsync(string methodName, JToken methodParam, Func sendNotification) + { Task t = sendNotification(methodParam); - if (methodName == "textDocument/didOpen"){ + if (methodName == "textDocument/didOpen") + { RefactLanguageClient.Instance.files.Add(methodParam["textDocument"]["uri"].ToString()); } return t; @@ -355,14 +476,18 @@ public Task HandleNotificationAsync(string methodName, JToken methodParam, Func< //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){ + public async Task HandleRequestAsync(string methodName, JToken methodParam, Func> sendRequest) + { var result = await sendRequest(methodParam); - if(methodName == "textDocument/completion"){ + if (methodName == "textDocument/completion") + { return JToken.Parse("[]"); - }else{ + } + else + { return result; } } } } -} +} \ No newline at end of file diff --git a/MultilineGreyText/RefactPackage.cs b/MultilineGreyText/RefactPackage.cs index 44355f1..8ccbe5a 100644 --- a/MultilineGreyText/RefactPackage.cs +++ b/MultilineGreyText/RefactPackage.cs @@ -1,6 +1,7 @@ using Community.VisualStudio.Toolkit; using Microsoft.VisualStudio.Shell; using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Threading; @@ -57,8 +58,11 @@ public RefactPackage(){ /// A provider for progress updates. /// A task representing the async work of package initialization, or an already completed task if there is none. Do not return null from this method. protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress){ + Debug.Write("Initialzing the extension!"); await this.RegisterCommandsAsync(); - await RefactAI.PauseRefactCommand.InitializeAsync(this); + + await PauseRefactCommand.InitializeAsync(this); + await TriggerCompletionCommand.InitializeAsync(this); } #endregion diff --git a/MultilineGreyText/RefactPackage.vsct b/MultilineGreyText/RefactPackage.vsct index 43890d4..a3973d1 100644 --- a/MultilineGreyText/RefactPackage.vsct +++ b/MultilineGreyText/RefactPackage.vsct @@ -1,102 +1,125 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MultilineGreyText/Resources/TriggerCompletionCommand.png b/MultilineGreyText/Resources/TriggerCompletionCommand.png new file mode 100644 index 0000000..b22d975 Binary files /dev/null and b/MultilineGreyText/Resources/TriggerCompletionCommand.png differ diff --git a/MultilineGreyText/TriggerCompletionCommand.cs b/MultilineGreyText/TriggerCompletionCommand.cs new file mode 100644 index 0000000..4ae80ad --- /dev/null +++ b/MultilineGreyText/TriggerCompletionCommand.cs @@ -0,0 +1,132 @@ +using Microsoft.VisualStudio.ComponentModelHost; +using Microsoft.VisualStudio.Editor; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.TextManager.Interop; +using System; +using System.ComponentModel.Design; +using System.Diagnostics; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Task = System.Threading.Tasks.Task; + +namespace RefactAI +{ + /// + /// Command handler + /// + internal sealed class TriggerCompletionCommand + { + /// + /// Command ID. + /// + public const int CommandId = 256; + + /// + /// Command menu group (command set GUID). + /// + public static readonly Guid CommandSet = new Guid("528a0c75-4c23-4946-8b7f-b28afb34defc"); + + /// + /// VS Package that provides this command, not null. + /// + private readonly AsyncPackage package; + + /// + /// Initializes a new instance of the class. + /// Adds our command handlers for menu (commands must exist in the command table file) + /// + /// Owner package, not null. + /// Command service to add command to, not null. + private TriggerCompletionCommand(AsyncPackage package, OleMenuCommandService commandService) + { + this.package = package ?? throw new ArgumentNullException(nameof(package)); + commandService = commandService ?? throw new ArgumentNullException(nameof(commandService)); + + var menuCommandID = new CommandID(CommandSet, CommandId); + var menuItem = new MenuCommand(this.Execute, menuCommandID); + commandService.AddCommand(menuItem); + } + + /// + /// Gets the instance of the command. + /// + public static TriggerCompletionCommand Instance + { + get; + private set; + } + + /// + /// Gets the service provider from the owner package. + /// + private Microsoft.VisualStudio.Shell.IAsyncServiceProvider ServiceProvider + { + get + { + return this.package; + } + } + + /// + /// Initializes the singleton instance of the command. + /// + /// Owner package, not null. + public static async Task InitializeAsync(AsyncPackage package) + { + // Switch to the main thread - the call to AddCommand in TriggerCompletionCommand's constructor requires + // the UI thread. + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken); + + OleMenuCommandService commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService; + Instance = new TriggerCompletionCommand(package, commandService); + } + + /// + /// This function is the callback used to execute the command when the menu item is clicked. + /// See the constructor to see how the menu item is associated with this function using + /// OleMenuCommandService service and MenuCommand class. + /// + /// Event sender. + /// Event args. + private void Execute(object sender, EventArgs e) + { + ThreadHelper.ThrowIfNotOnUIThread(); + string message = string.Format(CultureInfo.CurrentCulture, "Inside {0}.MenuItemCallback()", this.GetType().FullName); + string title = "TriggerCompletionCommand"; + + // Show a message box to prove we were here + VsShellUtilities.ShowMessageBox( + this.package, + message, + title, + OLEMSGICON.OLEMSGICON_INFO, + OLEMSGBUTTON.OLEMSGBUTTON_OK, + OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST); + + // Trigger LSP completions + TriggerLSPCompletions(); + } + + private void TriggerLSPCompletions() + { + ThreadHelper.ThrowIfNotOnUIThread(); + + var componentModel = (IComponentModel)Package.GetGlobalService(typeof(SComponentModel)); + var textManager = (IVsTextManager)Package.GetGlobalService(typeof(SVsTextManager)); + var adapterService = componentModel.GetService(); + var completionHandlerProvider = componentModel.GetService(); + + IVsTextView textView; + textManager.GetActiveView(1, null, out textView); + var wpfTextView = adapterService.GetWpfTextView(textView); + + if (wpfTextView != null) + { + var commandHandler = wpfTextView.Properties.GetProperty(typeof(RefactCompletionCommandHandler)); + commandHandler?.GetLSPCompletions(); + } + } + } +}