ui: vs plugin integration

Reworked vs solution based project setup to utilize new CDB feature
This commit is contained in:
Manuel Dobusch
2016-11-10 17:40:14 +01:00
parent 9aed97eee2
commit 0a3200f8ff
32 changed files with 744 additions and 254 deletions
+10
View File
@@ -84,3 +84,13 @@
-Click Create
-Close Progress Bar before CDB is finished
-Check no new CDB was created
-Switch to Coati
-Keep Visual Studio running
-Open the 'New Project From Visual Studio Solution'
-Click 'Create CDB' button
-Check 'Create Compilation Database' dialog opens
-Create a new CDB with Settings of your choice
-Wait for CDB to be finished
-Click 'Yes' in the 'CDB finished' dialog
-Switch back to Coati
-Check prefilled data is correct
Binary file not shown.
@@ -138,7 +138,7 @@
<Compile Include="Logging\VSOutputLogger.cs" />
<Compile Include="Utility\FileUtility.cs" />
<Compile Include="Guids.cs" />
<Compile Include="NetworkProtocolUtility.cs" />
<Compile Include="Utility\NetworkProtocolUtility.cs" />
<Compile Include="Utility\NetworkUtility.cs" />
<Compile Include="Resources.Designer.cs">
<AutoGen>True</AutoGen>
@@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.IO;
using EnvDTE;
using System.Windows.Forms;
namespace CoatiSoftware.CoatiPlugin
{
@@ -109,6 +110,8 @@ namespace CoatiSoftware.CoatiPlugin
private SolutionEvents _solutionEvents = null;
private bool _validSolutionLoaded = false;
public uint ServerPort
{
get
@@ -147,6 +150,8 @@ namespace CoatiSoftware.CoatiPlugin
System.Threading.Thread _serverThread = null;
public CoatiPluginPackage()
{}
@@ -159,16 +164,18 @@ namespace CoatiSoftware.CoatiPlugin
Utility.FileUtility._errorCallback = new Utility.FileUtility.ErrorCallback(OnFileUtilityError);
// register callbacks for changes of plugin settings
OptionPageGrid._serverPortChangeCallback = new OptionPageGrid.Callback(OnServerPortChanged);
OptionPageGrid._clientPortChangeCallback = new OptionPageGrid.Callback(OnClientPortChanged);
OptionPageGrid._loggingToggled = new OptionPageGrid.Callback(OnLoggingToggled);
OptionPageGrid._obfuscationToggled = new OptionPageGrid.Callback(OnObfuscationToggled);
// register the plugin UI elements
OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
if ( null != mcs )
{
CommandID setActiveTokenCommandID = new CommandID(GuidList.guidCoatiPluginCmdSet, (int)PkgCmdIDList.cmdidCoatiSetActiveToken);
CommandID createProjectID = new CommandID(GuidList.guidCoatiPluginCmdSet, (int)PkgCmdIDList.cmdidCoatiCreateProject); // cmdidCoatiCreateProject
CommandID createProjectID = new CommandID(GuidList.guidCoatiPluginCmdSet, (int)PkgCmdIDList.cmdidCoatiCreateProject);
CommandID createCDBID = new CommandID(GuidList.guidCoatiPluginCmdSet, (int)PkgCmdIDList.cmdidCoatiCreateCDB);
_menuItemSetActiveToken = new MenuCommand(MenuItemCallback, setActiveTokenCommandID);
@@ -184,8 +191,8 @@ namespace CoatiSoftware.CoatiPlugin
mcs.AddCommand(_menuItemCreateCDB);
}
// register callbacks to enable/disable interaction for eligible solutions
DTE dte = (DTE)GetService(typeof(DTE));
_solutionEvents = dte.Events.SolutionEvents;
_solutionEvents.Opened += OnSolutionOpened;
@@ -200,6 +207,7 @@ namespace CoatiSoftware.CoatiPlugin
try
{
// the coati ui should only be enabled when the solution contains C/C++ projects
DTE dte = (DTE)GetService(typeof(DTE));
List<String> languages = Utility.SolutionUtility.GetSolutionLanguages(dte);
@@ -220,9 +228,13 @@ namespace CoatiSoftware.CoatiPlugin
_menuItemSetActiveToken.Enabled = true;
_menuItemCreateProject.Enabled = true;
_menuItemCreateCDB.Enabled = true;
_validSolutionLoaded = true;
}
else
{
_validSolutionLoaded = false;
Logging.Logging.LogInfo("No C/C++ project was detected");
}
}
@@ -240,6 +252,8 @@ namespace CoatiSoftware.CoatiPlugin
_menuItemSetActiveToken.Enabled = false;
_menuItemCreateProject.Enabled = false;
_menuItemCreateCDB.Enabled = false;
_validSolutionLoaded = false;
}
private void InitNetwork()
@@ -293,11 +307,37 @@ namespace CoatiSoftware.CoatiPlugin
{
if(creationResult._cdb != null && creationResult._cdbDirectory.Length > 0 && creationResult._cdbName.Length > 0)
{
WriteCDBToFile(creationResult._cdb, creationResult._cdbDirectory, creationResult._cdbName);
if(WriteCDBToFile(creationResult._cdb, creationResult._cdbDirectory, creationResult._cdbName))
{
string title = "CDB finished";
string text = "The CDB '" + creationResult._cdbName + "' was created at directory \"" + creationResult._cdbDirectory + "\"\n";
text += "Do you want to auto-import it in Coati now?";
//string message = NetworkProtocolUtility.createCreateProjectMessage(creationResult._cdbDirectory + "\\" + creationResult._cdbName, creationResult._headerDirectories);
DialogResult result = MessageBox.Show(text, title, MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
//Utility.AsynchronousClient.Send(message);
if (result == DialogResult.Yes)
{
string message = Utility.NetworkProtocolUtility.CreateCreateProjectMessage(creationResult._cdbDirectory + "\\" + creationResult._cdbName + ".json", creationResult._headerDirectories);
Utility.AsynchronousClient.Send(message);
}
}
else
{
string title = "Error";
string text = "Failed to write CDB '" + creationResult._cdbName + "' to directory \"" + creationResult._cdbDirectory + "\"\n";
if (LoggingEnabled)
{
text += "See log for details.";
}
else
{
text += "Consider enabling logging to provide additional information (Tools->Options->Coati).";
}
DialogResult result = MessageBox.Show(text, title, MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
else
{
@@ -305,7 +345,7 @@ namespace CoatiSoftware.CoatiPlugin
}
}
private void WriteCDBToFile(SolutionParser.CompilationDatabase cdb, string directory, string fileName)
private bool WriteCDBToFile(SolutionParser.CompilationDatabase cdb, string directory, string fileName)
{
try
{
@@ -314,35 +354,17 @@ namespace CoatiSoftware.CoatiPlugin
}
catch(Exception e)
{
string foo = "Error";
string bar = "Failed to write CDB '" + fileName + "' to directory \"" + directory + "\"\n";
bar += "See log for details.";
Wizard.WindowMessage wm = new Wizard.WindowMessage();
wm.Title = foo;
wm.Message = bar;
wm.RefreshWindow();
wm.ShowDialog();
bar = "Failed to write CDB '" + Logging.Obfuscation.NameObfuscator.GetObfuscatedName(fileName) + "' to directory \"" + Logging.Obfuscation.NameObfuscator.GetObfuscatedName(directory) + "\"\n";
Logging.Logging.LogError(bar);
string text = "Failed to write CDB '" + Logging.Obfuscation.NameObfuscator.GetObfuscatedName(fileName) + "' to directory \"" + Logging.Obfuscation.NameObfuscator.GetObfuscatedName(directory) + "\"\n";
Logging.Logging.LogError(text);
Logging.Logging.LogError("Exception: " + e.Message);
return;
return false;
}
string title = "CDB finished";
string message = "The CDB '" + fileName + "' was created at directory \"" + directory + "\"\n";
message += "You can now use it in Coati.";
Wizard.WindowMessage windowMessage = new Wizard.WindowMessage();
windowMessage.Title = title;
windowMessage.Message = message;
windowMessage.RefreshWindow();
windowMessage.ShowDialog();
message = "The CDB '" + Logging.Obfuscation.NameObfuscator.GetObfuscatedName(fileName) + "' was created at directory \"" + Logging.Obfuscation.NameObfuscator.GetObfuscatedName(directory) + "\"\n";
string message = "The CDB '" + Logging.Obfuscation.NameObfuscator.GetObfuscatedName(fileName) + "' was created at directory \"" + Logging.Obfuscation.NameObfuscator.GetObfuscatedName(directory) + "\"\n";
Logging.Logging.LogInfo(message);
return true;
}
private void MenuItemCallback(object sender, EventArgs e)
@@ -361,7 +383,7 @@ namespace CoatiSoftware.CoatiPlugin
int lineNumber = Utility.FileUtility.GetActiveLineNumber(dte);
int columnNumber = Utility.FileUtility.GetActiveColumnNumber(dte);
string message = NetworkProtocolUtility.createActivateTokenMessage(filePath + fileName, lineNumber, columnNumber);
string message = Utility.NetworkProtocolUtility.CreateActivateTokenMessage(filePath + fileName, lineNumber, columnNumber);
Utility.AsynchronousClient.Send(message);
}
@@ -371,8 +393,7 @@ namespace CoatiSoftware.CoatiPlugin
}
else if(menuCommand.CommandID.ID == (int)PkgCmdIDList.cmdidCoatiCreateProject)
{
// CreateCoatiProjectOld();
// show hint to use CDB methad (only for CDB beta) and create project on ok
Wizard.WindowMessage windowMessage = new Wizard.WindowMessage();
windowMessage.Title = "Hint";
windowMessage.Message = "Consider using 'Create CDB' if errors arise during Coati's indexing. This will become standard in the future.";
@@ -383,6 +404,7 @@ namespace CoatiSoftware.CoatiPlugin
}
}
// will be removed once the CDB project creation is fully integrated
private void CreateCoatiProjectOld()
{
DTE dte = (DTE)GetService(typeof(DTE));
@@ -401,7 +423,7 @@ namespace CoatiSoftware.CoatiPlugin
if (solutionName.Length > 0)
{
string message = NetworkProtocolUtility.createCreateProjectMessage(solutionName);
string message = Utility.NetworkProtocolUtility.CreateCreateProjectMessage(solutionName);
Utility.AsynchronousClient.Send(message);
}
@@ -413,16 +435,30 @@ namespace CoatiSoftware.CoatiPlugin
private void OnNetworkReadCallback(string message)
{
NetworkProtocolUtility.CursorPosition cursorPosition = NetworkProtocolUtility.parseSetCursorMessage(message);
if (cursorPosition.Valid)
{
cursorPosition.ColumnNumber += 1; // VS counts columns starting at 1, coati starts at 0
DTE dte = (DTE)GetService(typeof(DTE));
if (Utility.FileUtility.OpenSourceFile(dte, cursorPosition.FilePath))
{
Utility.FileUtility.GoToLine(dte, cursorPosition.LineNumber, cursorPosition.ColumnNumber);
Utility.NetworkProtocolUtility.MESSAGE_TYPE messageType = Utility.NetworkProtocolUtility.GetMessageType(message);
Utility.SystemUtility.GetWindowFocus();
if(messageType == Utility.NetworkProtocolUtility.MESSAGE_TYPE.MOVE_CURSOR)
{
Utility.NetworkProtocolUtility.CursorPosition cursorPosition = Utility.NetworkProtocolUtility.ParseSetCursorMessage(message);
if (cursorPosition.Valid)
{
cursorPosition.ColumnNumber += 1; // VS counts columns starting at 1, coati starts at 0
DTE dte = (DTE)GetService(typeof(DTE));
if (Utility.FileUtility.OpenSourceFile(dte, cursorPosition.FilePath))
{
Utility.FileUtility.GoToLine(dte, cursorPosition.LineNumber, cursorPosition.ColumnNumber);
Utility.SystemUtility.GetWindowFocus();
}
}
}
else if(messageType == Utility.NetworkProtocolUtility.MESSAGE_TYPE.CREATE_CDB)
{
if(_validSolutionLoaded)
{
DTE dte = (DTE)GetService(typeof(DTE));
CreateCompilationDatabase(dte);
}
}
}
@@ -521,7 +557,7 @@ namespace CoatiSoftware.CoatiPlugin
string solutionName = System.IO.Path.GetFileNameWithoutExtension(dte.Solution.FullName);
window.m_solutionFileName = solutionName;
bool containsCFiles = true; // Utility.SolutionUtility.ContainsCFiles(dte); // takes ridiculously long
bool containsCFiles = true; // Utility.SolutionUtility.ContainsCFiles(dte); // takes ridiculously long, I'd rather just display the option by default
window.m_containsCFiles = containsCFiles;
window.UpdateGUI();
@@ -1,172 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CoatiSoftware.CoatiPlugin
{
class NetworkProtocolUtility
{
private static string s_divider = ">>";
private static string s_setActiveTokenPrefix = "setActiveToken";
private static string s_moveCursorPrefix = "moveCursor";
private static string s_endOfMessageToken = "<EOM>";
private static string s_createProjectPrefix = "createProject";
private static string s_createCDBProjectPrefix = "createCDBProject";
private static string s_ideId = "vs";
public class CursorPosition
{
private string _filePath = "";
private int _lineNumber = 0;
private int _columnNumber = 0;
private bool _valid = false;
public string FilePath
{
get { return _filePath; }
set { _filePath = value; }
}
public int LineNumber
{
get { return _lineNumber; }
set { _lineNumber = value; }
}
public int ColumnNumber
{
get { return _columnNumber; }
set { _columnNumber = value; }
}
public bool Valid
{
get { return _valid; }
set { _valid = value; }
}
}
public static string createActivateTokenMessage(string filePath, int lineNumber, int columnNumber)
{
string message = s_setActiveTokenPrefix;
message += s_divider;
message += filePath;
message += s_divider;
message += lineNumber.ToString();
message += s_divider;
message += columnNumber.ToString();
message += s_endOfMessageToken;
return message;
}
public static string createCreateProjectMessage(string solutionPath)
{
string message = s_createProjectPrefix;
message += s_divider;
message += solutionPath;
message += s_divider;
message += s_ideId;
message += s_endOfMessageToken;
return message;
}
public static string createCreateProjectMessage(string cdbPath, List<string> headerPaths)
{
string message = s_createCDBProjectPrefix;
message += s_divider;
message += cdbPath;
message += s_divider;
foreach(string path in headerPaths)
{
message += path;
message += s_divider;
}
message += s_endOfMessageToken;
return message;
}
public static CursorPosition parseSetCursorMessage(string message)
{
CursorPosition result = new CursorPosition();
// remove eof token right away
if(message.IndexOf(s_endOfMessageToken) > -1)
{
message = message.Substring(0, message.IndexOf(s_endOfMessageToken));
}
int pos = message.IndexOf(s_moveCursorPrefix);
if (pos < 0)
{
return result;
}
message = message.Substring(pos + s_moveCursorPrefix.Length + s_divider.Length);
pos = message.IndexOf(s_divider);
if (pos < 0)
{
return result;
}
string filePath = message.Substring(0, pos);
result.FilePath = filePath;
message = message.Substring(pos+2);
pos = message.IndexOf(s_divider);
if (pos < 0)
{
return result;
}
string sLineNumber = message.Substring(0, pos);
int lineNumber = 0;
bool valid = int.TryParse(sLineNumber, out lineNumber);
if (valid)
{
result.LineNumber = lineNumber;
}
message = message.Substring(pos+2);
string sColumnNumber = message;
int columnNumber = 0;
valid = int.TryParse(sColumnNumber, out columnNumber);
if (valid)
{
result.ColumnNumber = columnNumber;
result.Valid = true;
}
return result;
}
}
}
@@ -0,0 +1,237 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CoatiSoftware.CoatiPlugin.Utility
{
class NetworkProtocolUtility
{
private static string s_divider = ">>";
private static string s_setActiveTokenPrefix = "setActiveToken";
private static string s_moveCursorPrefix = "moveCursor";
private static string s_endOfMessageToken = "<EOM>";
private static string s_createProjectPrefix = "createProject";
private static string s_createCDBProjectPrefix = "createCDBProject";
private static string s_ideId = "vs";
private static string s_createCDB = "createCDB";
public enum MESSAGE_TYPE
{
UNKNOWN = 0,
MOVE_CURSOR,
CREATE_CDB
}
public class CursorPosition
{
private string _filePath = "";
private int _lineNumber = 0;
private int _columnNumber = 0;
private bool _valid = false;
public string FilePath
{
get { return _filePath; }
set { _filePath = value; }
}
public int LineNumber
{
get { return _lineNumber; }
set { _lineNumber = value; }
}
public int ColumnNumber
{
get { return _columnNumber; }
set { _columnNumber = value; }
}
public bool Valid
{
get { return _valid; }
set { _valid = value; }
}
}
public static string CreateActivateTokenMessage(string filePath, int lineNumber, int columnNumber)
{
string message = s_setActiveTokenPrefix;
message += s_divider;
message += filePath;
message += s_divider;
message += lineNumber.ToString();
message += s_divider;
message += columnNumber.ToString();
message += s_endOfMessageToken;
return message;
}
public static string CreateCreateProjectMessage(string solutionPath)
{
string message = s_createProjectPrefix;
message += s_divider;
message += solutionPath;
message += s_divider;
message += s_ideId;
message += s_endOfMessageToken;
return message;
}
public static string CreateCreateProjectMessage(string cdbPath, List<string> headerPaths)
{
string message = s_createCDBProjectPrefix;
message += s_divider;
message += cdbPath;
message += s_divider;
foreach(string path in headerPaths)
{
message += path;
message += s_divider;
}
message += s_ideId;
message += s_endOfMessageToken;
return message;
}
public static MESSAGE_TYPE GetMessageType(string message)
{
List<string> tokens = GetMessageTokens(message);
if (tokens.Count > 0)
{
if(tokens[0] == s_createCDB)
{
return MESSAGE_TYPE.CREATE_CDB;
}
else if(tokens[0] == s_moveCursorPrefix)
{
return MESSAGE_TYPE.MOVE_CURSOR;
}
else
{
return MESSAGE_TYPE.UNKNOWN;
}
}
return MESSAGE_TYPE.UNKNOWN;
}
public static CursorPosition ParseSetCursorMessage(string message)
{
CursorPosition result = new CursorPosition();
List<string> tokens = GetMessageTokens(message);
if(tokens.Count != 4)
{
Logging.Logging.LogError("Invalid message: " + message);
Logging.Logging.LogError("Invalid token count for 'move cursor' message. Expected 4, but got " + tokens.Count.ToString());
return result;
}
if(tokens[0] != s_moveCursorPrefix)
{
Logging.Logging.LogError("Invalid message: " + message);
Logging.Logging.LogError("Invalid message type. Expected '" + s_moveCursorPrefix + "' but got '" + tokens[0] + "'");
return result;
}
result.FilePath = tokens[1];
int lineNumber = 0;
if(int.TryParse(tokens[2], out lineNumber))
{
result.LineNumber = lineNumber;
}
else
{
Logging.Logging.LogError("Invalid message: " + message);
Logging.Logging.LogError("Failed to parse line number");
return result;
}
int columnNumber = 0;
if(int.TryParse(tokens[3], out columnNumber))
{
result.ColumnNumber = columnNumber;
}
else
{
Logging.Logging.LogError("Invalid message: " + message);
Logging.Logging.LogError("Failed to parse column number");
return result;
}
result.Valid = true;
return result;
}
private static List<string> GetMessageTokens(string message)
{
List<string> tokens = new List<string>();
if(RemoveEOMString(ref message))
{
tokens = message.Split(s_divider.ToCharArray()).ToList();
// removing empty strings
List<string> cleanTokens = new List<string>();
foreach(string token in tokens)
{
if(token.Length > 0)
{
cleanTokens.Add(token);
}
}
tokens = cleanTokens;
}
else
{
Logging.Logging.LogError("");
}
return tokens;
}
// returns false if no EOM string was found
private static bool RemoveEOMString(ref string message)
{
if (message.IndexOf(s_endOfMessageToken) > -1)
{
message = message.Substring(0, message.IndexOf(s_endOfMessageToken));
return true;
}
return false;
}
}
}
@@ -173,23 +173,35 @@ namespace CoatiSoftware.CoatiPlugin.Utility
Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IAsyncResult ar = client.BeginConnect(remoteEP, new AsyncCallback(ConnectCallback), client);
if (!connectDone.WaitOne(2000))
try
{
client.EndConnect(ar);
client.Shutdown(SocketShutdown.Both);
client.Close();
IAsyncResult ar = client.BeginConnect(remoteEP, new AsyncCallback(ConnectCallback), client);
if (!connectDone.WaitOne(2000))
{
client.EndConnect(ar);
client.Shutdown(SocketShutdown.Both);
client.Close();
Logging.Logging.LogWarning("Connection timed out, message was not sent");
Logging.Logging.LogWarning("Connection timed out, message was not sent");
return;
return;
}
Send(client, message);
sendDone.WaitOne();
}
catch(Exception e)
{
Logging.Logging.LogError("Exception: " + e.Message);
}
finally
{
if(client.Connected)
{
client.Shutdown(SocketShutdown.Both);
client.Close();
}
}
Send(client, message);
sendDone.WaitOne();
client.Shutdown(SocketShutdown.Both);
client.Close();
}
private static void ConnectCallback(IAsyncResult ar)
@@ -31,7 +31,7 @@ namespace CoatiSoftware.CoatiPlugin.Utility
public string Name = "";
public Project Project = null;
public Object Foo = null;
public Object UserData = null;
public virtual NodeType GetNodeType() { throw (new NotImplementedException()); }
}
@@ -108,14 +108,14 @@ namespace CoatiSoftware.CoatiPlugin.Wizard
if(node.GetNodeType() == Utility.SolutionUtility.SolutionStructure.Node.NodeType.PROJECT)
{
TreeNode treeNode = new TreeNode(node.Name);
node.Foo = treeNode;
node.UserData = treeNode;
treeViewProjects.Nodes.Add(treeNode);
}
else if(node.GetNodeType() == Utility.SolutionUtility.SolutionStructure.Node.NodeType.FOLDER)
{
TreeNode[] subNodes = GetSubNodes(node as Utility.SolutionUtility.SolutionStructure.FolderNode);
TreeNode treeNode = new TreeNode(node.Name, subNodes);
node.Foo = treeNode;
node.UserData = treeNode;
treeViewProjects.Nodes.Add(treeNode);
}
}
@@ -130,14 +130,14 @@ namespace CoatiSoftware.CoatiPlugin.Wizard
if(node.GetNodeType() == Utility.SolutionUtility.SolutionStructure.Node.NodeType.PROJECT)
{
TreeNode treeNode = new TreeNode(node.Name);
node.Foo = treeNode;
node.UserData = treeNode;
result.Add(treeNode);
}
else if(node.GetNodeType() == Utility.SolutionUtility.SolutionStructure.Node.NodeType.FOLDER)
{
TreeNode[] subNodes = GetSubNodes(node as Utility.SolutionUtility.SolutionStructure.FolderNode);
TreeNode treeNode = new TreeNode(node.Name, subNodes);
node.Foo = treeNode;
node.UserData = treeNode;
result.Add(treeNode);
}
}
@@ -297,12 +297,12 @@ namespace CoatiSoftware.CoatiPlugin.Wizard
{
Utility.SolutionUtility.SolutionStructure.Node node = nodeStack.Pop();
bool include = (node.Foo as TreeNode).Checked;
bool include = (node.UserData as TreeNode).Checked;
string name = node.Name;
if(node.GetNodeType() == Utility.SolutionUtility.SolutionStructure.Node.NodeType.PROJECT
&& (node.Foo as TreeNode).Checked == true)
&& (node.UserData as TreeNode).Checked == true)
{
solutionProjects.Add(node.Project);
}
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
<Metadata>
<Identity Id="acf15780-03b5-440e-a41e-db79b7043fc2" Version="0.8.18" Language="en-US" Publisher="Coati Software OG" />
<Identity Id="acf15780-03b5-440e-a41e-db79b7043fc2" Version="0.8.25" Language="en-US" Publisher="Coati Software OG" />
<DisplayName>CoatiPlugin</DisplayName>
<Description xml:space="preserve">This package allows Coati to communicate with Visual Studio and vice versa.</Description>
<MoreInfo>https://www.coati.io/</MoreInfo>